This commit is contained in:
2026-08-25 18:59:18 +08:00
parent fa62436a73
commit ec58641d09
23 changed files with 563 additions and 944 deletions

View File

@@ -1,4 +1,4 @@
package main
package logic
import (
"context"
@@ -6,10 +6,11 @@ import (
"strings"
"time"
"big-qmt/go-client/libs"
"big-qmt/go-client/sdk"
)
func overview(cfg Config, assets *sdk.Assets, positions []sdk.Position) {
func Overview(cfg Config, assets *sdk.Assets, positions []sdk.Position) {
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Printf("【时间】%s\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Printf("【配置】account_id: %s host_key: %s open_money: %.0f\n", cfg.AccountID, cfg.HostKey, cfg.OpenMoney)
@@ -31,8 +32,64 @@ func overview(cfg Config, assets *sdk.Assets, positions []sdk.Position) {
}
}
func runRound(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config, assets *sdk.Assets, ticks map[string]sdk.Tick, positions []sdk.Position) {
func RunOnce(ctx context.Context, client *sdk.Client, books *OrderBook, cfg Config) {
if !tradingTime(time.Now()) {
return
}
roundCtx, cancel := context.WithTimeout(ctx, cfg.HTTPTimeout*4)
defer cancel()
assets, err := client.Assets(roundCtx)
if err != nil {
logf("ERROR", "获取资产失败: %v", err)
return
}
positions, err := client.Positions(roundCtx)
if err != nil {
logf("ERROR", "获取持仓失败: %v", err)
return
}
signals := fetchSignal(cfg, "dcm_signal")
seen := map[string]struct{}{}
stockList := make([]string, 0, len(signals)+len(positions))
addCode := func(code string) {
n := normalizeCode(code, "")
if n == "" {
n = strings.ToUpper(strings.TrimSpace(code))
}
if n == "" {
return
}
if _, ok := seen[n]; ok {
return
}
seen[n] = struct{}{}
stockList = append(stockList, n)
}
for code := range signals {
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[normalizeCode(code, "")] = tick
ticks[code] = tick
}
}
runRound(roundCtx, client, books, cfg, assets, ticks, positions, signals)
}
func runRound(ctx context.Context, client *sdk.Client, books *OrderBook, cfg Config, assets *sdk.Assets, ticks map[string]sdk.Tick, positions []sdk.Position, signals map[string]map[string]any) {
books.cancelExpired(ctx, client, cfg)
hold := positionCodes(positions)
@@ -62,7 +119,7 @@ func runRound(ctx context.Context, client *sdk.Client, books *orderBook, cfg Con
}
}
}
marketOK := marketAllowOpen(cfg)
marketOK := libs.AllowOpen(cfg.APIHost, cfg.HTTPTimeout)
if len(openSignals) > 0 {
openSignal(ctx, client, books, cfg, assets, ticks, openSignals, marketOK)
}

View File

@@ -1,4 +1,4 @@
package main
package logic
import (
"os"
@@ -29,14 +29,14 @@ type Config struct {
ReboundThreshold float64
}
func loadConfig() Config {
func LoadConfig() Config {
cfg := Config{
QMTBaseURL: env("QMT_BASE_URL", "http://127.0.0.1:10086"),
QMTToken: env("QMT_TOKEN", "QMTbyYanweidong"),
AccountType: env("QMT_ACCOUNT", "stock"),
AccountID: env("ACCOUNT_ID", ""),
HostKey: env("HOST_KEY", ""),
APIHost: strings.TrimRight(env("API_HOST", "http://139.224.247.176:13499"), "/"),
APIHost: strings.TrimRight(env("API_HOST", "http://go.apinb.com"), "/"),
DataDir: env("DATA_DIR", "D:/qmt_strategy_state"),
HTTPTimeout: durationEnv("HTTP_TIMEOUT_SEC", 5) * time.Second,
OrderTimeout: durationEnv("ORDER_TIMEOUT_SEC", 60) * time.Second,
@@ -55,10 +55,6 @@ func loadConfig() Config {
logf("ERROR", "ACCOUNT_ID 为空")
os.Exit(1)
}
if strings.TrimSpace(cfg.HostKey) == "" {
logf("ERROR", "HOST_KEY 为空")
os.Exit(1)
}
if cfg.MinCashRatio < 0 || cfg.MinCashRatio >= 1 {
logf("ERROR", "MIN_CASH_RATIO 必须在 [0, 1)")
os.Exit(1)

View File

@@ -1,4 +1,4 @@
package main
package logic
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package main
package logic
import (
"context"
@@ -19,7 +19,7 @@ var openDip = struct {
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) {
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
}

View File

@@ -1,4 +1,4 @@
package main
package logic
import (
"context"
@@ -51,7 +51,7 @@ type submission struct {
Tag string
}
type orderBook struct {
type OrderBook struct {
mu sync.Mutex
cached []parsedOrder
hasCache bool
@@ -60,21 +60,21 @@ type orderBook struct {
subs []submission
}
func newOrderBook() *orderBook {
return &orderBook{
func NewOrderBook() *OrderBook {
return &OrderBook{
buyLocks: map[string]time.Time{},
sellLocks: map[string]time.Time{},
}
}
func (o *orderBook) invalidate() {
func (o *OrderBook) invalidate() {
o.mu.Lock()
defer o.mu.Unlock()
o.hasCache = false
o.cached = nil
}
func (o *orderBook) query(ctx context.Context, client *sdk.Client, cfg Config) ([]parsedOrder, error) {
func (o *OrderBook) query(ctx context.Context, client *sdk.Client, cfg Config) ([]parsedOrder, error) {
o.mu.Lock()
if o.hasCache {
out := append([]parsedOrder(nil), o.cached...)
@@ -82,7 +82,7 @@ func (o *orderBook) query(ctx context.Context, client *sdk.Client, cfg Config) (
return out, nil
}
o.mu.Unlock()
raw, err := client.TradeDetailData(ctx, cfg.AccountType, "order")
raw, err := client.TradeDetailData(ctx, "order")
if err != nil {
logf("ERROR", "[ZT][委托] 查询失败: %v", err)
return nil, err
@@ -98,13 +98,13 @@ func (o *orderBook) query(ctx context.Context, client *sdk.Client, cfg Config) (
return orders, nil
}
func (o *orderBook) refresh(ctx context.Context, client *sdk.Client, cfg Config) bool {
func (o *OrderBook) refresh(ctx context.Context, client *sdk.Client, cfg Config) bool {
o.invalidate()
_, err := o.query(ctx, client, cfg)
return err == nil
}
func (o *orderBook) activeSets(ctx context.Context, client *sdk.Client, cfg Config) (buys, sells map[string]struct{}, ok bool) {
func (o *OrderBook) activeSets(ctx context.Context, client *sdk.Client, cfg Config) (buys, sells map[string]struct{}, ok bool) {
orders, err := o.query(ctx, client, cfg)
if err != nil {
return nil, nil, false
@@ -123,7 +123,7 @@ func (o *orderBook) activeSets(ctx context.Context, client *sdk.Client, cfg Conf
return buys, sells, true
}
func (o *orderBook) cancelExpired(ctx context.Context, client *sdk.Client, cfg Config) bool {
func (o *OrderBook) cancelExpired(ctx context.Context, client *sdk.Client, cfg Config) bool {
o.invalidate()
orders, err := o.query(ctx, client, cfg)
if err != nil {
@@ -154,7 +154,7 @@ func (o *orderBook) cancelExpired(ctx context.Context, client *sdk.Client, cfg C
}
seen[key] = struct{}{}
if order.OrderID != "" {
can, err := client.CanCancelOrder(ctx, order.OrderID, cfg.AccountType)
can, err := client.CanCancelOrder(ctx, order.OrderID)
if err != nil {
logf("ERROR", "[ZT][委托] 查询是否可撤失败 %s: %v", order.OrderID, err)
continue
@@ -164,7 +164,7 @@ func (o *orderBook) cancelExpired(ctx context.Context, client *sdk.Client, cfg C
continue
}
}
ret, err := client.CancelByRule(ctx, order.StockCode, vol, cfg.AccountType)
ret, err := client.CancelByRule(ctx, order.StockCode, vol)
if err != nil {
logf("ERROR", "[ZT][委托] 撤单失败 %s %s: %v", order.OrderID, order.StockCode, err)
continue
@@ -183,7 +183,7 @@ func (o *orderBook) cancelExpired(ctx context.Context, client *sdk.Client, cfg C
return true
}
func (o *orderBook) claimed(state *ZTState, order parsedOrder) bool {
func (o *OrderBook) claimed(state *ZTState, order parsedOrder) bool {
if order.RemarkOwned {
return true
}
@@ -212,7 +212,7 @@ func (o *orderBook) claimed(state *ZTState, order parsedOrder) bool {
}
}
func (o *orderBook) unlockSide(code, side string) {
func (o *OrderBook) unlockSide(code, side string) {
o.mu.Lock()
defer o.mu.Unlock()
delete(o.locks(side), code)
@@ -227,28 +227,28 @@ func (o *orderBook) unlockSide(code, side string) {
o.subs = o.subs[:n]
}
func (o *orderBook) sideBusy(cfg Config, code, side string, active map[string]struct{}) bool {
func (o *OrderBook) sideBusy(cfg Config, code, side string, active map[string]struct{}) bool {
if _, ok := active[code]; ok {
return true
}
return o.locked(cfg, code, side)
}
func (o *orderBook) locked(cfg Config, code, side string) bool {
func (o *OrderBook) locked(cfg Config, code, side string) bool {
o.mu.Lock()
defer o.mu.Unlock()
ts, ok := o.locks(side)[code]
return ok && time.Since(ts) < cfg.OrderTimeout
}
func (o *orderBook) locks(side string) map[string]time.Time {
func (o *OrderBook) locks(side string) map[string]time.Time {
if side == "buy" {
return o.buyLocks
}
return o.sellLocks
}
func (o *orderBook) hasActive(ctx context.Context, client *sdk.Client, cfg Config, code, side string) bool {
func (o *OrderBook) hasActive(ctx context.Context, client *sdk.Client, cfg Config, code, side string) bool {
orders, err := o.query(ctx, client, cfg)
if err != nil {
return true
@@ -261,7 +261,7 @@ func (o *orderBook) hasActive(ctx context.Context, client *sdk.Client, cfg Confi
return false
}
func (o *orderBook) place(ctx context.Context, client *sdk.Client, cfg Config, side, code string, volume int, tag string) bool {
func (o *OrderBook) place(ctx context.Context, client *sdk.Client, cfg Config, side, code string, volume int, tag string) bool {
if volume <= 0 || volume%100 != 0 {
logf("ERROR", "[ZT][委托] %s 拒绝非整手数量=%d", code, volume)
return false

View File

@@ -1,4 +1,4 @@
package main
package logic
import (
"context"
@@ -32,7 +32,7 @@ func positionCodes(positions []sdk.Position) map[string]struct{} {
return out
}
func managePositions(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool) {
func managePositions(ctx context.Context, client *sdk.Client, books *OrderBook, cfg Config, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool) {
if positions == nil {
logf("ERROR", "[ZT][持仓] 持仓查询失败,本轮跳过")
return
@@ -129,7 +129,7 @@ func managePositions(ctx context.Context, client *sdk.Client, books *orderBook,
state.Save()
}
func syncItem(cfg Config, state *ZTState, code string, volume int, avgPrice float64, buys, sells map[string]struct{}, books *orderBook) *SymbolState {
func syncItem(cfg Config, state *ZTState, code string, volume int, avgPrice float64, buys, sells map[string]struct{}, books *OrderBook) *SymbolState {
item := state.Get(code)
if item == nil {
if volume > 0 {
@@ -162,7 +162,7 @@ func syncItem(cfg Config, state *ZTState, code string, volume int, avgPrice floa
return state.Get(code)
}
func syncOpen(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *orderBook) {
func syncOpen(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *OrderBook) {
if volume > 0 {
item.BaseQty, item.BaseCost = volume, avgPrice
}
@@ -178,7 +178,7 @@ func syncOpen(cfg Config, state *ZTState, item *SymbolState, volume int, avgPric
logf("INFO", "[ZT][持仓] %s 开仓确认 数量=%d 成本=%.2f", item.Code, item.BaseQty, item.BaseCost)
}
func syncAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *orderBook) {
func syncAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, buys map[string]struct{}, books *OrderBook) {
if volume > item.BaseQty {
item.AddQty = volume - item.BaseQty
if item.AddQty > 0 {
@@ -201,7 +201,7 @@ func syncAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice
item.Pending = ""
}
func syncSellAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *orderBook) {
func syncSellAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *OrderBook) {
if volume <= 0 {
if !books.sideBusy(cfg, item.Code, "sell", sells) {
state.Remove(item.Code)
@@ -223,7 +223,7 @@ func syncSellAdd(cfg Config, state *ZTState, item *SymbolState, volume int, avgP
}
}
func syncSellBase(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *orderBook) {
func syncSellBase(cfg Config, state *ZTState, item *SymbolState, volume int, avgPrice float64, sells map[string]struct{}, books *OrderBook) {
if volume <= 0 {
if !books.sideBusy(cfg, item.Code, "sell", sells) {
state.Remove(item.Code)
@@ -237,7 +237,7 @@ func syncSellBase(cfg Config, state *ZTState, item *SymbolState, volume int, avg
}
}
func addOnRebound(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config, item *SymbolState, price float64, marketOK bool) {
func addOnRebound(ctx context.Context, client *sdk.Client, books *OrderBook, cfg Config, item *SymbolState, price float64, marketOK bool) {
if !marketOK || !dipTriggered(&posDip.mu, posDip.store, cfg, "补仓", item.Code, price) {
return
}
@@ -266,7 +266,7 @@ func retreated(cfg Config, item *SymbolState, leg string, pnl float64) bool {
return grid < peak
}
func sellLeg(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config, item *SymbolState, usable, volume int, leg string, pnl float64) {
func sellLeg(ctx context.Context, client *sdk.Client, books *OrderBook, cfg Config, item *SymbolState, usable, volume int, leg string, pnl float64) {
volume -= volume % 100
if volume <= 0 || usable < volume {
logf("INFO", "[ZT][止盈] %s 可用股数不足,需要=%d 可用=%d", item.Code, volume, usable)

View File

@@ -1,4 +1,4 @@
package main
package logic
import (
"encoding/json"
@@ -116,7 +116,10 @@ func loadDailyFile(path string) *dailyCache {
func fetchSignal(cfg Config, name string) map[string]map[string]any {
cached := daily(cfg, name, "open_%s.json", func() (any, error) {
q := url.Values{"host_key": {cfg.HostKey}}
var q url.Values
if strings.TrimSpace(cfg.HostKey) != "" {
q = url.Values{"host_key": {cfg.HostKey}}
}
payload, err := getJSON(cfg.APIHost+"/a/"+name, q, cfg.HTTPTimeout)
if err != nil {
return nil, err
@@ -179,100 +182,3 @@ func normalizeZT(payload map[string]any) map[string]map[string]any {
}
return out
}
func passCodes(cfg Config) []string {
load := func() (any, error) {
payload, err := getJSON(cfg.APIHost+"/a/pass_codes", nil, cfg.HTTPTimeout)
if err != nil {
return nil, err
}
data, _ := payload["data"].([]any)
if data == nil {
return nil, fmt.Errorf("接口 data 不是数组")
}
codes := make([]string, 0, len(data))
for _, item := range data {
s := strings.ToUpper(strings.TrimSpace(fmt.Sprint(item)))
if s != "" && s != "<nil>" {
codes = append(codes, s)
}
}
return codes, nil
}
cached := daily(cfg, "pass_codes", "pass_codes_%s.json", load, time.Now())
codes := codesFromAny(cached)
if len(codes) > 0 {
return codes
}
logf("INFO", "pass_codes 为空,重新获取")
data, err := load()
if err != nil {
logf("ERROR", "pass_codes 重新获取失败: %v", err)
return nil
}
list, _ := data.([]string)
return list
}
func codesFromAny(cached *dailyCache) []string {
if cached == nil || !cached.OK {
return nil
}
switch v := cached.Data.(type) {
case []string:
return v
case []any:
out := make([]string, 0, len(v))
for _, item := range v {
s := strings.ToUpper(strings.TrimSpace(fmt.Sprint(item)))
if s != "" && s != "<nil>" {
out = append(out, s)
}
}
return out
}
return nil
}
func marketAllowOpen(cfg Config) bool {
payload, err := getJSON(cfg.APIHost+"/a/market", url.Values{"period": {"60m"}}, cfg.HTTPTimeout)
if err != nil {
logf("ERROR", "获取60m大盘信号失败: %s %v", cfg.APIHost+"/a/market", err)
return false
}
status := marketStatus(payload)
logf("INFO", "大盘信号: status=%s", status)
return status == "UP"
}
func marketStatus(payload map[string]any) string {
var value any = payload
if m, ok := value.(map[string]any); ok {
if d, exists := m["data"]; exists {
value = d
}
}
if arr, ok := value.([]any); ok {
if len(arr) == 0 {
value = nil
} else {
value = arr[len(arr)-1]
}
}
if m, ok := value.(map[string]any); ok {
if v, exists := m["action"]; exists {
value = v
} else if v, exists := m["status"]; exists {
value = v
} else if v, exists := m["signal"]; exists {
value = v
}
}
s := strings.ToUpper(strings.TrimSpace(fmt.Sprint(value)))
switch s {
case "UP", "DOWN", "NEUTRAL":
return s
default:
return "UNKNOWN"
}
}

View File

@@ -1,4 +1,4 @@
package main
package logic
import (
"encoding/json"

View File

@@ -1,4 +1,4 @@
package main
package logic
import (
"fmt"
@@ -92,13 +92,3 @@ func asIntS(s string) int {
}
return n
}
func asFloatS(s string) float64 {
s = strings.TrimSpace(s)
if s == "" {
return 0
}
var f float64
_, _ = fmt.Sscanf(s, "%f", &f)
return f
}

View File

@@ -5,101 +5,46 @@ import (
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"big-qmt/go-client/apps/zt/logic"
"big-qmt/go-client/sdk"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
cfg := loadConfig()
client := sdk.New(cfg.QMTBaseURL, cfg.QMTToken, cfg.AccountType, cfg.HTTPTimeout)
books := newOrderBook()
cfg := logic.LoadConfig()
client := sdk.New(cfg.QMTBaseURL, cfg.QMTToken, cfg.HTTPTimeout).SetAccountType(cfg.AccountType)
books := logic.NewOrderBook()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
startup := context.Background()
assets, err := client.Assets(startup, cfg.AccountType)
assets, err := client.Assets(startup)
if err != nil {
logf("ERROR", "启动获取资产失败: %v", err)
log.Printf("[ERROR] 启动获取资产失败: %v", err)
}
positions, err := client.Positions(startup, cfg.AccountType)
positions, err := client.Positions(startup)
if err != nil {
logf("ERROR", "启动获取持仓失败: %v", err)
log.Printf("[ERROR] 启动获取持仓失败: %v", err)
positions = []sdk.Position{}
}
overview(cfg, assets, positions)
logf("INFO", "[ZT] host_key=%s interval=%s", cfg.HostKey, cfg.LoopInterval)
logf("INFO", "[ZT] Init Success, waiting trading session")
logic.Overview(cfg, assets, positions)
log.Printf("[INFO] [ZT] host_key=%s interval=%s signal=%s/a/dcm_signal", cfg.HostKey, cfg.LoopInterval, cfg.APIHost)
log.Printf("[INFO] [ZT] Init Success, waiting trading session")
ticker := time.NewTicker(cfg.LoopInterval)
defer ticker.Stop()
runOnce(ctx, client, books, cfg)
logic.RunOnce(ctx, client, books, cfg)
for {
select {
case <-ctx.Done():
logf("INFO", "[ZT] 停止")
log.Printf("[INFO] [ZT] 停止")
return
case <-ticker.C:
runOnce(ctx, client, books, cfg)
logic.RunOnce(ctx, client, books, cfg)
}
}
}
func runOnce(ctx context.Context, client *sdk.Client, books *orderBook, cfg Config) {
if !tradingTime(time.Now()) {
return
}
roundCtx, cancel := context.WithTimeout(ctx, cfg.HTTPTimeout*4)
defer cancel()
assets, err := client.Assets(roundCtx, cfg.AccountType)
if err != nil {
logf("ERROR", "获取资产失败: %v", err)
return
}
positions, err := client.Positions(roundCtx, cfg.AccountType)
if err != nil {
logf("ERROR", "获取持仓失败: %v", err)
return
}
codes := passCodes(cfg)
seen := map[string]struct{}{}
stockList := make([]string, 0, len(codes)+len(positions))
addCode := func(code string) {
n := normalizeCode(code, "")
if n == "" {
n = strings.ToUpper(strings.TrimSpace(code))
}
if n == "" {
return
}
if _, ok := seen[n]; ok {
return
}
seen[n] = struct{}{}
stockList = append(stockList, n)
}
for _, code := range codes {
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[normalizeCode(code, "")] = tick
ticks[code] = tick
}
}
runRound(roundCtx, client, books, cfg, assets, ticks, positions)
}