dev 5
This commit is contained in:
@@ -39,7 +39,7 @@ func Overview(assets *sdk.Assets, positions []sdk.Position) {
|
||||
}
|
||||
}
|
||||
|
||||
func RunOnce(ctx context.Context, client *sdk.Client, signals *libs.SignalResult) {
|
||||
func RunOnce(ctx context.Context, client *sdk.Client, signals []*libs.SignalItem) {
|
||||
if !libs.TradingTime(time.Now()) {
|
||||
return
|
||||
}
|
||||
@@ -71,12 +71,13 @@ func RunOnce(ctx context.Context, client *sdk.Client, signals *libs.SignalResult
|
||||
allCodes = append(allCodes, pos_codes...)
|
||||
|
||||
// 5 验证有效开仓信号
|
||||
allowOpen := make([]libs.SignalItem, 0)
|
||||
for code, item := range signals.Data {
|
||||
if !slices.Contains(pos_codes, code) {
|
||||
allowOpen := make([]*libs.SignalItem, 0)
|
||||
for _, item := range signals {
|
||||
if !slices.Contains(pos_codes, item.Code) {
|
||||
allowOpen = append(allowOpen, item)
|
||||
}
|
||||
}
|
||||
allowOpen = SignalFilter(allowOpen, config.Account.SignalAllow)
|
||||
|
||||
// 6 获取行情tick
|
||||
ticks, err := client.FullTick(ctx, allCodes)
|
||||
@@ -87,10 +88,24 @@ func RunOnce(ctx context.Context, client *sdk.Client, signals *libs.SignalResult
|
||||
|
||||
// 7 执行开仓:有开仓信号 && 大盘指数允许开仓
|
||||
if len(allowOpen) > 0 && IsAllow {
|
||||
openSignal(ctx, client, ticks, allowOpen)
|
||||
openSignal(client, ticks, allowOpen)
|
||||
}
|
||||
|
||||
// 8 持仓计算
|
||||
buyBudget := assets.Available
|
||||
managePositions(ctx, client, ticks, positions, IsAllow, &buyBudget)
|
||||
managePositions(client, ticks, positions, IsAllow)
|
||||
}
|
||||
|
||||
func SignalFilter(in []*libs.SignalItem, name []string) []*libs.SignalItem {
|
||||
newSignalItem := make([]*libs.SignalItem, 0)
|
||||
if len(name) == 0 {
|
||||
return newSignalItem
|
||||
}
|
||||
for _, i := range in {
|
||||
for _, n := range name {
|
||||
if i.SignalKey == n {
|
||||
newSignalItem = append(newSignalItem, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return newSignalItem
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"big-qmt/go-client/config"
|
||||
"big-qmt/go-client/libs"
|
||||
"big-qmt/go-client/sdk"
|
||||
)
|
||||
|
||||
func openSignal(ctx context.Context, client *sdk.Client, ticks map[string]sdk.Tick, openSignals []libs.SignalItem) {
|
||||
func openSignal(client *sdk.Client, ticks map[string]sdk.Tick, openSignals []*libs.SignalItem) {
|
||||
for _, item := range openSignals {
|
||||
// 是否有锁
|
||||
if OrderBook.IsLock("BUY", item.Code) {
|
||||
155
go-client/apps/trend/logic/positions.go
Normal file
155
go-client/apps/trend/logic/positions.go
Normal file
@@ -0,0 +1,155 @@
|
||||
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()
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"big-qmt/go-client/apps/zt/logic"
|
||||
"big-qmt/go-client/apps/trend/logic"
|
||||
"big-qmt/go-client/config"
|
||||
"big-qmt/go-client/libs"
|
||||
"big-qmt/go-client/sdk"
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
StrategyName = "zt"
|
||||
StrategyName = "trend"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -38,7 +38,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化
|
||||
// 第三步 初始化
|
||||
logic.NewOrderBook()
|
||||
logic.InitWatch()
|
||||
if err := logic.InitState(StrategyName); err != nil {
|
||||
@@ -47,15 +47,14 @@ func main() {
|
||||
if err := logic.SyncPositions(positions); err != nil {
|
||||
log.Panicln("ERROR", err.Error())
|
||||
}
|
||||
|
||||
// 第三步:连接成功后接管首次持仓并打印账户概览。
|
||||
logic.Overview(assets, positions)
|
||||
signals, err := libs.FetchSignal(libs.Dcm_Signal, config.Account.HostKey)
|
||||
signals, err := libs.InitSignals()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [ZT] 获取开仓信号失败: %v", err)
|
||||
signals = &libs.SignalResult{Data: map[string]libs.SignalItem{}}
|
||||
log.Panicln("ERROR", err.Error())
|
||||
}
|
||||
log.Printf("[INFO] [ZT] 已加载 %d 个开仓信号", len(signals.Data))
|
||||
|
||||
// 打印启动信息
|
||||
logic.Overview(assets, positions)
|
||||
log.Printf("[INFO] 已加载 %d 个开仓信号", len(signals))
|
||||
|
||||
// 第四步:工作日每 30 秒触发,交易时段由 RunOnce 统一判断。
|
||||
scheduler := cron.New(
|
||||
@@ -68,11 +67,11 @@ func main() {
|
||||
log.Fatalf("[ERROR] 创建计划任务失败: %v", err)
|
||||
}
|
||||
scheduler.Start()
|
||||
log.Printf("[INFO] [ZT] 计划任务已启动")
|
||||
log.Printf("[INFO] 计划任务已启动")
|
||||
|
||||
<-ctx.Done()
|
||||
<-scheduler.Stop().Done()
|
||||
log.Printf("[INFO] [ZT] 停止")
|
||||
log.Printf("[INFO] 停止")
|
||||
}
|
||||
|
||||
func waitForQMT(ctx context.Context, client *sdk.Client) (*sdk.Assets, []sdk.Position, bool) {
|
||||
@@ -82,10 +81,10 @@ func waitForQMT(ctx context.Context, client *sdk.Client) (*sdk.Assets, []sdk.Pos
|
||||
_, positions, positionsErr := client.Positions(attempt)
|
||||
cancel()
|
||||
if assetsErr == nil && positionsErr == nil {
|
||||
log.Printf("[INFO] [ZT] QMT连接成功: %s", config.Global.QMTBaseURL)
|
||||
log.Printf("[INFO] QMT连接成功: %s", config.Global.QMTBaseURL)
|
||||
return assets, positions, true
|
||||
}
|
||||
log.Printf("[WARNING] [ZT] QMT未就绪,5秒后重试: assets=%v positions=%v", assetsErr, positionsErr)
|
||||
log.Printf("[WARNING] QMT未就绪,5秒后重试: assets=%v positions=%v", assetsErr, positionsErr)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, nil, false
|
||||
@@ -1,237 +0,0 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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(_ context.Context, client *sdk.Client, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool, budget *float64) {
|
||||
if QuantState == nil || OrderBook == nil || positions == nil {
|
||||
return
|
||||
}
|
||||
if err := OrderBook.Refresh(client); err != nil {
|
||||
logf("ERROR", "[持仓] 刷新委托失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
current := make(map[string]sdk.Position, len(positions))
|
||||
for _, position := range positions {
|
||||
if position.StockCode != "" {
|
||||
current[position.StockCode] = position
|
||||
}
|
||||
}
|
||||
|
||||
for _, code := range stateCodes() {
|
||||
syncPosition(code, current[code])
|
||||
}
|
||||
for code, position := range current {
|
||||
managePosition(client, ticks[code], position, marketOK, budget)
|
||||
}
|
||||
|
||||
saveState()
|
||||
}
|
||||
|
||||
func syncPosition(code string, position sdk.Position) {
|
||||
item, err := QuantState.Get(code)
|
||||
if err != nil || orderBusy(code, "BUY") || orderBusy(code, "SELL") {
|
||||
return
|
||||
}
|
||||
if position.Volume <= 0 {
|
||||
QuantState.Delete(code)
|
||||
forget(code)
|
||||
return
|
||||
}
|
||||
|
||||
if item.BaseStatus == StatusIng {
|
||||
item.BaseQty = max(0, position.Volume-item.AddedQty)
|
||||
item.BaseCost = position.OpenPrice
|
||||
item.BaseStatus = StatusOk
|
||||
}
|
||||
if item.AddedStatus == StatusIng {
|
||||
syncAdded(item, position)
|
||||
}
|
||||
QuantState.Set(item)
|
||||
}
|
||||
|
||||
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
|
||||
clearPeak(item.Code, legAdded)
|
||||
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 managePosition(client *sdk.Client, tick sdk.Tick, position sdk.Position, marketOK bool, budget *float64) {
|
||||
item, err := QuantState.Get(position.StockCode)
|
||||
if err != nil || tick.LastPrice <= 0 || !positionReady(item, position) {
|
||||
return
|
||||
}
|
||||
if item.AddedQty > 0 {
|
||||
pnl := profit(tick.LastPrice, item.AddedCost)
|
||||
if shouldSell(item.Code, legAdded, pnl) {
|
||||
sell(client, item, position.CanUseVolume, item.AddedQty, legAdded, pnl)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pnl := profit(tick.LastPrice, item.BaseCost)
|
||||
if shouldSell(item.Code, legBase, pnl) {
|
||||
sell(client, item, position.CanUseVolume, item.BaseQty, legBase, pnl)
|
||||
} else if pnl <= config.Account.LossTriggerPct {
|
||||
buyAdded(client, item, tick.LastPrice, marketOK, budget)
|
||||
}
|
||||
}
|
||||
|
||||
func positionReady(item *StateItem, position sdk.Position) bool {
|
||||
return item.BaseStatus != StatusIng && item.AddedStatus != StatusIng &&
|
||||
position.Volume > 0 && position.Volume%100 == 0 &&
|
||||
position.Volume == item.BaseQty+item.AddedQty
|
||||
}
|
||||
|
||||
func buyAdded(client *sdk.Client, item *StateItem, price float64, marketOK bool, budget *float64) {
|
||||
if !marketOK || budget == nil || PosbuyWatch == nil || !PosbuyWatch.Triggered("补仓", item.Code, price) {
|
||||
return
|
||||
}
|
||||
volume := calcBuyVolume(price, config.Account.BuyValue)
|
||||
amount := price * float64(volume)
|
||||
if volume <= 0 || 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
|
||||
saveState()
|
||||
}
|
||||
|
||||
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)
|
||||
saveState()
|
||||
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 || config.Account.GridStepPct <= 0 {
|
||||
return false
|
||||
}
|
||||
grid := int(math.Floor(pnl / config.Account.GridStepPct))
|
||||
key := peakKey(code, leg)
|
||||
peakMu.Lock()
|
||||
defer peakMu.Unlock()
|
||||
peak, tracked := peakGrids[key]
|
||||
if !tracked || grid > peak {
|
||||
peakGrids[key] = grid
|
||||
return false
|
||||
}
|
||||
return grid < peak
|
||||
}
|
||||
|
||||
func stateCodes() []string {
|
||||
QuantState.mu.Lock()
|
||||
defer QuantState.mu.Unlock()
|
||||
return append([]string(nil), QuantState.Codes...)
|
||||
}
|
||||
|
||||
func saveState() {
|
||||
if err := QuantState.Save(); err != nil {
|
||||
logf("ERROR", "%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func profit(price, cost float64) float64 {
|
||||
if cost <= 0 {
|
||||
return math.Inf(-1)
|
||||
}
|
||||
return (price - cost) / cost * 100
|
||||
}
|
||||
|
||||
func calcBuyVolume(price, value float64) int {
|
||||
return libs.CalcBuyVolume(price, value)
|
||||
}
|
||||
|
||||
func peakKey(code, leg string) string { return code + "|" + leg }
|
||||
|
||||
func clearPeak(code, leg string) {
|
||||
peakMu.Lock()
|
||||
delete(peakGrids, peakKey(code, leg))
|
||||
peakMu.Unlock()
|
||||
}
|
||||
|
||||
func forget(code string) {
|
||||
if OpenWatch != nil {
|
||||
OpenWatch.mu.Lock()
|
||||
delete(OpenWatch.Data, code)
|
||||
OpenWatch.mu.Unlock()
|
||||
}
|
||||
if PosbuyWatch != nil {
|
||||
PosbuyWatch.mu.Lock()
|
||||
delete(PosbuyWatch.Data, code)
|
||||
PosbuyWatch.mu.Unlock()
|
||||
}
|
||||
clearPeak(code, legBase)
|
||||
clearPeak(code, legAdded)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
# 做 T 策略说明
|
||||
|
||||
## 启动准备
|
||||
|
||||
策略启动后,根据当前计算机选择对应的交易账户。账户连接成功后,展示总资产、可用资金和当前持仓,并读取一次当日开仓信号。运行期间一直使用这份内存信号,不再重复请求。
|
||||
|
||||
首次运行且没有历史策略状态时,账户中已有的全部持仓都作为底仓接管。后续运行以已保存的策略状态为准。
|
||||
|
||||
## 运行时间
|
||||
|
||||
策略仅在周一至周五运行,周末不执行交易计算。每天运行时段为:
|
||||
|
||||
- 09:30 至 11:30;
|
||||
- 13:00 至 15:00。
|
||||
|
||||
交易时段内每 30 秒计算一次。午间休市和收盘后不执行交易计算。
|
||||
|
||||
## 每轮计算流程
|
||||
|
||||
每轮读取账户资产、当前持仓、最新行情和委托情况,并处理已经超过等待时间的委托。
|
||||
|
||||
随后判断开仓信号中的股票是否已经持仓。未持仓信号和已有持仓可以在同一轮中分别处理,不会因为存在未开仓信号而停止管理已有持仓。
|
||||
|
||||
大盘信号只控制买入行为。大盘不允许开仓时,不新建底仓,也不补仓;止盈卖出、委托清理和持仓状态同步仍然正常进行。
|
||||
|
||||
## 底仓开仓
|
||||
|
||||
开仓信号对应的股票尚未持仓,且大盘允许开仓时,进入价格观察阶段。
|
||||
|
||||
观察期间持续记录最低价格。当价格从观察低点反弹达到设定幅度后,触发底仓买入。
|
||||
|
||||
买入数量根据配置的 `buy_value` 和当前股价计算,向下取整为整手。不足一手时按一手买入。
|
||||
|
||||
提交底仓买入后,策略记录正在开仓的状态,等待委托和持仓结果确认。
|
||||
|
||||
## 补仓
|
||||
|
||||
底仓亏损达到配置的补仓触发比例后,进入补仓价格观察阶段。
|
||||
|
||||
观察期间持续记录新的最低价格。当价格从低点反弹达到设定幅度,且大盘允许买入时,触发补仓。
|
||||
|
||||
补仓数量同样根据 `buy_value` 和补仓时的股价独立计算,因此补仓数量不要求与底仓数量相同。部分成交的数量按实际补仓数量接管。
|
||||
|
||||
## 网格止盈
|
||||
|
||||
底仓和补仓分别计算盈利比例,并分别记录本次运行期间达到的最高盈利网格。
|
||||
|
||||
盈利达到最低止盈比例后,策略开始跟踪最高网格。当盈利从最高网格回落时,触发对应仓位的卖出:
|
||||
|
||||
- 补仓达到回撤条件时,只卖出补仓部分;
|
||||
- 底仓达到回撤条件时,可以卖出全部底仓。
|
||||
|
||||
最高盈利网格只在本次程序运行期间保留,程序重新启动后重新开始记录。
|
||||
|
||||
## 委托确认
|
||||
|
||||
每笔委托生成一个不超过 24 个字符的唯一订单号。策略根据账户回写的委托结果确认订单状态,并将结果同步到策略状态。
|
||||
|
||||
回写状态包括已提交、部分成交、全部成交、已撤销和已拒绝。没有回写结果的订单不会直接重复下单。
|
||||
|
||||
委托超过等待时间后,策略先检查当前委托和持仓。如果委托已经不存在,则释放该股票的等待状态,允许后续交易轮次重新判断,但不会在释放状态的同一轮自动重复下单。
|
||||
|
||||
## 状态保护
|
||||
|
||||
策略持续保存底仓数量与成本、补仓数量与成本、当前待确认动作和最近委托状态。
|
||||
|
||||
状态文件不存在时,启动前持仓全部作为底仓接管。状态文件内容异常时,策略停止交易处理,不会自动删除或重建异常文件。
|
||||
@@ -17,21 +17,29 @@ var (
|
||||
)
|
||||
|
||||
type GlobalConfig struct {
|
||||
QMTBaseURL string `yaml:"qmt_base_url"`
|
||||
QMTToken string `yaml:"qmt_token"`
|
||||
APIHost string `yaml:"api_host"`
|
||||
QMTDataDir string `yaml:"qmt_data_dir"`
|
||||
Hosts map[string]string `yaml:"hosts"`
|
||||
QMTBaseURL string `yaml:"qmt_base_url"`
|
||||
QMTToken string `yaml:"qmt_token"`
|
||||
APIHost string `yaml:"api_host"`
|
||||
QMTDataDir string `yaml:"qmt_data_dir"`
|
||||
Hosts map[string]string `yaml:"hosts"`
|
||||
Signals map[string]SignalConfig `yaml:"signals"`
|
||||
}
|
||||
|
||||
type SignalConfig struct {
|
||||
Url string `yaml:"url"`
|
||||
Timezone string `yaml:"timezone"`
|
||||
GtLastPriceIsOpen bool `yaml:"gt_last_price_is_open"`
|
||||
}
|
||||
|
||||
type AccountConfig struct {
|
||||
AccountID string `yaml:"account_id"`
|
||||
HostKey string `yaml:"host_key"`
|
||||
BuyValue float64 `yaml:"buy_value"`
|
||||
MinCashRatio float64 `yaml:"min_cash_ratio"`
|
||||
LossTriggerPct float64 `yaml:"loss_trigger_pct"`
|
||||
GridStepPct float64 `yaml:"grid_step_pct"`
|
||||
MinProfitPct float64 `yaml:"min_profit_pct"`
|
||||
AccountID string `yaml:"account_id"`
|
||||
HostKey string `yaml:"host_key"`
|
||||
BuyValue float64 `yaml:"buy_value"`
|
||||
MinCashRatio float64 `yaml:"min_cash_ratio"`
|
||||
LossTriggerPct float64 `yaml:"loss_trigger_pct"`
|
||||
GridStepPct float64 `yaml:"grid_step_pct"`
|
||||
MinProfitPct float64 `yaml:"min_profit_pct"`
|
||||
SignalAllow []string `yaml:"signal_allow"`
|
||||
}
|
||||
|
||||
// Load 根据 global.yaml 中的 hosts 映射加载当前计算机的账户配置。
|
||||
@@ -68,6 +76,7 @@ func Load(etcDir string) error {
|
||||
return fmt.Errorf("buy_value、grid_step_pct 和超时时间必须大于 0")
|
||||
}
|
||||
|
||||
account.HostKey = strings.ToLower(account.HostKey)
|
||||
Global = &global
|
||||
Account = &account
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# 当前计算机使用的账户和策略参数。
|
||||
account_id: CHANGE_ME
|
||||
host_key: ""
|
||||
host_key: "dev"
|
||||
buy_value: 5000
|
||||
min_cash_ratio: 0.10
|
||||
loss_trigger_pct: -30
|
||||
grid_step_pct: 1
|
||||
min_profit_pct: 2
|
||||
rebound_threshold: 0.61
|
||||
order_timeout_seconds: 60
|
||||
watch_timeout_seconds: 300
|
||||
signal_allow:
|
||||
- dcm
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
# 系统公共参数。hosts 将 Windows 计算机名映射到账户配置文件。
|
||||
qmt_base_url: http://127.0.0.1:10086
|
||||
qmt_token: QMTbyYanweidong
|
||||
api_host: http://go.apinb.com
|
||||
api_host: http://139.224.247.176:13499
|
||||
qmt_data_dir: D:/qmt_strategy_data
|
||||
state_dir: D:/qmt_strategy_state
|
||||
|
||||
hosts:
|
||||
DESKTOP-39H91QV: dev.yaml
|
||||
|
||||
signals:
|
||||
dcm:
|
||||
url: /a/dcm_signal
|
||||
timezone: * # *代表全时间段,9:30-10:30,13:30-14:30 代表2个时间段
|
||||
gt_last_price_is_open: false # 大于信号的昨收价是否开仓
|
||||
morning:
|
||||
url: /a/morning_signal
|
||||
timezone: 9:30-10:30 # *代表全时间段,9:30-10:30,13:30-14:30 代表2个时间段
|
||||
gt_last_price_is_open: true # 大于信号的昨收价是否开仓
|
||||
tail:
|
||||
url: /a/tail_signal
|
||||
timezone: 14:30-14:55 # *代表全时间段,9:30-10:30,13:30-14:30 代表2个时间段
|
||||
gt_last_price_is_open: false # 大于信号的昨收价是否开仓
|
||||
arbitrage:
|
||||
url: /a/arbitrage_signal
|
||||
timezone: * # *代表全时间段,9:30-10:30,13:30-14:30 代表2个时间段
|
||||
gt_last_price_is_open: false # 大于信号的昨收价是否开仓
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package libs
|
||||
|
||||
import (
|
||||
"big-qmt/go-client/config"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
var (
|
||||
Dcm_Signal = "/a/dcm_signal"
|
||||
)
|
||||
|
||||
type SignalResult struct {
|
||||
@@ -19,6 +15,7 @@ type SignalResult struct {
|
||||
}
|
||||
|
||||
type SignalItem struct {
|
||||
SignalKey string `json:"signal_key"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
@@ -26,21 +23,35 @@ type SignalItem struct {
|
||||
TechIndicator map[string]float64 `json:"tech_indicator"`
|
||||
}
|
||||
|
||||
func InitSignals() ([]*SignalItem, error) {
|
||||
CacheSignals := make([]*SignalItem, 0)
|
||||
for key, s := range config.Global.Signals {
|
||||
result, err := FetchSignal(s.Url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[信号] %s, 获取错误:%v", key, err)
|
||||
}
|
||||
for _, item := range result.Data {
|
||||
item.SignalKey = key
|
||||
CacheSignals = append(CacheSignals, &item)
|
||||
}
|
||||
}
|
||||
return CacheSignals, nil
|
||||
}
|
||||
|
||||
// FetchSignals 启动时读取信号,运行期间直接使用内存数据。
|
||||
func FetchSignal(subUrl, host_key string) (*SignalResult, error) {
|
||||
func FetchSignal(subUrl string) (*SignalResult, error) {
|
||||
// gen url.
|
||||
fullUrl := fmt.Sprintf("%s%s?host_key=%s&t=%s", API_HOST, subUrl, host_key, randStr(16))
|
||||
fullUrl := fmt.Sprintf("%s%s?t=%s", API_HOST, subUrl, randStr(16))
|
||||
// doing
|
||||
payload, err := GetJSON(fullUrl, HTTPTimeout)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] 获取60m大盘信号失败: %s %v", fullUrl, err)
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("开仓信号获取失败: %s %v", fullUrl, err)
|
||||
}
|
||||
|
||||
var result SignalResult
|
||||
err = json.Unmarshal(payload, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("开仓信号解析失败: %s %v", fullUrl, err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user