package logic import ( "context" "math" "sync" "big-qmt/go-client/config" "big-qmt/go-client/sdk" ) var posDip = struct { mu sync.Mutex store map[string]dipWatch }{store: map[string]dipWatch{}} var peakMu sync.Mutex var peakGrids = map[string]int{} func peakKey(code, leg string) string { return code + "|" + leg } func positionCodes(positions []sdk.Position) map[string]struct{} { out := map[string]struct{}{} for _, p := range positions { if p.Volume <= 0 { continue } code := p.StockCode if code != "" { out[code] = struct{}{} } } return out } func managePositions(ctx context.Context, client *sdk.Client, books *OrderBook, ticks map[string]sdk.Tick, positions []sdk.Position, buys, sells map[string]struct{}, marketOK bool, buyBudget *float64) { if positions == nil { logf("ERROR", "[ZT][持仓] 持仓查询失败,本轮跳过") return } state := getState() if state.LoadError != "" { logf("ERROR", "[ZT][持仓] 状态文件异常,本轮停止交易: %s", state.LoadError) return } before := map[string]struct{}{} for _, code := range state.Codes() { before[code] = struct{}{} } if ticks == nil { ticks = map[string]sdk.Tick{} } logf("INFO", "[ZT][持仓] 开始处理 %d 只", len(positions)) type row struct { volume, usable int avg, price float64 stock string item *SymbolState } 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(state, code, pos.Volume, pos.OpenPrice, buys, sells, books) if pos.Volume <= 0 { continue } price := ticks[code].LastPrice rows = append(rows, row{stock: code, volume: pos.Volume, usable: pos.CanUseVolume, avg: pos.OpenPrice, price: price, item: item}) } for _, code := range state.Codes() { if _, ok := seen[code]; !ok { syncItem(state, code, 0, 0, buys, sells, books) } } after := map[string]struct{}{} for _, code := range state.Codes() { after[code] = struct{}{} } for code := range before { if _, ok := after[code]; !ok { forget(code) } } for _, r := range rows { if r.item == nil || r.item.Pending != "" { continue } if r.avg <= 0 || r.price <= 0 || r.volume%100 != 0 { continue } if r.volume != r.item.BaseQty+r.item.AddQty { logf("INFO", "[ZT][持仓] %s 数量异常,底仓=%d 补仓=%d 现有=%d", r.stock, r.item.BaseQty, r.item.AddQty, r.volume) continue } holdingAdd := r.item.AddQty > 0 legName := "底仓" if holdingAdd { legName = "补仓腿" } logf("INFO", "[ZT][持仓] %s 现价=%.2f 成本=%.2f 可用=%d %s", r.stock, r.price, r.avg, r.usable, legName) if holdingAdd { addPnL := -999.0 if r.item.AddCost > 0 { addPnL = (r.price - r.item.AddCost) / r.item.AddCost * 100 } if retreated(r.item, "add", addPnL) { sellLeg(ctx, client, books, r.item, r.usable, r.item.AddQty, "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 r.item.AddQty <= 0 && r.item.AddCost <= 0 && basePnL <= config.Account.LossTriggerPct { addOnRebound(ctx, client, books, r.item, r.price, marketOK, buyBudget) } } // 首次没有状态文件时,本轮已将启动前持仓全部接管为底仓。 state.completeBootstrap() state.Save() } func syncItem(state *ZTState, code string, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) *SymbolState { item := state.Get(code) if item == nil { if volume > 0 { logf("ERROR", "[ZT][持仓] %s 无本地状态,跳过", code) } return nil } switch item.Pending { case pendingBaseOpening: syncOpen(state, item, volume, avgPrice, buys, books) case pendingAdd: syncAdd(state, item, volume, avgPrice, buys, books) case pendingSellAdd: syncSellAdd(state, item, volume, avgPrice, sells, books) case pendingSellBase: syncSellBase(state, item, volume, avgPrice, sells, books) default: if volume <= 0 { state.Remove(code) logf("INFO", "[ZT][持仓] %s 已无持仓,清除状态", code) return nil } } return state.Get(code) } func syncOpen(state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *OrderBook) { if volume > 0 { item.BaseQty, item.BaseCost = volume, avgPrice } if books.sideBusy(item.Code, sideBuy, buys) { return } if volume <= 0 { state.Remove(item.Code) logf("INFO", "[ZT][委托] %s 开仓委托已失效,允许重新开仓", item.Code) return } clearPending(item) logf("INFO", "[ZT][持仓] %s 开仓确认 数量=%d 成本=%.2f", item.Code, item.BaseQty, item.BaseCost) } func syncAdd(state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *OrderBook) { if volume > item.BaseQty { item.AddQty = volume - item.BaseQty if item.AddQty > 0 { item.AddCost = math.Max(0, (avgPrice*float64(volume)-item.BaseCost*float64(item.BaseQty))/float64(item.AddQty)) } } if books.sideBusy(item.Code, sideBuy, buys) { return } if volume <= 0 { state.Remove(item.Code) logf("INFO", "[ZT][委托] %s 补仓后无持仓,清除状态", item.Code) return } if volume <= item.BaseQty { item.AddQty = 0 item.AddCost = 0 logf("INFO", "[ZT][持仓] %s 补仓未成交,回退底仓", item.Code) } clearPending(item) } func syncSellAdd(state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *OrderBook) { if volume <= 0 { if !books.sideBusy(item.Code, sideSell, sells) { state.Remove(item.Code) logf("INFO", "[ZT][委托] %s 卖出后已无持仓,清除状态", item.Code) } return } if volume <= item.BaseQty { item.BaseQty, item.BaseCost = volume, avgPrice item.AddQty = 0 peakMu.Lock() delete(peakGrids, peakKey(item.Code, "add")) peakMu.Unlock() } else { item.AddQty = volume - item.BaseQty } if !books.sideBusy(item.Code, sideSell, sells) { clearPending(item) } } func syncSellBase(state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *OrderBook) { if volume <= 0 { if !books.sideBusy(item.Code, sideSell, sells) { state.Remove(item.Code) logf("INFO", "[ZT][委托] %s 卖出后已无持仓,清除状态", item.Code) } return } item.BaseQty, item.BaseCost = volume, avgPrice if !books.sideBusy(item.Code, sideSell, sells) { clearPending(item) } } func addOnRebound(ctx context.Context, client *sdk.Client, books *OrderBook, item *SymbolState, price float64, marketOK bool, buyBudget *float64) { if !marketOK || !dipTriggered(&posDip.mu, posDip.store, "补仓", item.Code, price) { return } volume := calcBuyVolume(price, config.Account.BuyValue) estimated := price * float64(volume) if buyBudget == nil || estimated > *buyBudget { logf("INFO", "[ZT][补仓] %s 可用买入预算不足,需要=%.2f", item.Code, estimated) return } orderID := newOrderTag("add") if books.place(ctx, client, sideBuy, item.Code, volume, orderID) { item.AddCost = price setPending(item, pendingAdd, orderID) *buyBudget -= estimated getState().Save() logf("INFO", "[ZT][补仓] %s 买入 %d 股", item.Code, volume) } } func retreated(item *SymbolState, 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 logf("INFO", "[ZT][止盈] %s %s峰值网格=%d", item.Code, leg, grid) return false } return grid < peak } func sellLeg(ctx context.Context, client *sdk.Client, books *OrderBook, item *SymbolState, usable, volume int, leg string, pnl float64) { volume -= volume % 100 if volume <= 0 || usable < volume { logf("INFO", "[ZT][止盈] %s 可用股数不足,需要=%d 可用=%d", item.Code, volume, usable) return } orderID := newOrderTag(leg) if !books.place(ctx, client, sideSell, item.Code, volume, orderID) { return } if leg == "add" { setPending(item, pendingSellAdd, orderID) } else { setPending(item, pendingSellBase, orderID) } getState().Save() logf("INFO", "[ZT][止盈] %s 卖出 %d 股,%s腿盈利=%.2f%%", item.Code, volume, leg, pnl) } func forget(code string) { openDip.mu.Lock() delete(openDip.store, code) openDip.mu.Unlock() posDip.mu.Lock() delete(posDip.store, code) posDip.mu.Unlock() peakMu.Lock() delete(peakGrids, peakKey(code, "base")) delete(peakGrids, peakKey(code, "add")) peakMu.Unlock() }