Compare commits
2 Commits
550fdbf016
...
566a07fea8
| Author | SHA1 | Date | |
|---|---|---|---|
| 566a07fea8 | |||
| 9604126ee7 |
@@ -87,9 +87,10 @@ func RunOnce(ctx context.Context, client *sdk.Client, books *OrderBook, signals
|
|||||||
|
|
||||||
// 7 执行开仓:有开仓信号 && 大盘指数允许开仓
|
// 7 执行开仓:有开仓信号 && 大盘指数允许开仓
|
||||||
if len(allowOpen) > 0 && IsAllow {
|
if len(allowOpen) > 0 && IsAllow {
|
||||||
openSignal(client, books, ticks, allowOpen)
|
openSignal(ctx, client, books, ticks, allowOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8 持仓计算
|
// 8 持仓计算
|
||||||
managePositions(client, books, ticks, positions, IsAllow)
|
buyBudget := assets.Available
|
||||||
|
managePositions(ctx, client, books, ticks, positions, IsAllow, &buyBudget)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,43 @@
|
|||||||
package logic
|
package logic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"big-qmt/go-client/config"
|
"big-qmt/go-client/config"
|
||||||
"big-qmt/go-client/libs"
|
"big-qmt/go-client/libs"
|
||||||
"big-qmt/go-client/sdk"
|
"big-qmt/go-client/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
func openSignal(client *sdk.Client, books *OrderBook, ticks map[string]sdk.Tick, openSignals []libs.SignalItem) {
|
func openSignal(ctx context.Context, client *sdk.Client, books *OrderBook, ticks map[string]sdk.Tick, openSignals []libs.SignalItem) {
|
||||||
state := getState()
|
|
||||||
if state.LoadError != "" {
|
|
||||||
logf("ERROR", "[ZT][开仓] 状态文件异常,禁止新开仓: %s", state.LoadError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, item := range openSignals {
|
for _, item := range openSignals {
|
||||||
if state.Get(item.Code) != nil {
|
// 是否有锁
|
||||||
|
if _, err := QuantState.Get(item.Code); err == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 验证价格
|
||||||
price := ticks[item.Code].LastPrice
|
price := ticks[item.Code].LastPrice
|
||||||
if price <= 0 {
|
if price <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !dipTriggered(&openDip.mu, openDip.store, "开仓", item.Code, price) {
|
// 防止接飞刀
|
||||||
|
if !OpenWatch.Triggered("开仓", item.Code, price) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 计算开仓数量
|
||||||
volume := libs.CalcBuyVolume(price, config.Account.BuyValue)
|
volume := libs.CalcBuyVolume(price, config.Account.BuyValue)
|
||||||
if volume <= 0 {
|
if volume <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 开仓
|
||||||
orderID := newOrderTag("base")
|
orderID := newOrderTag("base")
|
||||||
if !books.place(ctx, client, sideBuy, item.Code, volume, orderID) {
|
if !books.place(ctx, client, sideBuy, item.Code, volume, orderID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
setPending(state.Ensure(item.Code), pendingBaseOpening, orderID)
|
// 保存数量
|
||||||
state.Save()
|
QuantState.Set(&StateItem{Code: item.Code, BaseOrderId: orderID, BaseQty: volume, BaseCost: price, BaseStatus: StatusIng})
|
||||||
|
if err := QuantState.Save(); err != nil {
|
||||||
|
logf("ERROR", "%v", err)
|
||||||
|
}
|
||||||
logf("INFO", "[ZT][开仓] %s 买入 %d 股", item.Code, volume)
|
logf("INFO", "[ZT][开仓] %s 买入 %d 股", item.Code, volume)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,11 +105,6 @@ func (o *OrderBook) readReceipts() {
|
|||||||
o.mu.Unlock()
|
o.mu.Unlock()
|
||||||
|
|
||||||
status := strings.ToLower(receipt.Status)
|
status := strings.ToLower(receipt.Status)
|
||||||
state := getState()
|
|
||||||
if item := state.Get(receipt.StockCode); item != nil && item.PendingOrderID == receipt.OrderID {
|
|
||||||
item.OrderStatus = status
|
|
||||||
state.Save()
|
|
||||||
}
|
|
||||||
if (status == "filled" || status == "cancelled" || status == "rejected") && (receipt.Side == sideBuy || receipt.Side == sideSell) {
|
if (status == "filled" || status == "cancelled" || status == "rejected") && (receipt.Side == sideBuy || receipt.Side == sideSell) {
|
||||||
o.unlockSide(receipt.StockCode, receipt.Side)
|
o.unlockSide(receipt.StockCode, receipt.Side)
|
||||||
o.invalidate()
|
o.invalidate()
|
||||||
@@ -184,7 +179,7 @@ func (o *OrderBook) CancelExpired(ctx context.Context, client *sdk.Client) bool
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
state := getState()
|
state := QuantState
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
timeout := time.Duration(config.Account.OrderTimeoutSec) * time.Second
|
timeout := time.Duration(config.Account.OrderTimeoutSec) * time.Second
|
||||||
seen := map[string]struct{}{}
|
seen := map[string]struct{}{}
|
||||||
@@ -243,7 +238,7 @@ func (o *OrderBook) CancelExpired(ctx context.Context, client *sdk.Client) bool
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OrderBook) claimed(state *ZTState, order parsedOrder) bool {
|
func (o *OrderBook) claimed(state *State, order parsedOrder) bool {
|
||||||
if order.RemarkOwned {
|
if order.RemarkOwned {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -258,18 +253,11 @@ func (o *OrderBook) claimed(state *ZTState, order parsedOrder) bool {
|
|||||||
if state == nil {
|
if state == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
item := state.Get(order.StockCode)
|
item, err := state.Get(order.StockCode)
|
||||||
if item == nil || item.Pending == "" {
|
if err != nil {
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch item.Pending {
|
|
||||||
case pendingBaseOpening, pendingAdd:
|
|
||||||
return order.Side == sideBuy
|
|
||||||
case pendingSellAdd, pendingSellBase:
|
|
||||||
return order.Side == sideSell
|
|
||||||
default:
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
return order.OrderID == item.BaseOrderId || order.OrderID == item.AddedOrderId || item.BaseStatus == StatusIng || item.AddedStatus == StatusIng
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OrderBook) unlockSide(code, side string) {
|
func (o *OrderBook) unlockSide(code, side string) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"big-qmt/go-client/config"
|
"big-qmt/go-client/config"
|
||||||
|
"big-qmt/go-client/libs"
|
||||||
"big-qmt/go-client/sdk"
|
"big-qmt/go-client/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,29 +15,37 @@ var peakGrids = map[string]int{}
|
|||||||
|
|
||||||
func peakKey(code, leg string) string { return code + "|" + leg }
|
func peakKey(code, leg string) string { return code + "|" + leg }
|
||||||
|
|
||||||
func managePositions(client *sdk.Client, books *OrderBook, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool) {
|
func calcBuyVolume(price, value float64) int {
|
||||||
if positions == nil {
|
return libs.CalcBuyVolume(price, value)
|
||||||
logf("ERROR", "[ZT][持仓] 持仓查询失败,本轮跳过")
|
}
|
||||||
|
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
state := getState()
|
buys, sells, ok := books.activeSets(ctx, client)
|
||||||
if state.LoadError != "" {
|
if !ok {
|
||||||
logf("ERROR", "[ZT][持仓] 状态文件异常,本轮停止交易: %s", state.LoadError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
before := map[string]struct{}{}
|
before := map[string]struct{}{}
|
||||||
for _, code := range state.Codes() {
|
for _, code := range stateCodes(QuantState) {
|
||||||
before[code] = struct{}{}
|
before[code] = struct{}{}
|
||||||
}
|
}
|
||||||
if ticks == nil {
|
if ticks == nil {
|
||||||
ticks = map[string]sdk.Tick{}
|
ticks = map[string]sdk.Tick{}
|
||||||
}
|
}
|
||||||
logf("INFO", "[ZT][持仓] 开始处理 %d 只", len(positions))
|
|
||||||
type row struct {
|
type row struct {
|
||||||
volume, usable int
|
volume, usable int
|
||||||
avg, price float64
|
avg, price float64
|
||||||
stock string
|
stock string
|
||||||
item *SymbolState
|
item *StateItem
|
||||||
}
|
}
|
||||||
rows := make([]row, 0, len(positions))
|
rows := make([]row, 0, len(positions))
|
||||||
seen := map[string]struct{}{}
|
seen := map[string]struct{}{}
|
||||||
@@ -46,20 +55,18 @@ func managePositions(client *sdk.Client, books *OrderBook, ticks map[string]sdk.
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[code] = struct{}{}
|
seen[code] = struct{}{}
|
||||||
item := syncItem(state, code, pos.Volume, pos.OpenPrice, buys, sells, books)
|
item := syncItem(QuantState, code, pos.Volume, pos.OpenPrice, buys, sells, books)
|
||||||
if pos.Volume <= 0 {
|
if pos.Volume > 0 {
|
||||||
continue
|
rows = append(rows, row{stock: code, volume: pos.Volume, usable: pos.CanUseVolume, avg: pos.OpenPrice, price: ticks[code].LastPrice, item: item})
|
||||||
}
|
}
|
||||||
price := ticks[code].LastPrice
|
|
||||||
rows = append(rows, row{stock: code, volume: pos.Volume, usable: pos.CanUseVolume, avg: pos.OpenPrice, price: price, item: item})
|
|
||||||
}
|
}
|
||||||
for _, code := range state.Codes() {
|
for _, code := range stateCodes(QuantState) {
|
||||||
if _, ok := seen[code]; !ok {
|
if _, ok := seen[code]; !ok {
|
||||||
syncItem(state, code, 0, 0, buys, sells, books)
|
syncItem(QuantState, code, 0, 0, buys, sells, books)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
after := map[string]struct{}{}
|
after := map[string]struct{}{}
|
||||||
for _, code := range state.Codes() {
|
for _, code := range stateCodes(QuantState) {
|
||||||
after[code] = struct{}{}
|
after[code] = struct{}{}
|
||||||
}
|
}
|
||||||
for code := range before {
|
for code := range before {
|
||||||
@@ -68,29 +75,20 @@ func managePositions(client *sdk.Client, books *OrderBook, ticks map[string]sdk.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
if r.item == nil || r.item.Pending != "" {
|
if r.item == nil || r.item.BaseStatus == StatusIng || r.item.AddedStatus == StatusIng || r.avg <= 0 || r.price <= 0 || r.volume%100 != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if r.avg <= 0 || r.price <= 0 || r.volume%100 != 0 {
|
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
|
continue
|
||||||
}
|
}
|
||||||
if r.volume != r.item.BaseQty+r.item.AddQty {
|
if r.item.AddedQty > 0 {
|
||||||
logf("INFO", "[ZT][持仓] %s 数量异常,底仓=%d 补仓=%d 现有=%d", r.stock, r.item.BaseQty, r.item.AddQty, r.volume)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
holdingAdd := r.item.AddQty > 0
|
|
||||||
legName := "底仓"
|
|
||||||
if holdingAdd {
|
|
||||||
legName = "补仓腿"
|
|
||||||
}
|
|
||||||
logf("INFO", "[ZT][持仓] %s 现价=%.2f 成本=%.2f 可用=%d %s", r.stock, r.price, r.avg, r.usable, legName)
|
|
||||||
if holdingAdd {
|
|
||||||
addPnL := -999.0
|
addPnL := -999.0
|
||||||
if r.item.AddCost > 0 {
|
if r.item.AddedCost > 0 {
|
||||||
addPnL = (r.price - r.item.AddCost) / r.item.AddCost * 100
|
addPnL = (r.price - r.item.AddedCost) / r.item.AddedCost * 100
|
||||||
}
|
}
|
||||||
if retreated(r.item, "add", addPnL) {
|
if retreated(r.item, "add", addPnL) {
|
||||||
sellLeg(ctx, client, books, r.item, r.usable, r.item.AddQty, "add", addPnL)
|
sellLeg(ctx, client, books, r.item, r.usable, r.item.AddedQty, "add", addPnL)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -100,138 +98,96 @@ func managePositions(client *sdk.Client, books *OrderBook, ticks map[string]sdk.
|
|||||||
}
|
}
|
||||||
if retreated(r.item, "base", basePnL) {
|
if retreated(r.item, "base", basePnL) {
|
||||||
sellLeg(ctx, client, books, r.item, r.usable, r.item.BaseQty, "base", basePnL)
|
sellLeg(ctx, client, books, r.item, r.usable, r.item.BaseQty, "base", basePnL)
|
||||||
} else if r.item.AddQty <= 0 && r.item.AddCost <= 0 && basePnL <= config.Account.LossTriggerPct {
|
} else if basePnL <= config.Account.LossTriggerPct {
|
||||||
addOnRebound(ctx, client, books, r.item, r.price, marketOK, buyBudget)
|
addOnRebound(ctx, client, books, r.item, r.price, marketOK, buyBudget)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 首次没有状态文件时,本轮已将启动前持仓全部接管为底仓。
|
if err := QuantState.Save(); err != nil {
|
||||||
state.completeBootstrap()
|
logf("ERROR", "%v", err)
|
||||||
state.Save()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncItem(state *ZTState, code string, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) *SymbolState {
|
func syncItem(state *State, code string, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) *StateItem {
|
||||||
item := state.Get(code)
|
item, err := state.Get(code)
|
||||||
if item == nil {
|
if err != nil {
|
||||||
if volume > 0 {
|
if volume > 0 {
|
||||||
logf("ERROR", "[ZT][持仓] %s 无本地状态,跳过", code)
|
logf("ERROR", "[ZT][持仓] %s 无本地状态,跳过", code)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch item.Pending {
|
if item.BaseStatus == StatusIng {
|
||||||
case pendingBaseOpening:
|
syncBase(state, item, volume, avgPrice, buys, sells, books)
|
||||||
syncOpen(state, item, volume, avgPrice, buys, books)
|
} else if item.AddedStatus == StatusIng {
|
||||||
case pendingAdd:
|
syncAdded(state, item, volume, avgPrice, buys, sells, books)
|
||||||
syncAdd(state, item, volume, avgPrice, buys, books)
|
} else if volume <= 0 {
|
||||||
case pendingSellAdd:
|
state.Delete(code)
|
||||||
syncSellAdd(state, item, volume, avgPrice, sells, books)
|
return nil
|
||||||
case pendingSellBase:
|
|
||||||
syncSellBase(state, item, volume, avgPrice, sells, books)
|
|
||||||
default:
|
|
||||||
if volume <= 0 {
|
|
||||||
state.Remove(code)
|
|
||||||
logf("INFO", "[ZT][持仓] %s 已无持仓,清除状态", code)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return state.Get(code)
|
item, _ = state.Get(code)
|
||||||
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncOpen(state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *OrderBook) {
|
func syncBase(state *State, item *StateItem, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) {
|
||||||
if volume > 0 {
|
if books.sideBusy(item.Code, sideBuy, buys) || books.sideBusy(item.Code, sideSell, sells) {
|
||||||
item.BaseQty, item.BaseCost = volume, avgPrice
|
|
||||||
}
|
|
||||||
if books.sideBusy(item.Code, sideBuy, buys) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if volume <= 0 {
|
if volume <= 0 {
|
||||||
state.Remove(item.Code)
|
state.Delete(item.Code)
|
||||||
logf("INFO", "[ZT][委托] %s 开仓委托已失效,允许重新开仓", item.Code)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
clearPending(item)
|
item.BaseQty = volume - item.AddedQty
|
||||||
logf("INFO", "[ZT][持仓] %s 开仓确认 数量=%d 成本=%.2f", item.Code, item.BaseQty, item.BaseCost)
|
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 syncAdd(state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *OrderBook) {
|
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 {
|
if volume > item.BaseQty {
|
||||||
item.AddQty = volume - item.BaseQty
|
item.AddedQty = volume - item.BaseQty
|
||||||
if item.AddQty > 0 {
|
item.AddedCost = math.Max(0, (avgPrice*float64(volume)-item.BaseCost*float64(item.BaseQty))/float64(item.AddedQty))
|
||||||
item.AddCost = math.Max(0, (avgPrice*float64(volume)-item.BaseCost*float64(item.BaseQty))/float64(item.AddQty))
|
item.AddedStatus = StatusOk
|
||||||
}
|
} else {
|
||||||
}
|
|
||||||
if books.sideBusy(item.Code, sideBuy, buys) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if volume <= 0 {
|
|
||||||
state.Remove(item.Code)
|
|
||||||
logf("INFO", "[ZT][委托] %s 补仓后无持仓,清除状态", item.Code)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if volume <= item.BaseQty {
|
|
||||||
item.AddQty = 0
|
|
||||||
item.AddCost = 0
|
|
||||||
logf("INFO", "[ZT][持仓] %s 补仓未成交,回退底仓", item.Code)
|
|
||||||
}
|
|
||||||
clearPending(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncSellAdd(state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *OrderBook) {
|
|
||||||
if volume <= 0 {
|
|
||||||
if !books.sideBusy(item.Code, sideSell, sells) {
|
|
||||||
state.Remove(item.Code)
|
|
||||||
logf("INFO", "[ZT][委托] %s 卖出后已无持仓,清除状态", item.Code)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if volume <= item.BaseQty {
|
|
||||||
item.BaseQty, item.BaseCost = volume, avgPrice
|
item.BaseQty, item.BaseCost = volume, avgPrice
|
||||||
item.AddQty = 0
|
item.AddedQty, item.AddedCost, item.AddedStatus = 0, 0, StatusNone
|
||||||
peakMu.Lock()
|
peakMu.Lock()
|
||||||
delete(peakGrids, peakKey(item.Code, "add"))
|
delete(peakGrids, peakKey(item.Code, "add"))
|
||||||
peakMu.Unlock()
|
peakMu.Unlock()
|
||||||
} else {
|
|
||||||
item.AddQty = volume - item.BaseQty
|
|
||||||
}
|
|
||||||
if !books.sideBusy(item.Code, sideSell, sells) {
|
|
||||||
clearPending(item)
|
|
||||||
}
|
}
|
||||||
|
state.Set(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncSellBase(state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *OrderBook) {
|
func addOnRebound(ctx context.Context, client *sdk.Client, books *OrderBook, item *StateItem, price float64, marketOK bool, buyBudget *float64) {
|
||||||
if volume <= 0 {
|
if !marketOK || PosbuyWatch == nil || !PosbuyWatch.Triggered("补仓", item.Code, price) {
|
||||||
if !books.sideBusy(item.Code, sideSell, sells) {
|
|
||||||
state.Remove(item.Code)
|
|
||||||
logf("INFO", "[ZT][委托] %s 卖出后已无持仓,清除状态", item.Code)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.BaseQty, item.BaseCost = volume, avgPrice
|
volume := libs.CalcBuyVolume(price, config.Account.BuyValue)
|
||||||
if !books.sideBusy(item.Code, sideSell, sells) {
|
|
||||||
clearPending(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addOnRebound(ctx context.Context, client *sdk.Client, books *OrderBook, item *SymbolState, price float64, marketOK bool, buyBudget *float64) {
|
|
||||||
if !marketOK || !dipTriggered(&posDip.mu, posDip.store, "补仓", item.Code, price) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
volume := calcBuyVolume(price, config.Account.BuyValue)
|
|
||||||
estimated := price * float64(volume)
|
estimated := price * float64(volume)
|
||||||
if buyBudget == nil || estimated > *buyBudget {
|
if volume <= 0 || buyBudget == nil || estimated > *buyBudget {
|
||||||
logf("INFO", "[ZT][补仓] %s 可用买入预算不足,需要=%.2f", item.Code, estimated)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
orderID := newOrderTag("add")
|
orderID := newOrderTag("add")
|
||||||
if books.place(ctx, client, sideBuy, item.Code, volume, orderID) {
|
if books.place(ctx, client, sideBuy, item.Code, volume, orderID) {
|
||||||
item.AddCost = price
|
item.AddedOrderId, item.AddedQty, item.AddedCost, item.AddedStatus = orderID, volume, price, StatusIng
|
||||||
setPending(item, pendingAdd, orderID)
|
item.AddedNum++
|
||||||
|
QuantState.Set(item)
|
||||||
*buyBudget -= estimated
|
*buyBudget -= estimated
|
||||||
getState().Save()
|
if err := QuantState.Save(); err != nil {
|
||||||
logf("INFO", "[ZT][补仓] %s 买入 %d 股", item.Code, volume)
|
logf("ERROR", "%v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func retreated(item *SymbolState, leg string, pnl float64) bool {
|
func retreated(item *StateItem, leg string, pnl float64) bool {
|
||||||
if pnl < config.Account.MinProfitPct {
|
if pnl < config.Account.MinProfitPct {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -242,16 +198,14 @@ func retreated(item *SymbolState, leg string, pnl float64) bool {
|
|||||||
peak, ok := peakGrids[key]
|
peak, ok := peakGrids[key]
|
||||||
if !ok || grid > peak {
|
if !ok || grid > peak {
|
||||||
peakGrids[key] = grid
|
peakGrids[key] = grid
|
||||||
logf("INFO", "[ZT][止盈] %s %s峰值网格=%d", item.Code, leg, grid)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return grid < peak
|
return grid < peak
|
||||||
}
|
}
|
||||||
|
|
||||||
func sellLeg(ctx context.Context, client *sdk.Client, books *OrderBook, item *SymbolState, usable, volume int, leg string, pnl float64) {
|
func sellLeg(ctx context.Context, client *sdk.Client, books *OrderBook, item *StateItem, usable, volume int, leg string, pnl float64) {
|
||||||
volume -= volume % 100
|
volume -= volume % 100
|
||||||
if volume <= 0 || usable < volume {
|
if volume <= 0 || usable < volume {
|
||||||
logf("INFO", "[ZT][止盈] %s 可用股数不足,需要=%d 可用=%d", item.Code, volume, usable)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
orderID := newOrderTag(leg)
|
orderID := newOrderTag(leg)
|
||||||
@@ -259,21 +213,28 @@ func sellLeg(ctx context.Context, client *sdk.Client, books *OrderBook, item *Sy
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if leg == "add" {
|
if leg == "add" {
|
||||||
setPending(item, pendingSellAdd, orderID)
|
item.AddedOrderId, item.AddedStatus = orderID, StatusIng
|
||||||
} else {
|
} else {
|
||||||
setPending(item, pendingSellBase, orderID)
|
item.BaseOrderId, item.BaseStatus = orderID, StatusIng
|
||||||
|
}
|
||||||
|
QuantState.Set(item)
|
||||||
|
if err := QuantState.Save(); err != nil {
|
||||||
|
logf("ERROR", "%v", err)
|
||||||
}
|
}
|
||||||
getState().Save()
|
|
||||||
logf("INFO", "[ZT][止盈] %s 卖出 %d 股,%s腿盈利=%.2f%%", item.Code, volume, leg, pnl)
|
logf("INFO", "[ZT][止盈] %s 卖出 %d 股,%s腿盈利=%.2f%%", item.Code, volume, leg, pnl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func forget(code string) {
|
func forget(code string) {
|
||||||
openDip.mu.Lock()
|
if OpenWatch != nil {
|
||||||
delete(openDip.store, code)
|
OpenWatch.mu.Lock()
|
||||||
openDip.mu.Unlock()
|
delete(OpenWatch.Data, code)
|
||||||
posDip.mu.Lock()
|
OpenWatch.mu.Unlock()
|
||||||
delete(posDip.store, code)
|
}
|
||||||
posDip.mu.Unlock()
|
if PosbuyWatch != nil {
|
||||||
|
PosbuyWatch.mu.Lock()
|
||||||
|
delete(PosbuyWatch.Data, code)
|
||||||
|
PosbuyWatch.mu.Unlock()
|
||||||
|
}
|
||||||
peakMu.Lock()
|
peakMu.Lock()
|
||||||
delete(peakGrids, peakKey(code, "base"))
|
delete(peakGrids, peakKey(code, "base"))
|
||||||
delete(peakGrids, peakKey(code, "add"))
|
delete(peakGrids, peakKey(code, "add"))
|
||||||
|
|||||||
@@ -4,225 +4,149 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"big-qmt/go-client/config"
|
"big-qmt/go-client/config"
|
||||||
"big-qmt/go-client/sdk"
|
"big-qmt/go-client/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
pendingNone = ""
|
|
||||||
pendingBaseOpening = "base_opening"
|
|
||||||
pendingAdd = "add"
|
|
||||||
pendingSellAdd = "sell_add"
|
|
||||||
pendingSellBase = "sell_base"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SymbolState struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
BaseQty int `json:"base_qty"`
|
|
||||||
BaseCost float64 `json:"base_cost"`
|
|
||||||
AddQty int `json:"add_qty"`
|
|
||||||
AddCost float64 `json:"add_cost"`
|
|
||||||
Pending string `json:"pending"`
|
|
||||||
PendingOrderID string `json:"pending_order_id,omitempty"`
|
|
||||||
OrderStatus string `json:"order_status,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func setPending(item *SymbolState, pending, orderID string) {
|
|
||||||
item.Pending = pending
|
|
||||||
item.PendingOrderID = orderID
|
|
||||||
item.OrderStatus = "submitted"
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearPending(item *SymbolState) {
|
|
||||||
item.Pending = pendingNone
|
|
||||||
item.PendingOrderID = ""
|
|
||||||
item.OrderStatus = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type filePayload struct {
|
|
||||||
Version int `json:"version"`
|
|
||||||
Data map[string]any `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ZTState struct {
|
|
||||||
path string
|
|
||||||
Items map[string]*SymbolState
|
|
||||||
LoadError string
|
|
||||||
fresh bool
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
statesMu sync.Mutex
|
StatusNone = ""
|
||||||
states = map[string]*ZTState{}
|
StatusIng = "ING" // 处理中
|
||||||
|
StatusOk = "OK" // 成功
|
||||||
|
QuantState *State
|
||||||
)
|
)
|
||||||
|
|
||||||
// BootstrapState 在状态文件首次不存在时,将启动前已有持仓登记为底仓。
|
type State struct {
|
||||||
func BootstrapState(positions []sdk.Position) {
|
AbsPath string
|
||||||
state := getState()
|
mu sync.Mutex
|
||||||
if !state.Fresh() {
|
Items map[string]*StateItem
|
||||||
return
|
Codes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateItem struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
BaseOrderId string `json:"base_order_id"`
|
||||||
|
BaseQty int `json:"base_qty"`
|
||||||
|
BaseCost float64 `json:"base_cost"`
|
||||||
|
BaseStatus string `json:"base_status,omitempty"`
|
||||||
|
AddedOrderId string `json:"added_order_id"`
|
||||||
|
AddedNum int `json:"add_num"`
|
||||||
|
AddedQty int `json:"add_qty"`
|
||||||
|
AddedCost float64 `json:"add_cost"`
|
||||||
|
AddedStatus string `json:"added_status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitState(sn string) error {
|
||||||
|
absPath := path.Join(config.Global.QMTDataDir, fmt.Sprintf("%s_%s_state.json", sn, config.Account.AccountID))
|
||||||
|
items, err := loadStateFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var codes []string
|
||||||
|
for code, _ := range items {
|
||||||
|
codes = append(codes, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
QuantState = &State{
|
||||||
|
AbsPath: absPath,
|
||||||
|
Items: items,
|
||||||
|
Codes: codes,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadStateFile(fp string) (map[string]*StateItem, error) {
|
||||||
|
raw, err := os.ReadFile(fp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("[状态] 读取失败: %v", err)
|
||||||
|
}
|
||||||
|
var items map[string]*StateItem
|
||||||
|
if err := json.Unmarshal(raw, &items); err != nil {
|
||||||
|
return nil, fmt.Errorf("[状态] 解析失败:%s", err)
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncPositions(positions []sdk.Position) error {
|
||||||
for _, pos := range positions {
|
for _, pos := range positions {
|
||||||
code := pos.StockCode
|
code := pos.StockCode
|
||||||
if code == "" || pos.Volume <= 0 || pos.OpenPrice <= 0 {
|
if code == "" || pos.Volume <= 0 || pos.OpenPrice <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item := state.Ensure(code)
|
if !slices.Contains(QuantState.Codes, code) {
|
||||||
item.BaseQty, item.BaseCost = pos.Volume, pos.OpenPrice
|
item := &StateItem{
|
||||||
logf("WARNING", "[ZT][状态] %s 首次接管为底仓 数量=%d 成本=%.2f", code, pos.Volume, pos.OpenPrice)
|
Code: code,
|
||||||
}
|
BaseQty: pos.Volume,
|
||||||
state.completeBootstrap()
|
BaseCost: pos.OpenPrice,
|
||||||
state.Save()
|
BaseStatus: StatusOk,
|
||||||
}
|
}
|
||||||
|
QuantState.Append(item)
|
||||||
func getState() *ZTState {
|
logf("WARNING", "[状态] %s 首次接管为底仓 数量=%d 成本=%.2f", code, pos.Volume, pos.OpenPrice)
|
||||||
statesMu.Lock()
|
|
||||||
defer statesMu.Unlock()
|
|
||||||
accountID := config.Account.AccountID
|
|
||||||
if s, ok := states[accountID]; ok {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
s := loadZTState(config.Global.QMTDataDir, accountID)
|
|
||||||
states[accountID] = s
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadZTState(dataDir, accountID string) *ZTState {
|
|
||||||
st := &ZTState{
|
|
||||||
path: filepath.Join(dataDir, fmt.Sprintf("zt_%s_state.json", accountID)),
|
|
||||||
Items: map[string]*SymbolState{},
|
|
||||||
}
|
|
||||||
raw, err := os.ReadFile(st.path)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
st.fresh = true
|
|
||||||
return st
|
|
||||||
}
|
|
||||||
st.LoadError = err.Error()
|
|
||||||
logf("ERROR", "[ZT][状态] 读取状态文件失败: %v", err)
|
|
||||||
return st
|
|
||||||
}
|
|
||||||
var payload filePayload
|
|
||||||
if err := json.Unmarshal(raw, &payload); err != nil || payload.Version != 1 {
|
|
||||||
st.LoadError = "状态文件版本无效"
|
|
||||||
logf("ERROR", "[ZT][状态] %s", st.LoadError)
|
|
||||||
return st
|
|
||||||
}
|
|
||||||
data := payload.Data
|
|
||||||
if data == nil {
|
|
||||||
st.LoadError = "状态文件内容无效"
|
|
||||||
logf("ERROR", "[ZT][状态] %s", st.LoadError)
|
|
||||||
return st
|
|
||||||
}
|
|
||||||
symbolsAny, _ := data["symbols"]
|
|
||||||
symbols, _ := symbolsAny.(map[string]any)
|
|
||||||
if symbols == nil {
|
|
||||||
if _, ok := data["code"]; ok {
|
|
||||||
symbols = map[string]any{}
|
|
||||||
} else {
|
|
||||||
symbols = data
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for code, value := range symbols {
|
|
||||||
m, ok := value.(map[string]any)
|
return QuantState.Save()
|
||||||
if !ok {
|
}
|
||||||
continue
|
|
||||||
}
|
func (s *State) Append(i *StateItem) {
|
||||||
item := &SymbolState{Code: code}
|
s.mu.Lock()
|
||||||
b, _ := json.Marshal(m)
|
defer s.mu.Unlock()
|
||||||
_ = json.Unmarshal(b, item)
|
|
||||||
item.Code = code
|
s.Items[i.Code] = i
|
||||||
st.Items[code] = item
|
s.Codes = append(s.Codes, i.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) Get(code string) (*StateItem, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if i, ok := s.Items[code]; ok {
|
||||||
|
return i, nil
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("%s not found.", code)
|
||||||
}
|
}
|
||||||
return st
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ZTState) Fresh() bool {
|
func (s *State) Set(i *StateItem) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
return s.fresh
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ZTState) completeBootstrap() {
|
if _, ok := s.Items[i.Code]; !ok {
|
||||||
s.mu.Lock()
|
s.Codes = append(s.Codes, i.Code)
|
||||||
defer s.mu.Unlock()
|
|
||||||
s.fresh = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ZTState) Get(code string) *SymbolState {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return s.Items[code]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ZTState) Ensure(code string) *SymbolState {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
if item, ok := s.Items[code]; ok {
|
|
||||||
return item
|
|
||||||
}
|
}
|
||||||
item := &SymbolState{Code: code}
|
s.Items[i.Code] = i
|
||||||
s.Items[code] = item
|
|
||||||
return item
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ZTState) Remove(code string) {
|
func (s *State) Delete(code string) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
delete(s.Items, code)
|
delete(s.Items, code)
|
||||||
|
if index := slices.Index(s.Codes, code); index >= 0 {
|
||||||
|
s.Codes = slices.Delete(s.Codes, index, index+1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ZTState) Codes() []string {
|
func (s *State) Save() error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
out := make([]string, 0, len(s.Items))
|
|
||||||
for code := range s.Items {
|
|
||||||
out = append(out, code)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ZTState) Save() {
|
// 写入AbsPath文件
|
||||||
s.mu.Lock()
|
f, err := os.OpenFile(s.AbsPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||||
defer s.mu.Unlock()
|
|
||||||
if s.LoadError != "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := s.saveUnlocked(); err != nil {
|
|
||||||
s.LoadError = err.Error()
|
|
||||||
logf("ERROR", "[ZT][状态] 保存失败: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ZTState) saveUnlocked() error {
|
|
||||||
symbols := map[string]any{}
|
|
||||||
for code, item := range s.Items {
|
|
||||||
symbols[code] = item
|
|
||||||
}
|
|
||||||
payload := filePayload{Version: 1, Data: map[string]any{"symbols": symbols}}
|
|
||||||
raw, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("[状态] 打开文件失败: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
|
defer f.Close()
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmp := s.path + ".tmp"
|
|
||||||
if err := os.WriteFile(tmp, raw, 0o644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return replaceFile(tmp, s.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func replaceFile(tmp, dest string) error {
|
encoder := json.NewEncoder(f)
|
||||||
if err := os.Rename(tmp, dest); err == nil {
|
encoder.SetIndent("", " ")
|
||||||
return nil
|
if err := encoder.Encode(s.Items); err != nil {
|
||||||
|
return fmt.Errorf("[状态] 写入失败: %v", err)
|
||||||
}
|
}
|
||||||
_ = os.Remove(dest)
|
return nil
|
||||||
return os.Rename(tmp, dest)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,54 @@
|
|||||||
package logic
|
package logic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"big-qmt/go-client/config"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
WatchExpireTime = 5 * time.Minute
|
||||||
|
WatchReThreshold = 0.61
|
||||||
|
|
||||||
|
OpenWatch *WatchMu
|
||||||
|
PosbuyWatch *WatchMu
|
||||||
|
)
|
||||||
|
|
||||||
type dipWatch struct {
|
type dipWatch struct {
|
||||||
LastClose float64
|
LastClose float64
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
var openDip = struct {
|
type WatchMu struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
store map[string]dipWatch
|
Data map[string]dipWatch
|
||||||
}{store: map[string]dipWatch{}}
|
}
|
||||||
|
|
||||||
var posDip = struct {
|
func InitWatch() {
|
||||||
mu sync.Mutex
|
OpenWatch = &WatchMu{
|
||||||
store map[string]dipWatch
|
Data: make(map[string]dipWatch),
|
||||||
}{store: map[string]dipWatch{}}
|
}
|
||||||
|
PosbuyWatch = &WatchMu{
|
||||||
|
Data: make(map[string]dipWatch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func dipTriggered(mu *sync.Mutex, store map[string]dipWatch, tag, code string, price float64) bool {
|
func (w *WatchMu) Triggered(tag, code string, price float64) bool {
|
||||||
if price <= 0 {
|
if price <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
mu.Lock()
|
w.mu.Lock()
|
||||||
defer mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
watch, ok := store[code]
|
watch, ok := w.Data[code]
|
||||||
if !ok || now.After(watch.ExpiresAt) || now.Equal(watch.ExpiresAt) {
|
if !ok || now.After(watch.ExpiresAt) || now.Equal(watch.ExpiresAt) {
|
||||||
store[code] = dipWatch{LastClose: price, ExpiresAt: now.Add(time.Duration(config.Account.WatchTimeoutSec) * time.Second)}
|
w.Data[code] = dipWatch{LastClose: price, ExpiresAt: now.Add(WatchExpireTime)}
|
||||||
logf("INFO", "[%s-观察] %s 现价=%.2f", tag, code, price)
|
logf("INFO", "[%s-观察] %s 现价=%.2f", tag, code, price)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if price < watch.LastClose {
|
if price < watch.LastClose {
|
||||||
watch.LastClose = price
|
watch.LastClose = price
|
||||||
watch.ExpiresAt = now.Add(time.Duration(config.Account.WatchTimeoutSec) * time.Second)
|
watch.ExpiresAt = now.Add(WatchExpireTime)
|
||||||
store[code] = watch
|
w.Data[code] = watch
|
||||||
logf("INFO", "[%s-下跌] %s 刷新低点=%.2f", tag, code, price)
|
logf("INFO", "[%s-下跌] %s 刷新低点=%.2f", tag, code, price)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -45,11 +56,11 @@ func dipTriggered(mu *sync.Mutex, store map[string]dipWatch, tag, code string, p
|
|||||||
if rebound <= 0 {
|
if rebound <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if rebound < config.Account.ReboundThreshold {
|
if rebound < WatchReThreshold {
|
||||||
logf("INFO", "[%s-等待] %s 反弹=%.2f%% 阈值=%.2f%%", tag, code, rebound, config.Account.ReboundThreshold)
|
logf("INFO", "[%s-等待] %s 反弹=%.2f%% 阈值=%.2f%%", tag, code, rebound, WatchReThreshold)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
delete(store, code)
|
delete(w.Data, code)
|
||||||
logf("INFO", "[%s-触发] %s 反弹=%.2f%% 低点=%.2f", tag, code, rebound, watch.LastClose)
|
logf("INFO", "[%s-触发] %s 反弹=%.2f%% 低点=%.2f", tag, code, rebound, watch.LastClose)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,10 @@ import (
|
|||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
StrategyName = "zt"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
@@ -34,8 +38,16 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
logic.InitWatch()
|
||||||
|
if err := logic.InitState(StrategyName); err != nil {
|
||||||
|
log.Panicln("ERROR", err.Error())
|
||||||
|
}
|
||||||
|
if err := logic.SyncPositions(positions); err != nil {
|
||||||
|
log.Panicln("ERROR", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
// 第三步:连接成功后接管首次持仓并打印账户概览。
|
// 第三步:连接成功后接管首次持仓并打印账户概览。
|
||||||
logic.BootstrapState(positions)
|
|
||||||
logic.Overview(assets, positions)
|
logic.Overview(assets, positions)
|
||||||
signals, err := libs.FetchSignal(libs.Dcm_Signal, config.Account.HostKey)
|
signals, err := libs.FetchSignal(libs.Dcm_Signal, config.Account.HostKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -25,16 +25,14 @@ type GlobalConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AccountConfig struct {
|
type AccountConfig struct {
|
||||||
AccountID string `yaml:"account_id"`
|
AccountID string `yaml:"account_id"`
|
||||||
HostKey string `yaml:"host_key"`
|
HostKey string `yaml:"host_key"`
|
||||||
OrderTimeoutSec int `yaml:"order_timeout_seconds"`
|
OrderTimeoutSec int `yaml:"order_timeout_seconds"`
|
||||||
BuyValue float64 `yaml:"buy_value"`
|
BuyValue float64 `yaml:"buy_value"`
|
||||||
MinCashRatio float64 `yaml:"min_cash_ratio"`
|
MinCashRatio float64 `yaml:"min_cash_ratio"`
|
||||||
LossTriggerPct float64 `yaml:"loss_trigger_pct"`
|
LossTriggerPct float64 `yaml:"loss_trigger_pct"`
|
||||||
GridStepPct float64 `yaml:"grid_step_pct"`
|
GridStepPct float64 `yaml:"grid_step_pct"`
|
||||||
MinProfitPct float64 `yaml:"min_profit_pct"`
|
MinProfitPct float64 `yaml:"min_profit_pct"`
|
||||||
WatchTimeoutSec int `yaml:"watch_timeout_seconds"`
|
|
||||||
ReboundThreshold float64 `yaml:"rebound_threshold"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 根据 global.yaml 中的 hosts 映射加载当前计算机的账户配置。
|
// Load 根据 global.yaml 中的 hosts 映射加载当前计算机的账户配置。
|
||||||
|
|||||||
Reference in New Issue
Block a user