package logic import ( "context" "math" "sync" "big-qmt/go-client/config" "big-qmt/go-client/libs" "big-qmt/go-client/sdk" ) const ( legBase = "base" legAdded = "add" ) var ( peakMu sync.Mutex peakGrids = make(map[string]int) ) func managePositions(_ context.Context, client *sdk.Client, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool, budget *float64) { if QuantState == nil || OrderBook == nil || positions == nil { return } if err := OrderBook.Refresh(client); err != nil { logf("ERROR", "[持仓] 刷新委托失败: %v", err) return } current := make(map[string]sdk.Position, len(positions)) for _, position := range positions { if position.StockCode != "" { current[position.StockCode] = position } } for _, code := range stateCodes() { syncPosition(code, current[code]) } for code, position := range current { managePosition(client, ticks[code], position, marketOK, budget) } saveState() } func syncPosition(code string, position sdk.Position) { item, err := QuantState.Get(code) if err != nil || orderBusy(code, "BUY") || orderBusy(code, "SELL") { return } if position.Volume <= 0 { QuantState.Delete(code) forget(code) return } if item.BaseStatus == StatusIng { item.BaseQty = max(0, position.Volume-item.AddedQty) item.BaseCost = position.OpenPrice item.BaseStatus = StatusOk } if item.AddedStatus == StatusIng { syncAdded(item, position) } QuantState.Set(item) } func syncAdded(item *StateItem, position sdk.Position) { addedQty := position.Volume - item.BaseQty if addedQty <= 0 { item.BaseQty = position.Volume item.BaseCost = position.OpenPrice item.AddedQty = 0 item.AddedCost = 0 item.AddedStatus = StatusNone clearPeak(item.Code, legAdded) return } item.AddedQty = addedQty totalCost := position.OpenPrice * float64(position.Volume) baseCost := item.BaseCost * float64(item.BaseQty) item.AddedCost = math.Max(0, (totalCost-baseCost)/float64(addedQty)) item.AddedStatus = StatusOk } func managePosition(client *sdk.Client, tick sdk.Tick, position sdk.Position, marketOK bool, budget *float64) { item, err := QuantState.Get(position.StockCode) if err != nil || tick.LastPrice <= 0 || !positionReady(item, position) { return } if item.AddedQty > 0 { pnl := profit(tick.LastPrice, item.AddedCost) if shouldSell(item.Code, legAdded, pnl) { sell(client, item, position.CanUseVolume, item.AddedQty, legAdded, pnl) } return } pnl := profit(tick.LastPrice, item.BaseCost) if shouldSell(item.Code, legBase, pnl) { sell(client, item, position.CanUseVolume, item.BaseQty, legBase, pnl) } else if pnl <= config.Account.LossTriggerPct { buyAdded(client, item, tick.LastPrice, marketOK, budget) } } func positionReady(item *StateItem, position sdk.Position) bool { return item.BaseStatus != StatusIng && item.AddedStatus != StatusIng && position.Volume > 0 && position.Volume%100 == 0 && position.Volume == item.BaseQty+item.AddedQty } func buyAdded(client *sdk.Client, item *StateItem, price float64, marketOK bool, budget *float64) { if !marketOK || budget == nil || PosbuyWatch == nil || !PosbuyWatch.Triggered("补仓", item.Code, price) { return } volume := calcBuyVolume(price, config.Account.BuyValue) amount := price * float64(volume) if volume <= 0 || amount > *budget || orderBusy(item.Code, "BUY") { return } orderID := NewOrderID(legAdded) if !OrderBook.Place(client, sdk.OpBuy, item.Code, volume, orderID) { return } item.AddedOrderId = orderID item.AddedNum++ item.AddedQty = volume item.AddedCost = price item.AddedStatus = StatusIng QuantState.Set(item) *budget -= amount saveState() } func sell(client *sdk.Client, item *StateItem, usable, volume int, leg string, pnl float64) { volume -= volume % 100 if volume <= 0 || usable < volume || orderBusy(item.Code, "SELL") { return } orderID := NewOrderID(leg) if !OrderBook.Place(client, sdk.OpSell, item.Code, volume, orderID) { return } if leg == legAdded { item.AddedOrderId = orderID item.AddedStatus = StatusIng } else { item.BaseOrderId = orderID item.BaseStatus = StatusIng } QuantState.Set(item) saveState() logf("INFO", "[止盈] %s 卖出%d股,盈利=%.2f%%", item.Code, volume, pnl) } func orderBusy(code, side string) bool { OrderBook.mu.Lock() defer OrderBook.mu.Unlock() order := OrderBook.Data[side+"-"+code] if order == nil { return false } switch order.Status { case "48", "49", "50", "51", "52", "55": return true default: return false } } func shouldSell(code, leg string, pnl float64) bool { if pnl < config.Account.MinProfitPct || config.Account.GridStepPct <= 0 { return false } grid := int(math.Floor(pnl / config.Account.GridStepPct)) key := peakKey(code, leg) peakMu.Lock() defer peakMu.Unlock() peak, tracked := peakGrids[key] if !tracked || grid > peak { peakGrids[key] = grid return false } return grid < peak } func stateCodes() []string { QuantState.mu.Lock() defer QuantState.mu.Unlock() return append([]string(nil), QuantState.Codes...) } func saveState() { if err := QuantState.Save(); err != nil { logf("ERROR", "%v", err) } } func profit(price, cost float64) float64 { if cost <= 0 { return math.Inf(-1) } return (price - cost) / cost * 100 } func calcBuyVolume(price, value float64) int { return libs.CalcBuyVolume(price, value) } func peakKey(code, leg string) string { return code + "|" + leg } func clearPeak(code, leg string) { peakMu.Lock() delete(peakGrids, peakKey(code, leg)) peakMu.Unlock() } 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() } clearPeak(code, legBase) clearPeak(code, legAdded) }