243 lines
6.9 KiB
Go
243 lines
6.9 KiB
Go
package logic
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"sync"
|
|
|
|
"big-qmt/go-client/config"
|
|
"big-qmt/go-client/libs"
|
|
"big-qmt/go-client/sdk"
|
|
)
|
|
|
|
var peakMu sync.Mutex
|
|
var peakGrids = map[string]int{}
|
|
|
|
func peakKey(code, leg string) string { return code + "|" + leg }
|
|
|
|
func calcBuyVolume(price, value float64) int {
|
|
return libs.CalcBuyVolume(price, value)
|
|
}
|
|
|
|
func stateCodes(state *State) []string {
|
|
state.mu.Lock()
|
|
defer state.mu.Unlock()
|
|
return append([]string(nil), state.Codes...)
|
|
}
|
|
|
|
func managePositions(ctx context.Context, client *sdk.Client, books *OrderBook, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool, buyBudget *float64) {
|
|
if positions == nil || QuantState == nil {
|
|
logf("ERROR", "[ZT][持仓] 持仓或状态不可用,本轮跳过")
|
|
return
|
|
}
|
|
buys, sells, ok := books.activeSets(ctx, client)
|
|
if !ok {
|
|
return
|
|
}
|
|
before := map[string]struct{}{}
|
|
for _, code := range stateCodes(QuantState) {
|
|
before[code] = struct{}{}
|
|
}
|
|
if ticks == nil {
|
|
ticks = map[string]sdk.Tick{}
|
|
}
|
|
type row struct {
|
|
volume, usable int
|
|
avg, price float64
|
|
stock string
|
|
item *StateItem
|
|
}
|
|
rows := make([]row, 0, len(positions))
|
|
seen := map[string]struct{}{}
|
|
for _, pos := range positions {
|
|
code := pos.StockCode
|
|
if code == "" {
|
|
continue
|
|
}
|
|
seen[code] = struct{}{}
|
|
item := syncItem(QuantState, code, pos.Volume, pos.OpenPrice, buys, sells, books)
|
|
if pos.Volume > 0 {
|
|
rows = append(rows, row{stock: code, volume: pos.Volume, usable: pos.CanUseVolume, avg: pos.OpenPrice, price: ticks[code].LastPrice, item: item})
|
|
}
|
|
}
|
|
for _, code := range stateCodes(QuantState) {
|
|
if _, ok := seen[code]; !ok {
|
|
syncItem(QuantState, code, 0, 0, buys, sells, books)
|
|
}
|
|
}
|
|
after := map[string]struct{}{}
|
|
for _, code := range stateCodes(QuantState) {
|
|
after[code] = struct{}{}
|
|
}
|
|
for code := range before {
|
|
if _, ok := after[code]; !ok {
|
|
forget(code)
|
|
}
|
|
}
|
|
for _, r := range rows {
|
|
if r.item == nil || r.item.BaseStatus == StatusIng || r.item.AddedStatus == StatusIng || r.avg <= 0 || r.price <= 0 || r.volume%100 != 0 {
|
|
continue
|
|
}
|
|
if r.volume != r.item.BaseQty+r.item.AddedQty {
|
|
logf("INFO", "[ZT][持仓] %s 数量异常,底仓=%d 补仓=%d 现有=%d", r.stock, r.item.BaseQty, r.item.AddedQty, r.volume)
|
|
continue
|
|
}
|
|
if r.item.AddedQty > 0 {
|
|
addPnL := -999.0
|
|
if r.item.AddedCost > 0 {
|
|
addPnL = (r.price - r.item.AddedCost) / r.item.AddedCost * 100
|
|
}
|
|
if retreated(r.item, "add", addPnL) {
|
|
sellLeg(ctx, client, books, r.item, r.usable, r.item.AddedQty, "add", addPnL)
|
|
}
|
|
continue
|
|
}
|
|
basePnL := -999.0
|
|
if r.item.BaseCost > 0 {
|
|
basePnL = (r.price - r.item.BaseCost) / r.item.BaseCost * 100
|
|
}
|
|
if retreated(r.item, "base", basePnL) {
|
|
sellLeg(ctx, client, books, r.item, r.usable, r.item.BaseQty, "base", basePnL)
|
|
} else if basePnL <= config.Account.LossTriggerPct {
|
|
addOnRebound(ctx, client, books, r.item, r.price, marketOK, buyBudget)
|
|
}
|
|
}
|
|
if err := QuantState.Save(); err != nil {
|
|
logf("ERROR", "%v", err)
|
|
}
|
|
}
|
|
|
|
func syncItem(state *State, code string, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) *StateItem {
|
|
item, err := state.Get(code)
|
|
if err != nil {
|
|
if volume > 0 {
|
|
logf("ERROR", "[ZT][持仓] %s 无本地状态,跳过", code)
|
|
}
|
|
return nil
|
|
}
|
|
if item.BaseStatus == StatusIng {
|
|
syncBase(state, item, volume, avgPrice, buys, sells, books)
|
|
} else if item.AddedStatus == StatusIng {
|
|
syncAdded(state, item, volume, avgPrice, buys, sells, books)
|
|
} else if volume <= 0 {
|
|
state.Delete(code)
|
|
return nil
|
|
}
|
|
item, _ = state.Get(code)
|
|
return item
|
|
}
|
|
|
|
func syncBase(state *State, item *StateItem, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) {
|
|
if books.sideBusy(item.Code, sideBuy, buys) || books.sideBusy(item.Code, sideSell, sells) {
|
|
return
|
|
}
|
|
if volume <= 0 {
|
|
state.Delete(item.Code)
|
|
return
|
|
}
|
|
item.BaseQty = volume - item.AddedQty
|
|
if item.BaseQty < 0 {
|
|
item.BaseQty, item.AddedQty, item.AddedCost, item.AddedStatus = volume, 0, 0, StatusNone
|
|
}
|
|
item.BaseCost = avgPrice
|
|
item.BaseStatus = StatusOk
|
|
state.Set(item)
|
|
}
|
|
|
|
func syncAdded(state *State, item *StateItem, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) {
|
|
if books.sideBusy(item.Code, sideBuy, buys) || books.sideBusy(item.Code, sideSell, sells) {
|
|
return
|
|
}
|
|
if volume <= 0 {
|
|
state.Delete(item.Code)
|
|
return
|
|
}
|
|
if volume > item.BaseQty {
|
|
item.AddedQty = volume - item.BaseQty
|
|
item.AddedCost = math.Max(0, (avgPrice*float64(volume)-item.BaseCost*float64(item.BaseQty))/float64(item.AddedQty))
|
|
item.AddedStatus = StatusOk
|
|
} else {
|
|
item.BaseQty, item.BaseCost = volume, avgPrice
|
|
item.AddedQty, item.AddedCost, item.AddedStatus = 0, 0, StatusNone
|
|
peakMu.Lock()
|
|
delete(peakGrids, peakKey(item.Code, "add"))
|
|
peakMu.Unlock()
|
|
}
|
|
state.Set(item)
|
|
}
|
|
|
|
func addOnRebound(ctx context.Context, client *sdk.Client, books *OrderBook, item *StateItem, price float64, marketOK bool, buyBudget *float64) {
|
|
if !marketOK || PosbuyWatch == nil || !PosbuyWatch.Triggered("补仓", item.Code, price) {
|
|
return
|
|
}
|
|
volume := libs.CalcBuyVolume(price, config.Account.BuyValue)
|
|
estimated := price * float64(volume)
|
|
if volume <= 0 || buyBudget == nil || estimated > *buyBudget {
|
|
return
|
|
}
|
|
orderID := newOrderTag("add")
|
|
if books.place(ctx, client, sideBuy, item.Code, volume, orderID) {
|
|
item.AddedOrderId, item.AddedQty, item.AddedCost, item.AddedStatus = orderID, volume, price, StatusIng
|
|
item.AddedNum++
|
|
QuantState.Set(item)
|
|
*buyBudget -= estimated
|
|
if err := QuantState.Save(); err != nil {
|
|
logf("ERROR", "%v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func retreated(item *StateItem, leg string, pnl float64) bool {
|
|
if pnl < config.Account.MinProfitPct {
|
|
return false
|
|
}
|
|
grid := int(math.Floor(pnl / config.Account.GridStepPct))
|
|
key := peakKey(item.Code, leg)
|
|
peakMu.Lock()
|
|
defer peakMu.Unlock()
|
|
peak, ok := peakGrids[key]
|
|
if !ok || grid > peak {
|
|
peakGrids[key] = grid
|
|
return false
|
|
}
|
|
return grid < peak
|
|
}
|
|
|
|
func sellLeg(ctx context.Context, client *sdk.Client, books *OrderBook, item *StateItem, usable, volume int, leg string, pnl float64) {
|
|
volume -= volume % 100
|
|
if volume <= 0 || usable < volume {
|
|
return
|
|
}
|
|
orderID := newOrderTag(leg)
|
|
if !books.place(ctx, client, sideSell, item.Code, volume, orderID) {
|
|
return
|
|
}
|
|
if leg == "add" {
|
|
item.AddedOrderId, item.AddedStatus = orderID, StatusIng
|
|
} else {
|
|
item.BaseOrderId, item.BaseStatus = orderID, StatusIng
|
|
}
|
|
QuantState.Set(item)
|
|
if err := QuantState.Save(); err != nil {
|
|
logf("ERROR", "%v", err)
|
|
}
|
|
logf("INFO", "[ZT][止盈] %s 卖出 %d 股,%s腿盈利=%.2f%%", item.Code, volume, leg, pnl)
|
|
}
|
|
|
|
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()
|
|
}
|
|
peakMu.Lock()
|
|
delete(peakGrids, peakKey(code, "base"))
|
|
delete(peakGrids, peakKey(code, "add"))
|
|
peakMu.Unlock()
|
|
}
|