Files
big-qmt/go-client/apps/zt/logic/positions.go

238 lines
5.7 KiB
Go
Raw Normal View History

2026-08-25 18:59:18 +08:00
package logic
2026-08-25 16:40:18 +08:00
import (
"context"
"math"
"sync"
2026-08-25 22:35:44 +08:00
"big-qmt/go-client/config"
"big-qmt/go-client/libs"
2026-08-25 16:40:18 +08:00
"big-qmt/go-client/sdk"
)
2026-08-26 16:37:06 +08:00
const (
legBase = "base"
legAdded = "add"
)
2026-08-26 16:37:06 +08:00
var (
peakMu sync.Mutex
peakGrids = make(map[string]int)
)
2026-08-26 16:37:06 +08:00
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 {
2026-08-25 16:40:18 +08:00
return
}
2026-08-26 16:37:06 +08:00
if err := OrderBook.Refresh(client); err != nil {
logf("ERROR", "[持仓] 刷新委托失败: %v", err)
2026-08-25 16:40:18 +08:00
return
}
2026-08-26 16:37:06 +08:00
current := make(map[string]sdk.Position, len(positions))
for _, position := range positions {
if position.StockCode != "" {
current[position.StockCode] = position
2026-08-25 16:40:18 +08:00
}
}
2026-08-26 16:37:06 +08:00
for _, code := range stateCodes() {
syncPosition(code, current[code])
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
for code, position := range current {
managePosition(client, ticks[code], position, marketOK, budget)
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
saveState()
}
func syncPosition(code string, position sdk.Position) {
item, err := QuantState.Get(code)
if err != nil || orderBusy(code, "BUY") || orderBusy(code, "SELL") {
return
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
if position.Volume <= 0 {
QuantState.Delete(code)
forget(code)
return
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
if item.BaseStatus == StatusIng {
item.BaseQty = max(0, position.Volume-item.AddedQty)
item.BaseCost = position.OpenPrice
item.BaseStatus = StatusOk
}
2026-08-26 16:37:06 +08:00
if item.AddedStatus == StatusIng {
syncAdded(item, position)
}
QuantState.Set(item)
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
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
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
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
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
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) {
2026-08-25 16:40:18 +08:00
return
}
2026-08-26 16:37:06 +08:00
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)
}
2026-08-25 16:40:18 +08:00
return
}
2026-08-26 16:37:06 +08:00
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)
}
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
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) {
2026-08-25 16:40:18 +08:00
return
}
2026-08-26 16:37:06 +08:00
volume := calcBuyVolume(price, config.Account.BuyValue)
amount := price * float64(volume)
if volume <= 0 || amount > *budget || orderBusy(item.Code, "BUY") {
2026-08-25 16:40:18 +08:00
return
}
2026-08-26 16:37:06 +08:00
orderID := NewOrderID(legAdded)
if !OrderBook.Place(client, sdk.OpBuy, item.Code, volume, orderID) {
return
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
item.AddedOrderId = orderID
item.AddedNum++
item.AddedQty = volume
item.AddedCost = price
item.AddedStatus = StatusIng
QuantState.Set(item)
*budget -= amount
saveState()
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
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") {
2026-08-25 16:40:18 +08:00
return
}
2026-08-26 16:37:06 +08:00
orderID := NewOrderID(leg)
if !OrderBook.Place(client, sdk.OpSell, item.Code, volume, orderID) {
2026-08-25 16:40:18 +08:00
return
}
2026-08-26 16:37:06 +08:00
if leg == legAdded {
item.AddedOrderId = orderID
item.AddedStatus = StatusIng
} else {
item.BaseOrderId = orderID
item.BaseStatus = StatusIng
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
QuantState.Set(item)
saveState()
logf("INFO", "[止盈] %s 卖出%d股盈利=%.2f%%", item.Code, volume, pnl)
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
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 {
2026-08-25 16:40:18 +08:00
return false
}
2026-08-25 22:35:44 +08:00
grid := int(math.Floor(pnl / config.Account.GridStepPct))
2026-08-26 16:37:06 +08:00
key := peakKey(code, leg)
2026-08-25 16:40:18 +08:00
peakMu.Lock()
defer peakMu.Unlock()
2026-08-26 16:37:06 +08:00
peak, tracked := peakGrids[key]
if !tracked || grid > peak {
2026-08-25 16:40:18 +08:00
peakGrids[key] = grid
return false
}
return grid < peak
}
2026-08-26 16:37:06 +08:00
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)
2026-08-25 16:40:18 +08:00
}
2026-08-26 16:37:06 +08:00
}
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()
2026-08-25 16:40:18 +08:00
}
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()
}
2026-08-26 16:37:06 +08:00
clearPeak(code, legBase)
clearPeak(code, legAdded)
2026-08-25 16:40:18 +08:00
}