156 lines
3.8 KiB
Go
156 lines
3.8 KiB
Go
|
|
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()
|
|||
|
|
}
|