115 lines
2.7 KiB
Go
115 lines
2.7 KiB
Go
package logic
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
"big-qmt/go-client/sdk"
|
|
)
|
|
|
|
type dipWatch struct {
|
|
LastClose float64
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
var openDip = struct {
|
|
mu sync.Mutex
|
|
store map[string]dipWatch
|
|
}{store: map[string]dipWatch{}}
|
|
|
|
func openSignal(ctx context.Context, client *sdk.Client, books *OrderBook, cfg Config, assets *sdk.Assets, ticks map[string]sdk.Tick, openSignals map[string]map[string]any, marketOK bool) {
|
|
if !marketOK {
|
|
return
|
|
}
|
|
if assets == nil {
|
|
return
|
|
}
|
|
if assets.Available < assets.Total*cfg.MinCashRatio {
|
|
return
|
|
}
|
|
state := getState(cfg)
|
|
if state.LoadError != "" {
|
|
logf("ERROR", "[ZT][开仓] 状态文件异常,禁止新开仓: %s", state.LoadError)
|
|
return
|
|
}
|
|
for signalCode, signal := range openSignals {
|
|
code := normalizeCode(signalCode, "")
|
|
if code == "" {
|
|
if c, ok := signal["code"].(string); ok {
|
|
code = normalizeCode(c, "")
|
|
}
|
|
}
|
|
if code == "" {
|
|
logf("ERROR", "[ZT][开仓] 无效股票代码=%s", signalCode)
|
|
continue
|
|
}
|
|
if state.Get(code) != nil {
|
|
continue
|
|
}
|
|
price := ticks[code].LastPrice
|
|
if price <= 0 {
|
|
continue
|
|
}
|
|
if !dipTriggered(&openDip.mu, openDip.store, cfg, "开仓", code, price) {
|
|
continue
|
|
}
|
|
volume := calcOpenVolume(price, cfg.OpenMoney)
|
|
if volume <= 0 {
|
|
continue
|
|
}
|
|
if !books.place(ctx, client, cfg, "buy", code, volume, newOrderTag("base")) {
|
|
continue
|
|
}
|
|
state.Ensure(code).Pending = "base_opening"
|
|
state.Save()
|
|
logf("INFO", "[ZT][开仓] %s 买入 %d 股", code, volume)
|
|
}
|
|
state.Save()
|
|
}
|
|
|
|
func calcOpenVolume(price, openMoney float64) int {
|
|
if price <= 0 || openMoney <= 0 {
|
|
return 0
|
|
}
|
|
hands := int(math.Floor(openMoney / (price * 100)))
|
|
if hands == 0 {
|
|
hands = 1
|
|
}
|
|
return hands * 100
|
|
}
|
|
|
|
func dipTriggered(mu *sync.Mutex, store map[string]dipWatch, cfg Config, tag, code string, price float64) bool {
|
|
if price <= 0 {
|
|
return false
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
now := time.Now()
|
|
watch, ok := store[code]
|
|
if !ok || now.After(watch.ExpiresAt) || now.Equal(watch.ExpiresAt) {
|
|
store[code] = dipWatch{LastClose: price, ExpiresAt: now.Add(cfg.WatchTimeout)}
|
|
logf("INFO", "[%s-观察] %s 现价=%.2f", tag, code, price)
|
|
return false
|
|
}
|
|
if price < watch.LastClose {
|
|
watch.LastClose = price
|
|
watch.ExpiresAt = now.Add(cfg.WatchTimeout)
|
|
store[code] = watch
|
|
logf("INFO", "[%s-下跌] %s 刷新低点=%.2f", tag, code, price)
|
|
return false
|
|
}
|
|
rebound := (price - watch.LastClose) / watch.LastClose * 100
|
|
if rebound <= 0 {
|
|
return false
|
|
}
|
|
if rebound < cfg.ReboundThreshold {
|
|
logf("INFO", "[%s-等待] %s 反弹=%.2f%% 阈值=%.2f%%", tag, code, rebound, cfg.ReboundThreshold)
|
|
return false
|
|
}
|
|
delete(store, code)
|
|
logf("INFO", "[%s-触发] %s 反弹=%.2f%% 低点=%.2f", tag, code, rebound, watch.LastClose)
|
|
return true
|
|
}
|