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() }