dev 2
This commit is contained in:
298
go-client/apps/zt/logic/positions.go
Normal file
298
go-client/apps/zt/logic/positions.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"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 := normalizeCode(p.StockCode, "")
|
||||
if code != "" {
|
||||
out[code] = 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) {
|
||||
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 {
|
||||
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 := normalizeCode(pos.StockCode, "")
|
||||
if code == "" {
|
||||
continue
|
||||
}
|
||||
seen[code] = struct{}{}
|
||||
item := syncItem(cfg, 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(cfg, 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(cfg, r.item, "add", addPnL) {
|
||||
sellLeg(ctx, client, books, cfg, 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(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)
|
||||
}
|
||||
}
|
||||
state.Save()
|
||||
}
|
||||
|
||||
func syncItem(cfg Config, 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)
|
||||
default:
|
||||
if volume <= 0 {
|
||||
state.Remove(code)
|
||||
logf("INFO", "[ZT][持仓] %s 已无持仓,清除状态", code)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return state.Get(code)
|
||||
}
|
||||
|
||||
func syncOpen(cfg Config, 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) {
|
||||
return
|
||||
}
|
||||
if volume <= 0 {
|
||||
state.Remove(item.Code)
|
||||
logf("INFO", "[ZT][委托] %s 开仓委托已失效,允许重新开仓", item.Code)
|
||||
return
|
||||
}
|
||||
item.Pending = ""
|
||||
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) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
item.Pending = ""
|
||||
}
|
||||
|
||||
func syncSellAdd(cfg Config, 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) {
|
||||
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(cfg, item.Code, "sell", sells) {
|
||||
item.Pending = ""
|
||||
}
|
||||
}
|
||||
|
||||
func syncSellBase(cfg Config, 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) {
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return
|
||||
}
|
||||
if books.place(ctx, client, cfg, "buy", item.Code, item.BaseQty, newOrderTag("add")) {
|
||||
item.AddCost = price
|
||||
item.Pending = "add"
|
||||
getState(cfg).Save()
|
||||
logf("INFO", "[ZT][补仓] %s 买入 %d 股", item.Code, item.BaseQty)
|
||||
}
|
||||
}
|
||||
|
||||
func retreated(cfg Config, item *SymbolState, leg string, pnl float64) bool {
|
||||
if pnl < cfg.MinProfitPct {
|
||||
return false
|
||||
}
|
||||
grid := int(math.Floor(pnl / cfg.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, cfg Config, 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)) {
|
||||
return
|
||||
}
|
||||
if leg == "add" {
|
||||
item.Pending = "sell_add"
|
||||
} else {
|
||||
item.Pending = "sell_base"
|
||||
}
|
||||
getState(cfg).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()
|
||||
}
|
||||
Reference in New Issue
Block a user