Files
big-qmt/go-client/apps/zt/logic/positions.go

243 lines
6.9 KiB
Go

package logic
import (
"context"
"math"
"sync"
"big-qmt/go-client/config"
"big-qmt/go-client/libs"
"big-qmt/go-client/sdk"
)
var peakMu sync.Mutex
var peakGrids = map[string]int{}
func peakKey(code, leg string) string { return code + "|" + leg }
func calcBuyVolume(price, value float64) int {
return libs.CalcBuyVolume(price, value)
}
func stateCodes(state *State) []string {
state.mu.Lock()
defer state.mu.Unlock()
return append([]string(nil), state.Codes...)
}
func managePositions(ctx context.Context, client *sdk.Client, books *OrderBook, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool, buyBudget *float64) {
if positions == nil || QuantState == nil {
logf("ERROR", "[ZT][持仓] 持仓或状态不可用,本轮跳过")
return
}
buys, sells, ok := books.activeSets(ctx, client)
if !ok {
return
}
before := map[string]struct{}{}
for _, code := range stateCodes(QuantState) {
before[code] = struct{}{}
}
if ticks == nil {
ticks = map[string]sdk.Tick{}
}
type row struct {
volume, usable int
avg, price float64
stock string
item *StateItem
}
rows := make([]row, 0, len(positions))
seen := map[string]struct{}{}
for _, pos := range positions {
code := pos.StockCode
if code == "" {
continue
}
seen[code] = struct{}{}
item := syncItem(QuantState, code, pos.Volume, pos.OpenPrice, buys, sells, books)
if pos.Volume > 0 {
rows = append(rows, row{stock: code, volume: pos.Volume, usable: pos.CanUseVolume, avg: pos.OpenPrice, price: ticks[code].LastPrice, item: item})
}
}
for _, code := range stateCodes(QuantState) {
if _, ok := seen[code]; !ok {
syncItem(QuantState, code, 0, 0, buys, sells, books)
}
}
after := map[string]struct{}{}
for _, code := range stateCodes(QuantState) {
after[code] = struct{}{}
}
for code := range before {
if _, ok := after[code]; !ok {
forget(code)
}
}
for _, r := range rows {
if r.item == nil || r.item.BaseStatus == StatusIng || r.item.AddedStatus == StatusIng || r.avg <= 0 || r.price <= 0 || r.volume%100 != 0 {
continue
}
if r.volume != r.item.BaseQty+r.item.AddedQty {
logf("INFO", "[ZT][持仓] %s 数量异常,底仓=%d 补仓=%d 现有=%d", r.stock, r.item.BaseQty, r.item.AddedQty, r.volume)
continue
}
if r.item.AddedQty > 0 {
addPnL := -999.0
if r.item.AddedCost > 0 {
addPnL = (r.price - r.item.AddedCost) / r.item.AddedCost * 100
}
if retreated(r.item, "add", addPnL) {
sellLeg(ctx, client, books, r.item, r.usable, r.item.AddedQty, "add", addPnL)
}
continue
}
basePnL := -999.0
if r.item.BaseCost > 0 {
basePnL = (r.price - r.item.BaseCost) / r.item.BaseCost * 100
}
if retreated(r.item, "base", basePnL) {
sellLeg(ctx, client, books, r.item, r.usable, r.item.BaseQty, "base", basePnL)
} else if basePnL <= config.Account.LossTriggerPct {
addOnRebound(ctx, client, books, r.item, r.price, marketOK, buyBudget)
}
}
if err := QuantState.Save(); err != nil {
logf("ERROR", "%v", err)
}
}
func syncItem(state *State, code string, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) *StateItem {
item, err := state.Get(code)
if err != nil {
if volume > 0 {
logf("ERROR", "[ZT][持仓] %s 无本地状态,跳过", code)
}
return nil
}
if item.BaseStatus == StatusIng {
syncBase(state, item, volume, avgPrice, buys, sells, books)
} else if item.AddedStatus == StatusIng {
syncAdded(state, item, volume, avgPrice, buys, sells, books)
} else if volume <= 0 {
state.Delete(code)
return nil
}
item, _ = state.Get(code)
return item
}
func syncBase(state *State, item *StateItem, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) {
if books.sideBusy(item.Code, sideBuy, buys) || books.sideBusy(item.Code, sideSell, sells) {
return
}
if volume <= 0 {
state.Delete(item.Code)
return
}
item.BaseQty = volume - item.AddedQty
if item.BaseQty < 0 {
item.BaseQty, item.AddedQty, item.AddedCost, item.AddedStatus = volume, 0, 0, StatusNone
}
item.BaseCost = avgPrice
item.BaseStatus = StatusOk
state.Set(item)
}
func syncAdded(state *State, item *StateItem, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) {
if books.sideBusy(item.Code, sideBuy, buys) || books.sideBusy(item.Code, sideSell, sells) {
return
}
if volume <= 0 {
state.Delete(item.Code)
return
}
if volume > item.BaseQty {
item.AddedQty = volume - item.BaseQty
item.AddedCost = math.Max(0, (avgPrice*float64(volume)-item.BaseCost*float64(item.BaseQty))/float64(item.AddedQty))
item.AddedStatus = StatusOk
} else {
item.BaseQty, item.BaseCost = volume, avgPrice
item.AddedQty, item.AddedCost, item.AddedStatus = 0, 0, StatusNone
peakMu.Lock()
delete(peakGrids, peakKey(item.Code, "add"))
peakMu.Unlock()
}
state.Set(item)
}
func addOnRebound(ctx context.Context, client *sdk.Client, books *OrderBook, item *StateItem, price float64, marketOK bool, buyBudget *float64) {
if !marketOK || PosbuyWatch == nil || !PosbuyWatch.Triggered("补仓", item.Code, price) {
return
}
volume := libs.CalcBuyVolume(price, config.Account.BuyValue)
estimated := price * float64(volume)
if volume <= 0 || buyBudget == nil || estimated > *buyBudget {
return
}
orderID := newOrderTag("add")
if books.place(ctx, client, sideBuy, item.Code, volume, orderID) {
item.AddedOrderId, item.AddedQty, item.AddedCost, item.AddedStatus = orderID, volume, price, StatusIng
item.AddedNum++
QuantState.Set(item)
*buyBudget -= estimated
if err := QuantState.Save(); err != nil {
logf("ERROR", "%v", err)
}
}
}
func retreated(item *StateItem, leg string, pnl float64) bool {
if pnl < config.Account.MinProfitPct {
return false
}
grid := int(math.Floor(pnl / config.Account.GridStepPct))
key := peakKey(item.Code, leg)
peakMu.Lock()
defer peakMu.Unlock()
peak, ok := peakGrids[key]
if !ok || grid > peak {
peakGrids[key] = grid
return false
}
return grid < peak
}
func sellLeg(ctx context.Context, client *sdk.Client, books *OrderBook, item *StateItem, usable, volume int, leg string, pnl float64) {
volume -= volume % 100
if volume <= 0 || usable < volume {
return
}
orderID := newOrderTag(leg)
if !books.place(ctx, client, sideSell, item.Code, volume, orderID) {
return
}
if leg == "add" {
item.AddedOrderId, item.AddedStatus = orderID, StatusIng
} else {
item.BaseOrderId, item.BaseStatus = orderID, StatusIng
}
QuantState.Set(item)
if err := QuantState.Save(); err != nil {
logf("ERROR", "%v", err)
}
logf("INFO", "[ZT][止盈] %s 卖出 %d 股,%s腿盈利=%.2f%%", item.Code, volume, leg, pnl)
}
func forget(code string) {
if OpenWatch != nil {
OpenWatch.mu.Lock()
delete(OpenWatch.Data, code)
OpenWatch.mu.Unlock()
}
if PosbuyWatch != nil {
PosbuyWatch.mu.Lock()
delete(PosbuyWatch.Data, code)
PosbuyWatch.mu.Unlock()
}
peakMu.Lock()
delete(peakGrids, peakKey(code, "base"))
delete(peakGrids, peakKey(code, "add"))
peakMu.Unlock()
}