Files
big-qmt/go-client/apps/zt/logic/positions.go
2026-08-26 16:37:06 +08:00

238 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}