dev 5
This commit is contained in:
@@ -10,218 +10,215 @@ import (
|
||||
"big-qmt/go-client/sdk"
|
||||
)
|
||||
|
||||
var peakMu sync.Mutex
|
||||
var peakGrids = map[string]int{}
|
||||
const (
|
||||
legBase = "base"
|
||||
legAdded = "add"
|
||||
)
|
||||
|
||||
func peakKey(code, leg string) string { return code + "|" + leg }
|
||||
var (
|
||||
peakMu sync.Mutex
|
||||
peakGrids = make(map[string]int)
|
||||
)
|
||||
|
||||
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][持仓] 持仓或状态不可用,本轮跳过")
|
||||
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
|
||||
}
|
||||
buys, sells, ok := books.activeSets(ctx, client)
|
||||
if !ok {
|
||||
if err := OrderBook.Refresh(client); err != nil {
|
||||
logf("ERROR", "[持仓] 刷新委托失败: %v", err)
|
||||
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})
|
||||
|
||||
current := make(map[string]sdk.Position, len(positions))
|
||||
for _, position := range positions {
|
||||
if position.StockCode != "" {
|
||||
current[position.StockCode] = position
|
||||
}
|
||||
}
|
||||
for _, code := range stateCodes(QuantState) {
|
||||
if _, ok := seen[code]; !ok {
|
||||
syncItem(QuantState, code, 0, 0, buys, sells, books)
|
||||
}
|
||||
|
||||
for _, code := range stateCodes() {
|
||||
syncPosition(code, current[code])
|
||||
}
|
||||
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)
|
||||
for code, position := range current {
|
||||
managePosition(client, ticks[code], position, marketOK, budget)
|
||||
}
|
||||
|
||||
saveState()
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
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.BaseQty = max(0, position.Volume-item.AddedQty)
|
||||
item.BaseCost = position.OpenPrice
|
||||
item.BaseStatus = StatusOk
|
||||
}
|
||||
item, _ = state.Get(code)
|
||||
return item
|
||||
if item.AddedStatus == StatusIng {
|
||||
syncAdded(item, position)
|
||||
}
|
||||
QuantState.Set(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) {
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
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 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) {
|
||||
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 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)
|
||||
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 retreated(item *StateItem, leg string, pnl float64) bool {
|
||||
if pnl < config.Account.MinProfitPct {
|
||||
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(item.Code, leg)
|
||||
key := peakKey(code, leg)
|
||||
peakMu.Lock()
|
||||
defer peakMu.Unlock()
|
||||
peak, ok := peakGrids[key]
|
||||
if !ok || grid > peak {
|
||||
peak, tracked := peakGrids[key]
|
||||
if !tracked || 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)
|
||||
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)
|
||||
}
|
||||
logf("INFO", "[ZT][止盈] %s 卖出 %d 股,%s腿盈利=%.2f%%", item.Code, volume, leg, pnl)
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -235,8 +232,6 @@ func forget(code string) {
|
||||
delete(PosbuyWatch.Data, code)
|
||||
PosbuyWatch.mu.Unlock()
|
||||
}
|
||||
peakMu.Lock()
|
||||
delete(peakGrids, peakKey(code, "base"))
|
||||
delete(peakGrids, peakKey(code, "add"))
|
||||
peakMu.Unlock()
|
||||
clearPeak(code, legBase)
|
||||
clearPeak(code, legAdded)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user