Files
big-qmt/go-client/apps/trend/logic/positions.go
2026-08-26 23:27:42 +08:00

156 lines
3.8 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 (
"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(client *sdk.Client, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool) {
for _, pos := range positions {
item, err := QuantState.Get(code)
if err != nil || item.BaseStatus == StatusIng || item.AddedStatus == StatusIng || position.Volume != item.BaseQty+item.AddedQty {
continue
}
price := ticks[code].LastPrice
if price <= 0 {
continue
}
if item.AddedQty > 0 {
pnl := (price - item.AddedCost) / item.AddedCost * 100
if shouldSell(code, legAdded, pnl) {
sell(client, item, position.CanUseVolume, item.AddedQty, legAdded, pnl)
}
continue
}
pnl := (price - item.BaseCost) / item.BaseCost * 100
if shouldSell(code, legBase, pnl) {
sell(client, item, position.CanUseVolume, item.BaseQty, legBase, pnl)
} else if pnl <= config.Account.LossTriggerPct {
buyAdded(client, item, price, marketOK, budget)
}
}
}
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
peakMu.Lock()
delete(peakGrids, item.Code+"|"+legAdded)
peakMu.Unlock()
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 buyAdded(client *sdk.Client, item *StateItem, price float64, marketOK bool, budget *float64) {
if !marketOK || !PosbuyWatch.Triggered("补仓", item.Code, price) {
return
}
volume := libs.CalcBuyVolume(price, config.Account.BuyValue)
amount := price * float64(volume)
if 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
}
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)
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 {
return false
}
grid := int(math.Floor(pnl / config.Account.GridStepPct))
key := code + "|" + leg
peakMu.Lock()
defer peakMu.Unlock()
peak, tracked := peakGrids[key]
if !tracked || grid > peak {
peakGrids[key] = grid
return false
}
return grid < peak
}
func forget(code string) {
OpenWatch.mu.Lock()
delete(OpenWatch.Data, code)
OpenWatch.mu.Unlock()
PosbuyWatch.mu.Lock()
delete(PosbuyWatch.Data, code)
PosbuyWatch.mu.Unlock()
peakMu.Lock()
delete(peakGrids, code+"|"+legBase)
delete(peakGrids, code+"|"+legAdded)
peakMu.Unlock()
}