This commit is contained in:
2026-08-25 22:35:44 +08:00
parent ec58641d09
commit f14423418a
23 changed files with 738 additions and 680 deletions

View File

@@ -5,6 +5,7 @@ import (
"math"
"sync"
"big-qmt/go-client/config"
"big-qmt/go-client/sdk"
)
@@ -24,7 +25,7 @@ func positionCodes(positions []sdk.Position) map[string]struct{} {
if p.Volume <= 0 {
continue
}
code := normalizeCode(p.StockCode, "")
code := p.StockCode
if code != "" {
out[code] = struct{}{}
}
@@ -32,18 +33,14 @@ func positionCodes(positions []sdk.Position) map[string]struct{} {
return out
}
func managePositions(ctx context.Context, client *sdk.Client, books *OrderBook, cfg Config, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool) {
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(cfg)
if !books.cancelExpired(ctx, client, cfg) {
logf("ERROR", "[ZT][持仓] 委托查询失败,本轮跳过")
return
}
buys, sells, ok := books.activeSets(ctx, client, cfg)
if !ok {
state := getState()
if state.LoadError != "" {
logf("ERROR", "[ZT][持仓] 状态文件异常,本轮停止交易: %s", state.LoadError)
return
}
before := map[string]struct{}{}
@@ -63,12 +60,12 @@ func managePositions(ctx context.Context, client *sdk.Client, books *OrderBook,
rows := make([]row, 0, len(positions))
seen := map[string]struct{}{}
for _, pos := range positions {
code := normalizeCode(pos.StockCode, "")
code := pos.StockCode
if code == "" {
continue
}
seen[code] = struct{}{}
item := syncItem(cfg, state, code, pos.Volume, pos.OpenPrice, buys, sells, books)
item := syncItem(state, code, pos.Volume, pos.OpenPrice, buys, sells, books)
if pos.Volume <= 0 {
continue
}
@@ -77,7 +74,7 @@ func managePositions(ctx context.Context, client *sdk.Client, books *OrderBook,
}
for _, code := range state.Codes() {
if _, ok := seen[code]; !ok {
syncItem(cfg, state, code, 0, 0, buys, sells, books)
syncItem(state, code, 0, 0, buys, sells, books)
}
}
after := map[string]struct{}{}
@@ -111,8 +108,8 @@ func managePositions(ctx context.Context, client *sdk.Client, books *OrderBook,
if r.item.AddCost > 0 {
addPnL = (r.price - r.item.AddCost) / r.item.AddCost * 100
}
if retreated(cfg, r.item, "add", addPnL) {
sellLeg(ctx, client, books, cfg, r.item, r.usable, r.item.AddQty, "add", addPnL)
if retreated(r.item, "add", addPnL) {
sellLeg(ctx, client, books, r.item, r.usable, r.item.AddQty, "add", addPnL)
}
continue
}
@@ -120,38 +117,34 @@ func managePositions(ctx context.Context, client *sdk.Client, books *OrderBook,
if r.item.BaseCost > 0 {
basePnL = (r.price - r.item.BaseCost) / r.item.BaseCost * 100
}
if retreated(cfg, r.item, "base", basePnL) {
sellLeg(ctx, client, books, cfg, r.item, r.usable, r.item.BaseQty, "base", basePnL)
} else if r.item.AddQty <= 0 && r.item.AddCost <= 0 && basePnL <= cfg.LossTriggerPct {
addOnRebound(ctx, client, books, cfg, r.item, r.price, marketOK)
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(cfg Config, state *ZTState, code string, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) *SymbolState {
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 {
if cfg.AdoptExisting && avgPrice > 0 {
item = state.Ensure(code)
item.BaseQty, item.BaseCost, item.Pending = volume, avgPrice, ""
logf("WARNING", "[ZT][持仓] %s 接管为底仓", code)
return item
}
logf("ERROR", "[ZT][持仓] %s 无本地状态,跳过", code)
}
return nil
}
switch item.Pending {
case "base_opening":
syncOpen(cfg, state, item, volume, avgPrice, buys, books)
case "add":
syncAdd(cfg, state, item, volume, avgPrice, buys, books)
case "sell_add":
syncSellAdd(cfg, state, item, volume, avgPrice, sells, books)
case "sell_base":
syncSellBase(cfg, state, item, volume, avgPrice, sells, books)
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)
@@ -162,11 +155,11 @@ func syncItem(cfg Config, state *ZTState, code string, volume int, avgPrice floa
return state.Get(code)
}
func syncOpen(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *OrderBook) {
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(cfg, item.Code, "buy", buys) {
if books.sideBusy(item.Code, sideBuy, buys) {
return
}
if volume <= 0 {
@@ -174,18 +167,18 @@ func syncOpen(cfg Config, state *ZTState, item *SymbolState, volume int, avgPric
logf("INFO", "[ZT][委托] %s 开仓委托已失效,允许重新开仓", item.Code)
return
}
item.Pending = ""
clearPending(item)
logf("INFO", "[ZT][持仓] %s 开仓确认 数量=%d 成本=%.2f", item.Code, item.BaseQty, item.BaseCost)
}
func syncAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *OrderBook) {
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(cfg, item.Code, "buy", buys) {
if books.sideBusy(item.Code, sideBuy, buys) {
return
}
if volume <= 0 {
@@ -198,12 +191,12 @@ func syncAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice
item.AddCost = 0
logf("INFO", "[ZT][持仓] %s 补仓未成交,回退底仓", item.Code)
}
item.Pending = ""
clearPending(item)
}
func syncSellAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *OrderBook) {
func syncSellAdd(state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *OrderBook) {
if volume <= 0 {
if !books.sideBusy(cfg, item.Code, "sell", sells) {
if !books.sideBusy(item.Code, sideSell, sells) {
state.Remove(item.Code)
logf("INFO", "[ZT][委托] %s 卖出后已无持仓,清除状态", item.Code)
}
@@ -218,42 +211,50 @@ func syncSellAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgP
} else {
item.AddQty = volume - item.BaseQty
}
if !books.sideBusy(cfg, item.Code, "sell", sells) {
item.Pending = ""
if !books.sideBusy(item.Code, sideSell, sells) {
clearPending(item)
}
}
func syncSellBase(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *OrderBook) {
func syncSellBase(state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *OrderBook) {
if volume <= 0 {
if !books.sideBusy(cfg, item.Code, "sell", sells) {
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(cfg, item.Code, "sell", sells) {
item.Pending = ""
if !books.sideBusy(item.Code, sideSell, sells) {
clearPending(item)
}
}
func addOnRebound(ctx context.Context, client *sdk.Client, books *OrderBook, cfg Config, item *SymbolState, price float64, marketOK bool) {
if !marketOK || !dipTriggered(&posDip.mu, posDip.store, cfg, "补仓", item.Code, price) {
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
}
if books.place(ctx, client, cfg, "buy", item.Code, item.BaseQty, newOrderTag("add")) {
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
item.Pending = "add"
getState(cfg).Save()
logf("INFO", "[ZT][补仓] %s 买入 %d 股", item.Code, item.BaseQty)
setPending(item, pendingAdd, orderID)
*buyBudget -= estimated
getState().Save()
logf("INFO", "[ZT][补仓] %s 买入 %d 股", item.Code, volume)
}
}
func retreated(cfg Config, item *SymbolState, leg string, pnl float64) bool {
if pnl < cfg.MinProfitPct {
func retreated(item *SymbolState, leg string, pnl float64) bool {
if pnl < config.Account.MinProfitPct {
return false
}
grid := int(math.Floor(pnl / cfg.GridStepPct))
grid := int(math.Floor(pnl / config.Account.GridStepPct))
key := peakKey(item.Code, leg)
peakMu.Lock()
defer peakMu.Unlock()
@@ -266,21 +267,22 @@ func retreated(cfg Config, item *SymbolState, leg string, pnl float64) bool {
return grid < peak
}
func sellLeg(ctx context.Context, client *sdk.Client, books *OrderBook, cfg Config, item *SymbolState, usable, volume int, leg string, pnl float64) {
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
}
if !books.place(ctx, client, cfg, "sell", item.Code, volume, newOrderTag(leg)) {
orderID := newOrderTag(leg)
if !books.place(ctx, client, sideSell, item.Code, volume, orderID) {
return
}
if leg == "add" {
item.Pending = "sell_add"
setPending(item, pendingSellAdd, orderID)
} else {
item.Pending = "sell_base"
setPending(item, pendingSellBase, orderID)
}
getState(cfg).Save()
getState().Save()
logf("INFO", "[ZT][止盈] %s 卖出 %d 股,%s腿盈利=%.2f%%", item.Code, volume, leg, pnl)
}