This commit is contained in:
2026-08-26 02:10:05 +08:00
parent f14423418a
commit 550fdbf016
11 changed files with 140 additions and 177 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"slices"
"strings"
"time"
@@ -42,83 +43,53 @@ func RunOnce(ctx context.Context, client *sdk.Client, books *OrderBook, signals
if !libs.TradingTime(time.Now()) {
return
}
// 每轮先消费 QMT 回写,终态订单会立即释放本地委托锁。
books.readReceipts()
roundCtx, cancel := context.WithTimeout(ctx, config.HttpTimeOut*4)
defer cancel()
assets, err := client.Assets(roundCtx)
// 1 取消过期订单
books.CancelExpired(ctx, client)
// 2 验证可用资金
assets, err := client.Assets(ctx)
if err != nil {
logf("ERROR", "获取资产失败: %v", err)
return
}
positions, err := client.Positions(roundCtx)
if assets.Available < assets.Total*config.Account.MinCashRatio {
logf("INFO", "资金总闸:可用金额太少,禁止开新仓")
return
}
// 3 获取大盘状态
IsAllow := libs.MarketAllowOpen()
// 4 获取持仓
var allCodes []string
pos_codes, positions, err := client.Positions(ctx)
if err != nil {
logf("ERROR", "获取持仓失败: %v", err)
return
}
allCodes = append(allCodes, pos_codes...)
seen := map[string]struct{}{}
stockList := make([]string, 0, len(signals.Data)+len(positions))
addCode := func(code string) {
if code == "" {
return
// 5 验证有效开仓信号
allowOpen := make([]libs.SignalItem, 0)
for code, item := range signals.Data {
if !slices.Contains(pos_codes, code) {
allowOpen = append(allowOpen, item)
}
if _, ok := seen[code]; ok {
return
}
seen[code] = struct{}{}
stockList = append(stockList, code)
}
for code := range signals.Data {
addCode(code)
}
for _, p := range positions {
addCode(p.StockCode)
}
ticks := map[string]sdk.Tick{}
if len(stockList) > 0 {
raw, err := client.FullTick(roundCtx, stockList)
if err != nil {
logf("ERROR", "获取行情失败: %v", err)
return
}
for code, tick := range raw {
ticks[code] = tick
}
}
runRound(roundCtx, client, books, assets, ticks, positions, signals.Data)
}
func runRound(ctx context.Context, client *sdk.Client, books *OrderBook, assets *sdk.Assets, ticks map[string]sdk.Tick, positions []sdk.Position, signals map[string]libs.SignalItem) {
if !books.cancelExpired(ctx, client) {
logf("ERROR", "[ZT] 委托查询失败,本轮跳过")
return
}
buys, sells, ok := books.activeSets(ctx, client)
if !ok {
// 6 获取行情tick
ticks, err := client.FullTick(ctx, allCodes)
if err != nil {
logf("ERROR", "获取行情失败: %v", err)
return
}
hold := positionCodes(positions)
openSignals := map[string]libs.SignalItem{}
for code, signal := range signals {
if _, held := hold[code]; held {
continue
}
openSignals[code] = signal
// 7 执行开仓:有开仓信号 && 大盘指数允许开仓
if len(allowOpen) > 0 && IsAllow {
openSignal(client, books, ticks, allowOpen)
}
for code := range buys {
delete(openSignals, code)
}
marketOK := libs.AllowOpen()
buyBudget := 0.0
if assets != nil {
buyBudget = assets.Available - assets.Total*config.Account.MinCashRatio
}
if len(openSignals) > 0 {
openSignal(ctx, client, books, ticks, openSignals, marketOK, &buyBudget)
}
managePositions(ctx, client, books, ticks, positions, buys, sells, marketOK, &buyBudget)
// 8 持仓计算
managePositions(client, books, ticks, positions, IsAllow)
}

View File

@@ -0,0 +1,55 @@
package logic
import (
"big-qmt/go-client/config"
"sync"
"time"
)
type dipWatch struct {
LastClose float64
ExpiresAt time.Time
}
var openDip = struct {
mu sync.Mutex
store map[string]dipWatch
}{store: map[string]dipWatch{}}
var posDip = struct {
mu sync.Mutex
store map[string]dipWatch
}{store: map[string]dipWatch{}}
func dipTriggered(mu *sync.Mutex, store map[string]dipWatch, 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(time.Duration(config.Account.WatchTimeoutSec) * time.Second)}
logf("INFO", "[%s-观察] %s 现价=%.2f", tag, code, price)
return false
}
if price < watch.LastClose {
watch.LastClose = price
watch.ExpiresAt = now.Add(time.Duration(config.Account.WatchTimeoutSec) * time.Second)
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 < config.Account.ReboundThreshold {
logf("INFO", "[%s-等待] %s 反弹=%.2f%% 阈值=%.2f%%", tag, code, rebound, config.Account.ReboundThreshold)
return false
}
delete(store, code)
logf("INFO", "[%s-触发] %s 反弹=%.2f%% 低点=%.2f", tag, code, rebound, watch.LastClose)
return true
}

View File

@@ -1,114 +1,38 @@
package logic
import (
"context"
"math"
"sync"
"time"
"big-qmt/go-client/config"
"big-qmt/go-client/libs"
"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, ticks map[string]sdk.Tick, openSignals map[string]libs.SignalItem, marketOK bool, buyBudget *float64) {
if !marketOK {
return
}
if buyBudget == nil || *buyBudget <= 0 {
return
}
func openSignal(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 code := range openSignals {
if code == "" {
logf("ERROR", "[ZT][开仓] 无效股票代码")
for _, item := range openSignals {
if state.Get(item.Code) != nil {
continue
}
if state.Get(code) != nil {
continue
}
price := ticks[code].LastPrice
price := ticks[item.Code].LastPrice
if price <= 0 {
continue
}
if !dipTriggered(&openDip.mu, openDip.store, "开仓", code, price) {
if !dipTriggered(&openDip.mu, openDip.store, "开仓", item.Code, price) {
continue
}
volume := calcBuyVolume(price, config.Account.BuyValue)
volume := libs.CalcBuyVolume(price, config.Account.BuyValue)
if volume <= 0 {
continue
}
estimated := price * float64(volume)
if estimated > *buyBudget {
logf("INFO", "[ZT][开仓] %s 可用买入预算不足,需要=%.2f 剩余=%.2f", code, estimated, *buyBudget)
continue
}
orderID := newOrderTag("base")
if !books.place(ctx, client, sideBuy, code, volume, orderID) {
if !books.place(ctx, client, sideBuy, item.Code, volume, orderID) {
continue
}
setPending(state.Ensure(code), pendingBaseOpening, orderID)
*buyBudget -= estimated
setPending(state.Ensure(item.Code), pendingBaseOpening, orderID)
state.Save()
logf("INFO", "[ZT][开仓] %s 买入 %d 股", code, volume)
logf("INFO", "[ZT][开仓] %s 买入 %d 股", item.Code, volume)
}
}
func calcBuyVolume(price, buyValue float64) int {
if price <= 0 || buyValue <= 0 {
return 0
}
// 不足一手时仍按最低一手委托。
hands := int(math.Floor(buyValue / (price * 100)))
if hands == 0 {
hands = 1
}
return hands * 100
}
func dipTriggered(mu *sync.Mutex, store map[string]dipWatch, 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(time.Duration(config.Account.WatchTimeoutSec) * time.Second)}
logf("INFO", "[%s-观察] %s 现价=%.2f", tag, code, price)
return false
}
if price < watch.LastClose {
watch.LastClose = price
watch.ExpiresAt = now.Add(time.Duration(config.Account.WatchTimeoutSec) * time.Second)
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 < config.Account.ReboundThreshold {
logf("INFO", "[%s-等待] %s 反弹=%.2f%% 阈值=%.2f%%", tag, code, rebound, config.Account.ReboundThreshold)
return false
}
delete(store, code)
logf("INFO", "[%s-触发] %s 反弹=%.2f%% 低点=%.2f", tag, code, rebound, watch.LastClose)
return true
}

View File

@@ -178,7 +178,7 @@ func (o *OrderBook) activeSets(ctx context.Context, client *sdk.Client) (buys, s
return buys, sells, true
}
func (o *OrderBook) cancelExpired(ctx context.Context, client *sdk.Client) bool {
func (o *OrderBook) CancelExpired(ctx context.Context, client *sdk.Client) bool {
o.invalidate()
orders, err := o.query(ctx, client)
if err != nil {

View File

@@ -9,31 +9,12 @@ import (
"big-qmt/go-client/sdk"
)
var posDip = struct {
mu sync.Mutex
store map[string]dipWatch
}{store: map[string]dipWatch{}}
var peakMu sync.Mutex
var peakGrids = map[string]int{}
func peakKey(code, leg string) string { return code + "|" + leg }
func positionCodes(positions []sdk.Position) map[string]struct{} {
out := map[string]struct{}{}
for _, p := range positions {
if p.Volume <= 0 {
continue
}
code := p.StockCode
if code != "" {
out[code] = struct{}{}
}
}
return out
}
func managePositions(ctx context.Context, client *sdk.Client, books *OrderBook, ticks map[string]sdk.Tick, positions []sdk.Position, buys, sells map[string]struct{}, marketOK bool, buyBudget *float64) {
func managePositions(client *sdk.Client, books *OrderBook, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool) {
if positions == nil {
logf("ERROR", "[ZT][持仓] 持仓查询失败,本轮跳过")
return