28 changed files with 1374 additions and 548 deletions
@ -0,0 +1,175 @@
|
||||
package backtest |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sig-pub/pkg/trade" |
||||
"sig-pub/pkg/types" |
||||
"sig-pub/pkg/utils/collect" |
||||
"sig-pub/pkg/zlog" |
||||
) |
||||
|
||||
type BacktestTradeAccount struct { |
||||
trader *TradeSimulator // 下单器
|
||||
traderOrderId int64 // 订单id递增
|
||||
cash float64 // 可用资金
|
||||
positions map[string]*trade.Position // 仓位信息,key: symbol, value: qty(不能持有同一交易产品反方向单, 交易中不能改变杠杆)
|
||||
trades map[int64]*trade.TradeOrder // 所有交易单
|
||||
openTrades map[string][]int64 // 未平仓交易单,key: symbol, value: tradeId
|
||||
closeTrades []int64 // 平仓交易单
|
||||
} |
||||
|
||||
func NewBacktestTradeAccount(cash float64) *BacktestTradeAccount { |
||||
return &BacktestTradeAccount{ |
||||
trader: NewTradeSimulator(0.0005, 0.0008), |
||||
traderOrderId: 1, |
||||
cash: cash, |
||||
positions: make(map[string]*trade.Position), |
||||
} |
||||
} |
||||
|
||||
// Equity 账户净值
|
||||
func (a *BacktestTradeAccount) GetCash() float64 { |
||||
return a.cash |
||||
} |
||||
|
||||
// Equity 账户净值
|
||||
func (a *BacktestTradeAccount) GetEquity() float64 { |
||||
return a.cash |
||||
} |
||||
|
||||
// IsSymbolOpen 指定交易对是否有持仓
|
||||
func (a *BacktestTradeAccount) IsSymbolOpen(symbol string) bool { |
||||
return a.positions[symbol] != nil |
||||
} |
||||
|
||||
func (a *BacktestTradeAccount) GetOpenTradeInsts() (instIds []string) { |
||||
for instId := range a.openTrades { |
||||
instIds = append(instIds, instId) |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (a *BacktestTradeAccount) GetOpenTrades(instId string) []*trade.TradeOrder { |
||||
var trades []*trade.TradeOrder |
||||
for _, tradeId := range a.openTrades[instId] { |
||||
trades = append(trades, a.trades[tradeId]) |
||||
} |
||||
return trades |
||||
} |
||||
|
||||
// GetSymbolOpenPosition 获取指定交易对的仓位信息
|
||||
func (a *BacktestTradeAccount) GetSymbolOpenPosition(symbol string) *trade.Position { |
||||
return a.positions[symbol] |
||||
} |
||||
|
||||
// MarketOrder 市价下单
|
||||
func (a *BacktestTradeAccount) MarketOrder(ticket trade.TradeTicket) (order *trade.TradeOrder, err error) { |
||||
instId := ticket.InstId |
||||
if pos, ok := a.positions[instId]; ok { |
||||
// 检查反方向单
|
||||
if pos.Side != ticket.Side { |
||||
err = fmt.Errorf("cannot open opposite side position") |
||||
return |
||||
} |
||||
// 同方向杠杆倍数
|
||||
if pos.Leverage != ticket.Leverage { |
||||
err = fmt.Errorf("cannot change leverage on existing position") |
||||
return |
||||
} |
||||
} |
||||
|
||||
cost, order := a.trader.ExecuteMarket(a.traderOrderId, instId, ticket) |
||||
if cost > a.cash { |
||||
err = fmt.Errorf("insufficient cash") |
||||
return |
||||
} |
||||
a.traderOrderId++ |
||||
a.cash -= cost |
||||
a.trades[order.TradeId] = order |
||||
a.openTrades[instId] = append(a.openTrades[instId], order.TradeId) |
||||
|
||||
// open/add position
|
||||
pos, ok := a.positions[instId] |
||||
if !ok { |
||||
pos = &trade.Position{ |
||||
InstId: order.InstId, |
||||
Side: order.Side, |
||||
Qty: order.Qty, |
||||
Leverage: order.Leverage, |
||||
EntryPx: order.Price, |
||||
EntryTs: order.Ctime, |
||||
PeakPx: order.Price, |
||||
} |
||||
a.positions[instId] = pos |
||||
} else { |
||||
totalQty := pos.Qty + order.Qty |
||||
pos.EntryPx = (pos.EntryPx*pos.Qty + order.Price*order.Qty) / totalQty |
||||
pos.Qty = totalQty |
||||
if pos.Side == types.SideLong && order.Price < pos.PeakPx { |
||||
pos.PeakPx = order.Price |
||||
} |
||||
if pos.Side == types.SideShort && order.Price > pos.PeakPx { |
||||
pos.PeakPx = order.Price |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// CloseTradeOrder 订单订单平仓
|
||||
func (a *BacktestTradeAccount) CloseTradeOrder(ticket trade.TradeTicket) (err error) { |
||||
instId := ticket.InstId |
||||
// 检查仓位
|
||||
pos, ok := a.positions[instId] |
||||
if !ok { |
||||
err = fmt.Errorf("no open position for symbol: %s", instId) |
||||
return |
||||
} |
||||
if pos.Side == ticket.Side { |
||||
err = fmt.Errorf("cannot close position with same side trade") |
||||
return |
||||
} |
||||
if ticket.Qty > pos.Qty { |
||||
err = fmt.Errorf("close quantity exceeds position quantity") |
||||
return |
||||
} |
||||
|
||||
cost, order := a.trader.ExecuteMarket(a.traderOrderId, instId, ticket) |
||||
a.traderOrderId++ |
||||
a.cash += cost |
||||
a.trades[order.TradeId] = order |
||||
a.closeTrades = append(a.closeTrades, order.TradeId) |
||||
if len(ticket.TradesId) > 0 { |
||||
if trades, ok := a.openTrades[instId]; ok { |
||||
// remove open trade order
|
||||
removes := collect.Remove(&trades, func(tradeId int64) bool { |
||||
return collect.In(tradeId, ticket.TradesId...) |
||||
}) |
||||
a.openTrades[instId] = trades |
||||
if removes != len(ticket.TradesId) { |
||||
zlog.Warningf("close ticket tradeId not exists: instId=%s, tradeId=%#v", ticket.InstId, ticket.TradesId) |
||||
} |
||||
} |
||||
if len(a.openTrades[instId]) == 0 { |
||||
delete(a.openTrades, instId) |
||||
} |
||||
} |
||||
|
||||
// update position
|
||||
pos.Qty -= ticket.Qty |
||||
if pos.Qty <= 0 { |
||||
delete(a.positions, instId) |
||||
if _, ok := a.openTrades[instId]; ok { |
||||
zlog.Warningf("close all position trades: %s", instId) |
||||
} |
||||
} |
||||
|
||||
// todo 平仓单统计
|
||||
|
||||
return |
||||
} |
||||
|
||||
// CloseOpsition 仓位平仓
|
||||
func (a *BacktestTradeAccount) CloseOpsition(pos *trade.Position) (err error) { |
||||
|
||||
return |
||||
} |
||||
@ -1,134 +0,0 @@
|
||||
package trade |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sig-pub/pkg/types" |
||||
"sig-pub/pkg/types/decimals" |
||||
) |
||||
|
||||
// Exit 止盈止损策略(trading service 管理)
|
||||
type ICloseStrategy interface { |
||||
OnKline(k types.Kline, pos *Position) (closePos bool, cause Cause) |
||||
OnPrice(price float64, pos *Position) (closePos bool, cause Cause) |
||||
OnSigStrategySingal(sigSide types.Side, pos *Position) (closePos bool, cause Cause) |
||||
} |
||||
|
||||
// 平仓策略参数
|
||||
type CloseStrategyParam struct { |
||||
StopLossPct float64 `json:"stopLossPct"` // 固定止损 static stoploss
|
||||
TakeProfitPct float64 `json:"takeProfitPct"` // 固定止盈 static take profit
|
||||
ProfitRetracePcts [][]float64 `json:"profitRetracePcts"` // 基于最高利润回撤触发平仓 (例如 [[0.01, 0.3], [0.02, 0.2]] 最高利润超过1%时30%回撤则触发平仓,最高利润超过2%时20%回撤就触发平仓)
|
||||
CloseOnSideReverse bool `json:"closeOnSideReverse"` // 交易信号和持单方向相反时是否进行平仓
|
||||
Fee bool `json:"fee"` // 计算止盈止损时是否包含手续费
|
||||
} |
||||
|
||||
// CloseStrategy 平仓策略
|
||||
type CloseStrategy struct { |
||||
CloseStrategyParam |
||||
} |
||||
|
||||
func NewCloseStrategy(param CloseStrategyParam) (cs *CloseStrategy, err error) { |
||||
if param.StopLossPct < 0 { |
||||
err = fmt.Errorf("stopLossPct can't less zero") |
||||
return |
||||
} |
||||
cs = &CloseStrategy{CloseStrategyParam: param} |
||||
return |
||||
} |
||||
|
||||
// Update 当k线更新判断是否关闭仓位
|
||||
func (s *CloseStrategy) OnKline(k types.Kline, pos *Position) (closePos bool, cause Cause) { |
||||
closePrice := decimals.MustToFloat64(k.Close) |
||||
return s.OnPrice(closePrice, pos) |
||||
} |
||||
|
||||
// OnPrice 当k线更新判断是否关闭仓位
|
||||
func (s *CloseStrategy) OnPrice(price float64, pos *Position) (closePos bool, cause Cause) { |
||||
if !pos.Side.IsValid() { |
||||
return |
||||
} |
||||
// update peak px
|
||||
if pos.Side == types.SideLong && (price > pos.PeakPx) { |
||||
pos.PeakPx = price |
||||
} |
||||
if pos.Side == types.SideShort && (price < pos.PeakPx) { |
||||
pos.PeakPx = price |
||||
} |
||||
|
||||
entry := pos.EntryPx |
||||
// side long:
|
||||
if pos.Side == types.SideLong { |
||||
// 固定止损
|
||||
if s.StopLossPct > 0 && price <= entry*(1-s.StopLossPct) { |
||||
return true, CauseCloseStoploss |
||||
} |
||||
// 固定止盈
|
||||
if s.TakeProfitPct > 0 && price >= entry*(1+s.TakeProfitPct) { |
||||
return true, CauseCloseTakeprofit |
||||
} |
||||
// 基于最高利润动态止盈
|
||||
if len(s.ProfitRetracePcts) > 0 { |
||||
// peak profit fraction
|
||||
peakProfit := (pos.PeakPx - entry) / entry |
||||
minProfitToTrail, trailingPct := float64(0), float64(0) |
||||
for _, profit := range s.ProfitRetracePcts { |
||||
if len(profit) != 2 { |
||||
continue |
||||
} |
||||
_minProfitToTrail := profit[0] // 启动最高利润回撤的最小盈利阈值
|
||||
_trailingPct := profit[1] // 基于最高利润回撤触发平仓
|
||||
if peakProfit >= _minProfitToTrail && _minProfitToTrail > minProfitToTrail { |
||||
minProfitToTrail = _minProfitToTrail |
||||
trailingPct = _trailingPct |
||||
} |
||||
} |
||||
if minProfitToTrail > 0 && trailingPct > 0 { |
||||
trail := entry + (pos.PeakPx-entry)*(1-trailingPct) |
||||
if price <= trail { |
||||
return true, CauseCloseTrailing |
||||
} |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// side short:
|
||||
if s.StopLossPct > 0 && price >= pos.EntryPx*(1+s.StopLossPct) { |
||||
return true, CauseCloseStoploss |
||||
} |
||||
if s.TakeProfitPct > 0 && price <= pos.EntryPx*(1-s.TakeProfitPct) { |
||||
return true, CauseCloseTakeprofit |
||||
} |
||||
// 基于最高利润动态止盈
|
||||
if len(s.ProfitRetracePcts) > 0 { |
||||
// peak profit fraction
|
||||
peakProfit := (entry - pos.PeakPx) / entry |
||||
minProfitToTrail, trailingPct := float64(0), float64(0) |
||||
for _, profit := range s.ProfitRetracePcts { |
||||
if len(profit) != 2 { |
||||
continue |
||||
} |
||||
_minProfitToTrail := profit[0] // 启动最高利润回撤的最小盈利阈值
|
||||
_trailingPct := profit[1] // 基于最高利润回撤触发平仓
|
||||
if peakProfit >= _minProfitToTrail && _minProfitToTrail >= minProfitToTrail { |
||||
minProfitToTrail = _minProfitToTrail |
||||
trailingPct = _trailingPct |
||||
} |
||||
} |
||||
if minProfitToTrail > 0 && trailingPct > 0 { |
||||
trail := entry - (entry-pos.PeakPx)*(1+trailingPct) |
||||
if price >= trail { |
||||
return true, CauseCloseTrailing |
||||
} |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// OnSigStrategySingal 根据策略信号尝试平掉相反方向的仓位。例如策略返回 SELL 时,平掉 BUY 持仓
|
||||
func (s *CloseStrategy) OnSigStrategySingal(sigSide types.Side, pos *Position) (closePos bool, cause Cause) { |
||||
if !s.CloseOnSideReverse { |
||||
return |
||||
} |
||||
return sigSide != pos.Side, CauseCloseReverseSingal |
||||
} |
||||
@ -1,42 +0,0 @@
|
||||
package trade |
||||
|
||||
import ( |
||||
"sig-pub/pkg/types" |
||||
) |
||||
|
||||
type IRickStrategy interface { |
||||
OnSignal(signalSide types.Side) (ok bool, cause Cause) |
||||
} |
||||
|
||||
type RiskStrategyParam struct { |
||||
SkipOnSideOpposite bool // 当前持有反方向单时
|
||||
SkipOnSideSame bool // 当前持有相同方向单时
|
||||
|
||||
} |
||||
|
||||
// RiskStrategy 风险管理策略
|
||||
// Kelly准则优化方法
|
||||
type RiskStrategy struct { |
||||
RiskStrategyParam |
||||
} |
||||
|
||||
func NewRiskStrategy(param RiskStrategyParam) (rs *RiskStrategy, err error) { |
||||
rs = &RiskStrategy{ |
||||
RiskStrategyParam: param, |
||||
} |
||||
return |
||||
} |
||||
|
||||
// SideAssess 收到信号时进行评估, 返回过滤后的交易信号
|
||||
// 对交易方向进行信心分数评估, 后续开仓仓位
|
||||
// 1.当前持有反方向单时, 不进行开仓
|
||||
// 2.当前持有同方向单时, 根据信心分数评估是否加仓
|
||||
func (s *RiskStrategy) SigRiskAnalyze(account ITradeAccount, signalSide types.Side) (doTrade bool, causes []Cause, err error) { |
||||
|
||||
return true, nil, nil |
||||
} |
||||
|
||||
func (s *RiskStrategy) SigRiskAnalyze1(account string, signalSide types.Side) (doTrade bool, causes []Cause, err error) { |
||||
|
||||
return true, nil, nil |
||||
} |
||||
@ -0,0 +1,146 @@
|
||||
package trade |
||||
|
||||
import ( |
||||
"sig-pub/pkg/strategy" |
||||
"sig-pub/pkg/types" |
||||
"sig-pub/pkg/types/decimals" |
||||
"time" |
||||
) |
||||
|
||||
// CloseAssess 价格更新评估是否平仓
|
||||
// @return closeTicket平仓单信息
|
||||
func (s *SigTradeStrategy) CloseAssessOnPrice(ctx strategy.IInstanceIntervalSigStrategyContext, account ITradeAccount, instId string, price float64) (closeTickets []TradeTicket, err error) { |
||||
openTrades := account.GetOpenTrades() |
||||
if len(openTrades) == 0 { |
||||
return |
||||
} |
||||
for _, trd := range openTrades { |
||||
k := ctx.Get(trd.InstId, PriceDriverInterval, 0) |
||||
price := decimals.MustToFloat64(k.Close) |
||||
closeTrade, cause := s.closeTradeOnPrice(price, trd) |
||||
if closeTrade { |
||||
closeTickets = append(closeTickets, TradeTicket{ |
||||
InstId: trd.InstId, |
||||
Side: trd.Side.Opposite(), |
||||
Price: price, |
||||
Leverage: trd.Leverage, |
||||
Qty: trd.Qty, |
||||
Interval: string(k.Interval), |
||||
Ktime: k.Interval.MustAddMul(k.Ts, 1), |
||||
Ctime: time.Now().UnixMilli(), |
||||
Cause: cause, |
||||
TradesId: []int64{trd.TradeId}, |
||||
}) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (s *SigTradeStrategy) closeTradeOnPrice(price float64, pos *TradeOrder) (closeTrade bool, cause Cause) { |
||||
if !pos.Side.IsValid() { |
||||
return |
||||
} |
||||
// update peak px
|
||||
if pos.Side == types.SideLong && (price > pos.PeakPx) { |
||||
pos.PeakPx = price |
||||
} |
||||
if pos.Side == types.SideShort && (price < pos.PeakPx) { |
||||
pos.PeakPx = price |
||||
} |
||||
|
||||
entry := pos.EntryPx |
||||
// side long:
|
||||
if pos.Side == types.SideLong { |
||||
// 固定止损
|
||||
if s.closeParam.StopLossPct > 0 && price <= entry*(1-s.closeParam.StopLossPct) { |
||||
return true, CauseCloseStoploss |
||||
} |
||||
// 固定止盈
|
||||
if s.closeParam.TakeProfitPct > 0 && price >= entry*(1+s.closeParam.TakeProfitPct) { |
||||
return true, CauseCloseTakeprofit |
||||
} |
||||
// 基于最高利润动态止盈
|
||||
if len(s.closeParam.ProfitRetracePcts) > 0 { |
||||
// peak profit fraction
|
||||
peakProfit := (pos.PeakPx - entry) / entry |
||||
minProfitToTrail, trailingPct := float64(0), float64(0) |
||||
for _, profit := range s.closeParam.ProfitRetracePcts { |
||||
if len(profit) != 2 { |
||||
continue |
||||
} |
||||
_minProfitToTrail := profit[0] // 启动最高利润回撤的最小盈利阈值
|
||||
_trailingPct := profit[1] // 基于最高利润回撤触发平仓
|
||||
if peakProfit >= _minProfitToTrail && _minProfitToTrail > minProfitToTrail { |
||||
minProfitToTrail = _minProfitToTrail |
||||
trailingPct = _trailingPct |
||||
} |
||||
} |
||||
if minProfitToTrail > 0 && trailingPct > 0 { |
||||
trail := entry + (pos.PeakPx-entry)*(1-trailingPct) |
||||
if price <= trail { |
||||
return true, CauseCloseTrailing |
||||
} |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// side short:
|
||||
if s.closeParam.StopLossPct > 0 && price >= pos.EntryPx*(1+s.closeParam.StopLossPct) { |
||||
return true, CauseCloseStoploss |
||||
} |
||||
if s.closeParam.TakeProfitPct > 0 && price <= pos.EntryPx*(1-s.closeParam.TakeProfitPct) { |
||||
return true, CauseCloseTakeprofit |
||||
} |
||||
// 基于最高利润动态止盈
|
||||
if len(s.closeParam.ProfitRetracePcts) > 0 { |
||||
// peak profit fraction
|
||||
peakProfit := (entry - pos.PeakPx) / entry |
||||
minProfitToTrail, trailingPct := float64(0), float64(0) |
||||
for _, profit := range s.closeParam.ProfitRetracePcts { |
||||
if len(profit) != 2 { |
||||
continue |
||||
} |
||||
_minProfitToTrail := profit[0] // 启动最高利润回撤的最小盈利阈值
|
||||
_trailingPct := profit[1] // 基于最高利润回撤触发平仓
|
||||
if peakProfit >= _minProfitToTrail && _minProfitToTrail >= minProfitToTrail { |
||||
minProfitToTrail = _minProfitToTrail |
||||
trailingPct = _trailingPct |
||||
} |
||||
} |
||||
if minProfitToTrail > 0 && trailingPct > 0 { |
||||
trail := entry - (entry-pos.PeakPx)*(1+trailingPct) |
||||
if price >= trail { |
||||
return true, CauseCloseTrailing |
||||
} |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// CloseAssessOnSig 信号触发时评估是否平仓
|
||||
func (s *SigTradeStrategy) CloseAssessOnSig(ctx strategy.IInstanceIntervalSigStrategyContext, account ITradeAccount, instId string, sigSide types.Side) (closeTickets []TradeTicket, err error) { |
||||
if !s.closeParam.CloseOnSideReverse { |
||||
return |
||||
} |
||||
oppositeSide := sigSide.Opposite() |
||||
for _, trade := range account.GetOpenTrades() { |
||||
// 关闭反方向单
|
||||
if trade.Side == oppositeSide { |
||||
k := ctx.Get(trade.InstId, PriceDriverInterval, 0) |
||||
price := decimals.MustToFloat64(k.Close) |
||||
closeTickets = append(closeTickets, TradeTicket{ |
||||
TradesId: []int64{trade.TradeId}, |
||||
Side: trade.Side.Opposite(), |
||||
Price: price, |
||||
Leverage: trade.Leverage, |
||||
Qty: trade.Qty, |
||||
Interval: string(k.Interval), |
||||
Ktime: k.Interval.MustAddMul(k.Ts, 1), |
||||
Ctime: time.Now().UnixMilli(), |
||||
Cause: CauseCloseReverseSingal, |
||||
}) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
@ -0,0 +1,88 @@
|
||||
package trade |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sig-pub/pkg/strategy" |
||||
"sig-pub/pkg/types" |
||||
"sig-pub/pkg/types/decimals" |
||||
"time" |
||||
) |
||||
|
||||
const ( |
||||
PriceDriverInterval = types.Interval1m // 价格更新使用1分钟k线
|
||||
) |
||||
|
||||
type SigTradeStrategy struct { |
||||
closeParam *CloseStrategyInput |
||||
} |
||||
|
||||
func NewSigTradeStrategy() *SigTradeStrategy { |
||||
return &SigTradeStrategy{} |
||||
} |
||||
|
||||
func (s *SigTradeStrategy) New() strategy.ISigStrategy { |
||||
return &SigTradeStrategy{} |
||||
} |
||||
|
||||
func (s *SigTradeStrategy) Meta() strategy.StrategyMeta { |
||||
return strategy.StrategyMeta{ |
||||
Name: "SigTradeStrategy", |
||||
Desc: "默认交易策略", |
||||
Input: []types.InputArg{ |
||||
// 交易参数
|
||||
{Name: "maxPosPct", Type: types.InputTypeUFloat, Desc: "单笔交易最大仓位占比"}, |
||||
{Name: "maxExposurePct", Type: types.InputTypeUFloat, Desc: "最大总敞口占比"}, |
||||
{Name: "maxLots", Type: types.InputTypeUInt, Desc: "最大手数/数量"}, |
||||
// 平仓止损参数
|
||||
{Name: "stopLossPct", Type: types.InputTypeUFloat, Desc: "固定止损比例"}, |
||||
{Name: "takeProfitPct", Type: types.InputTypeUFloat, Desc: "固定止盈比例"}, |
||||
{Name: "profitRetracePcts", Type: types.InputTypeUFloats2D, Desc: "基于最高利润回撤触发平仓 (例如 [[0.01, 0.3], [0.02, 0.2]] 最高利润超过1%时30%回撤则触发平仓, 最高利润超过2%时20%回撤就触发平仓)"}, |
||||
{Name: "closeOnSideReverse", Type: types.InputTypeBool, Desc: "交易信号和持单方向相反时是否进行平仓"}, |
||||
{Name: "fee", Type: types.InputTypeBool, Desc: "计算止盈止损时是否包含手续费"}, |
||||
// 风险评估参数...
|
||||
}, |
||||
} |
||||
} |
||||
|
||||
// 校验参数, 并根据参数初始化策略
|
||||
func (s *SigTradeStrategy) Init(input types.Input) (err error) { |
||||
s.closeParam = new(CloseStrategyInput) |
||||
input.DecodeInput(s.closeParam) |
||||
if s.closeParam.StopLossPct < 0 { |
||||
err = fmt.Errorf("stopLossPct can't less zero") |
||||
return |
||||
} |
||||
return |
||||
} |
||||
|
||||
// 需要的各周期最小数据k线数
|
||||
func (s *SigTradeStrategy) CandlePeriods(ctx strategy.IInstanceIntervalSigStrategyContext) (tradeInsts []string, iPeriods *types.IntervalState[int16]) { |
||||
iPeriods = types.NewIntervalState[int16]() |
||||
iPeriods.Set(PriceDriverInterval, 1) |
||||
return |
||||
} |
||||
|
||||
// RishAssess 信号风险评估, 是否进行交易
|
||||
func (s *SigTradeStrategy) RishAssess(ctx strategy.IInstanceIntervalSigStrategyContext, account ITradeAccount, sigInstId string, sigSide types.Side) (doTrade bool, cause Cause, err error) { |
||||
|
||||
return true, 0, nil |
||||
} |
||||
|
||||
// TradeAssess 生成下单参数(交易量/方向/杠杆)
|
||||
// 控制滑点, 仓位管理
|
||||
// 持仓中币种不能改变杠杆
|
||||
func (s *SigTradeStrategy) TradeAssess(ctx strategy.IInstanceIntervalSigStrategyContext, account ITradeAccount, sigInstId string, sigSide types.Side) (ta TradeTicket, err error) { |
||||
k := ctx.Get(sigInstId, PriceDriverInterval, 0) |
||||
price := decimals.MustToFloat64(k.Close) |
||||
ta = TradeTicket{ |
||||
InstId: sigInstId, |
||||
Side: sigSide, |
||||
Price: price, |
||||
Leverage: 1, |
||||
Qty: 0.02, |
||||
Interval: string(k.Interval), |
||||
Ktime: k.Interval.MustAddMul(k.Ts, 1), |
||||
Ctime: time.Now().UnixMilli(), |
||||
} |
||||
return |
||||
} |
||||
@ -1,65 +1,58 @@
|
||||
package trade |
||||
|
||||
import ( |
||||
"sig-pub/pkg/strategy" |
||||
"sig-pub/pkg/types" |
||||
"sig-pub/pkg/types/decimals" |
||||
|
||||
"github.com/govalues/decimal" |
||||
) |
||||
|
||||
// IRiskStrategy 风险控制接口
|
||||
type IRiskStrategy interface { |
||||
strategy.ISigStrategy |
||||
|
||||
// 需要的各周期最小数据k线数
|
||||
CandlePeriods(ctx strategy.IInstanceIntervalSigStrategyContext) (tradeInsts []string, iPeriods *types.IntervalState[int16]) |
||||
|
||||
// RishAssess 信号风险评估, 是否进行交易
|
||||
RishAssess(ctx strategy.IInstanceIntervalSigStrategyContext, account ITradeAccount, sigInstId string, sigSide types.Side) (ok bool, cause Cause, err error) |
||||
} |
||||
|
||||
// ITradeStrategy 下单策略
|
||||
// 根据购买信号和账户信息生成下单参数
|
||||
// TradeStrategy 下单买入策略接口(控制滑点, 仓位管理)
|
||||
type ITradeStrategy interface { |
||||
// 下单, 币种,方向,杠杆
|
||||
Trade(arg ...string) |
||||
strategy.ISigStrategy |
||||
|
||||
// 市场价格更新
|
||||
Update(ctx ITradeStrategyContext, account ITradeAccount) |
||||
} |
||||
// 需要的各周期最小数据k线数
|
||||
CandlePeriods(ctx strategy.IInstanceIntervalSigStrategyContext) (tradeInsts []string, iPeriods *types.IntervalState[int16]) |
||||
|
||||
type ITradeStrategyContext interface { |
||||
// 最新价格
|
||||
LastPrice() decimal.Decimal |
||||
// TradeAssess 生成下单参数(交易量/方向/杠杆)
|
||||
// 控制滑点, 仓位管理
|
||||
// 持仓中币种不能改变杠杆
|
||||
TradeAssess(ctx strategy.IInstanceIntervalSigStrategyContext, account ITradeAccount, sigInstId string, sigSide types.Side) (ta TradeTicket, err error) |
||||
} |
||||
|
||||
type TradeStrategyParam struct { |
||||
MaxPosPct float64 // 单笔交易最大仓位占比
|
||||
MaxExposurePct float64 // 最大总敞口占比
|
||||
MaxLots float64 // 最大手数/数量 (optional)
|
||||
} |
||||
// Exit 止盈止损策略(trading service 管理)
|
||||
type ICloseStrategy interface { |
||||
strategy.ISigStrategy |
||||
|
||||
type TradeStrategy struct { |
||||
param TradeStrategyParam |
||||
} |
||||
// 需要的各周期最小数据k线数
|
||||
CandlePeriods(ctx strategy.IInstanceIntervalSigStrategyContext) (tradeInsts []string, iPeriods *types.IntervalState[int16]) |
||||
|
||||
func NewTradeStrategy(param TradeStrategyParam) (*TradeStrategy, error) { |
||||
return &TradeStrategy{ |
||||
param: param, |
||||
}, nil |
||||
} |
||||
// CloseAssess 价格更新评估是否平仓
|
||||
// @return closeTicket平仓单信息
|
||||
CloseAssessOnPrice(ctx strategy.IInstanceIntervalSigStrategyContext, account ITradeAccount, instId string, price float64) (closeTickets []TradeTicket, err error) |
||||
|
||||
func (s *TradeStrategy) SigTrade(side types.Side, k types.Kline) (ta TradeArg, err error) { |
||||
price := decimals.MustToFloat64(k.Close) |
||||
time := k.Interval.MustAddMul(k.Ts, 1) |
||||
ta = TradeArg{ |
||||
Side: side, |
||||
Price: price, |
||||
Leverage: 1, |
||||
Qty: 0.02, |
||||
KInterval: string(k.Interval), |
||||
KTime: k.Ts, |
||||
Time: time, |
||||
} |
||||
return |
||||
// CloseAssessOnSig 信号触发时评估是否平仓
|
||||
CloseAssessOnSig(ctx strategy.IInstanceIntervalSigStrategyContext, account ITradeAccount, instId string, sigSide types.Side) (closeTickets []TradeTicket, err error) |
||||
} |
||||
|
||||
type TradeArg struct { |
||||
type TradeTicket struct { |
||||
InstId string |
||||
Side types.Side // 开仓方向
|
||||
Price float64 // 开仓价格
|
||||
Leverage int32 // 杠杆倍数
|
||||
Qty float64 // 交易量 qty为基础货币数量
|
||||
KInterval string // 交易k线周期
|
||||
KTime int64 // 交易k线时间
|
||||
Time int64 // 交易时间
|
||||
Qty float64 // 交易量 qty为交易产品数量
|
||||
Interval string // k线周期
|
||||
Ktime int64 // k线时间
|
||||
Ctime int64 // 创建时间(ctime-ktime=信号延迟)
|
||||
Cause Cause |
||||
TradesId []int64 // 关联交易订单id (仅平仓使用)
|
||||
} |
||||
|
||||
@ -0,0 +1,39 @@
|
||||
package types |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/bytedance/sonic" |
||||
) |
||||
|
||||
func TestInput(t *testing.T) { |
||||
m := `{"stopLossPct": 0.01, "takeProfitPct": 0.2, "profitRetracePcts": [[0.005,0.5],[0.01,0.4],[0.02,0.3],[0.03,0.2]], "closeOnSideReverse": true}` |
||||
var in Input |
||||
err := sonic.UnmarshalString(m, &in) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
var floats [][]float64 |
||||
in.Decode("profitRetracePcts", &floats) |
||||
t.Log(floats) |
||||
|
||||
var f float64 |
||||
in.Decode("takeProfitPct", &f) |
||||
t.Log(f) |
||||
|
||||
var b bool |
||||
in.Decode("closeOnSideReverse", &b) |
||||
t.Log(b) |
||||
|
||||
type closeStrategyParam struct { |
||||
StopLossPct float64 |
||||
TakeProfitPct float64 |
||||
ProfitRetracePcts [][]float64 |
||||
CloseOnSideReverse bool |
||||
Fee bool |
||||
} |
||||
csp := &closeStrategyParam{} |
||||
in.DecodeInput(csp) |
||||
t.Logf("%#v", csp) |
||||
} |
||||
@ -0,0 +1,387 @@
|
||||
package expression |
||||
|
||||
import ( |
||||
"errors" |
||||
"strconv" |
||||
"strings" |
||||
"unicode" |
||||
) |
||||
|
||||
// Deprecated: 待优化, 简单表达式解析与求值器,支持变量、比较运算符和逻辑运算符
|
||||
type Parser struct { |
||||
expr string |
||||
pos int |
||||
} |
||||
|
||||
type Node interface { |
||||
Eval(vars map[string]interface{}) (bool, error) |
||||
} |
||||
|
||||
type numberNode struct{ val float64 } |
||||
type boolNode struct{ val bool } |
||||
type varNode struct{ name string } |
||||
type binaryNode struct { |
||||
op string |
||||
left, right Node |
||||
} |
||||
type compareNode struct { |
||||
op string |
||||
left, right Node |
||||
} |
||||
type logicNode struct { |
||||
op string |
||||
left, right Node |
||||
} |
||||
type unaryNode struct { |
||||
op string |
||||
operand Node |
||||
} |
||||
|
||||
// NewParser 创建解析器
|
||||
func Parse(expr string) (*Parser, error) { |
||||
expr = strings.ReplaceAll(expr, " ", "") // 去除空格
|
||||
if expr == "" { |
||||
return nil, errors.New("表达式为空") |
||||
} |
||||
return &Parser{expr: expr}, nil |
||||
} |
||||
|
||||
// 解析并返回 AST 根节点
|
||||
func (p *Parser) Parse() (Node, error) { |
||||
node, err := p.parseLogicOr() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if p.pos != len(p.expr) { |
||||
return nil, errors.New("表达式末尾有多余字符") |
||||
} |
||||
return node, nil |
||||
} |
||||
|
||||
// ====================== 解析层级 ======================
|
||||
|
||||
func (p *Parser) parseLogicOr() (Node, error) { |
||||
left, err := p.parseLogicAnd() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for p.pos < len(p.expr) && p.substr(p.pos, 2) == "||" { |
||||
p.pos += 2 |
||||
right, err := p.parseLogicAnd() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
left = &logicNode{op: "||", left: left, right: right} |
||||
} |
||||
return left, nil |
||||
} |
||||
|
||||
func (p *Parser) parseLogicAnd() (Node, error) { |
||||
left, err := p.parseComparison() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for p.pos < len(p.expr) && p.substr(p.pos, 2) == "&&" { |
||||
p.pos += 2 |
||||
right, err := p.parseComparison() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
left = &logicNode{op: "&&", left: left, right: right} |
||||
} |
||||
return left, nil |
||||
} |
||||
|
||||
func (p *Parser) parseComparison() (Node, error) { |
||||
left, err := p.parseExpression() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
ops := []string{">=", "<=", "==", "!=", ">", "<"} |
||||
var op string |
||||
for _, candidate := range ops { |
||||
if p.substr(p.pos, len(candidate)) == candidate { |
||||
op = candidate |
||||
p.pos += len(candidate) |
||||
break |
||||
} |
||||
} |
||||
if op != "" { |
||||
right, err := p.parseExpression() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &compareNode{op: op, left: left, right: right}, nil |
||||
} |
||||
return left, nil |
||||
} |
||||
|
||||
func (p *Parser) parseExpression() (Node, error) { |
||||
left, err := p.parseTerm() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for p.pos < len(p.expr) && (p.current() == '+' || p.current() == '-') { |
||||
op := string(p.current()) |
||||
p.pos++ |
||||
right, err := p.parseTerm() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
left = &binaryNode{op: op, left: left, right: right} |
||||
} |
||||
return left, nil |
||||
} |
||||
|
||||
func (p *Parser) parseTerm() (Node, error) { |
||||
left, err := p.parseUnary() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for p.pos < len(p.expr) && (p.current() == '*' || p.current() == '/') { |
||||
op := string(p.current()) |
||||
p.pos++ |
||||
right, err := p.parseUnary() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
left = &binaryNode{op: op, left: left, right: right} |
||||
} |
||||
return left, nil |
||||
} |
||||
|
||||
func (p *Parser) parseUnary() (Node, error) { |
||||
if p.current() == '!' { |
||||
p.pos++ |
||||
operand, err := p.parseUnary() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &unaryNode{op: "!", operand: operand}, nil |
||||
} |
||||
return p.parseAtom() |
||||
} |
||||
|
||||
func (p *Parser) parseAtom() (Node, error) { |
||||
ch := p.current() |
||||
|
||||
if ch == '(' { |
||||
p.pos++ |
||||
node, err := p.parseLogicOr() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if p.current() != ')' { |
||||
return nil, errors.New("缺少右括号") |
||||
} |
||||
p.pos++ |
||||
return node, nil |
||||
} |
||||
|
||||
if unicode.IsDigit(rune(ch)) || ch == '.' || ch == '-' && p.pos+1 < len(p.expr) && (unicode.IsDigit(rune(p.expr[p.pos+1])) || p.expr[p.pos+1] == '.') { |
||||
return p.parseNumber() |
||||
} |
||||
|
||||
if unicode.IsLetter(rune(ch)) || ch == '_' { |
||||
return p.parseIdentifierOrBool() |
||||
} |
||||
|
||||
return nil, errors.New("无效字符: " + string(ch)) |
||||
} |
||||
|
||||
func (p *Parser) parseNumber() (Node, error) { |
||||
start := p.pos |
||||
if p.current() == '-' { |
||||
p.pos++ |
||||
} |
||||
for p.pos < len(p.expr) && (unicode.IsDigit(rune(p.expr[p.pos])) || p.expr[p.pos] == '.') { |
||||
p.pos++ |
||||
} |
||||
val, err := strconv.ParseFloat(p.expr[start:p.pos], 64) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &numberNode{val: val}, nil |
||||
} |
||||
|
||||
func (p *Parser) parseIdentifierOrBool() (Node, error) { |
||||
start := p.pos |
||||
for p.pos < len(p.expr) && (unicode.IsLetter(rune(p.expr[p.pos])) || unicode.IsDigit(rune(p.expr[p.pos])) || p.expr[p.pos] == '_') { |
||||
p.pos++ |
||||
} |
||||
name := p.expr[start:p.pos] |
||||
|
||||
if name == "true" { |
||||
return &boolNode{val: true}, nil |
||||
} |
||||
if name == "false" { |
||||
return &boolNode{val: false}, nil |
||||
} |
||||
return &varNode{name: name}, nil |
||||
} |
||||
|
||||
// ====================== 辅助函数 ======================
|
||||
|
||||
func (p *Parser) current() byte { |
||||
if p.pos >= len(p.expr) { |
||||
return 0 |
||||
} |
||||
return p.expr[p.pos] |
||||
} |
||||
|
||||
func (p *Parser) substr(start, length int) string { |
||||
if start+length > len(p.expr) { |
||||
return "" |
||||
} |
||||
return p.expr[start : start+length] |
||||
} |
||||
|
||||
// ====================== 求值 ======================
|
||||
|
||||
func (n *numberNode) Eval(_ map[string]interface{}) (bool, error) { |
||||
return false, errors.New("数字节点不能直接作为布尔表达式") |
||||
} |
||||
|
||||
func (n *boolNode) Eval(_ map[string]interface{}) (bool, error) { |
||||
return n.val, nil |
||||
} |
||||
|
||||
func (n *varNode) Eval(vars map[string]interface{}) (bool, error) { |
||||
val, exists := vars[n.name] |
||||
if !exists { |
||||
return false, errors.New("未定义的变量: " + n.name) |
||||
} |
||||
switch v := val.(type) { |
||||
case float64: |
||||
return v != 0, nil // 数字非零视为 true(可选逻辑)
|
||||
case bool: |
||||
return v, nil |
||||
default: |
||||
return false, errors.New("变量必须是 float64 或 bool") |
||||
} |
||||
} |
||||
|
||||
func (n *binaryNode) Eval(vars map[string]interface{}) (bool, error) { |
||||
_, err := evalToFloat(n.left, vars) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
r, err := evalToFloat(n.right, vars) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
switch n.op { |
||||
case "+": |
||||
return false, errors.New("二元运算不能直接返回 bool") |
||||
case "-": |
||||
return false, errors.New("二元运算不能直接返回 bool") |
||||
case "*": |
||||
return false, errors.New("二元运算不能直接返回 bool") |
||||
case "/": |
||||
if r == 0 { |
||||
return false, errors.New("除以零") |
||||
} |
||||
return false, errors.New("二元运算不能直接返回 bool") |
||||
} |
||||
return false, errors.New("未知运算符") |
||||
} |
||||
|
||||
func (n *compareNode) Eval(vars map[string]interface{}) (bool, error) { |
||||
l, err := evalToFloat(n.left, vars) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
r, err := evalToFloat(n.right, vars) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
switch n.op { |
||||
case ">": |
||||
return l > r, nil |
||||
case "<": |
||||
return l < r, nil |
||||
case "==": |
||||
return l == r, nil |
||||
case ">=": |
||||
return l >= r, nil |
||||
case "<=": |
||||
return l <= r, nil |
||||
case "!=": |
||||
return l != r, nil |
||||
} |
||||
return false, errors.New("未知比较符") |
||||
} |
||||
|
||||
func (n *logicNode) Eval(vars map[string]interface{}) (bool, error) { |
||||
l, err := evalToBool(n.left, vars) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
r, err := evalToBool(n.right, vars) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
if n.op == "&&" { |
||||
return l && r, nil |
||||
} |
||||
return l || r, nil |
||||
} |
||||
|
||||
func (n *unaryNode) Eval(vars map[string]interface{}) (bool, error) { |
||||
val, err := evalToBool(n.operand, vars) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
return !val, nil |
||||
} |
||||
|
||||
// 辅助求值函数
|
||||
func evalToFloat(node Node, vars map[string]interface{}) (float64, error) { |
||||
// 简化实现:这里假设算术表达式最终求值后用于比较
|
||||
// 实际项目中可扩展返回 interface{}
|
||||
switch n := node.(type) { |
||||
case *numberNode: |
||||
return n.val, nil |
||||
case *varNode: |
||||
if v, ok := vars[n.name].(float64); ok { |
||||
return v, nil |
||||
} |
||||
return 0, errors.New("变量不是数字") |
||||
case *binaryNode: |
||||
l, _ := evalToFloat(n.left, vars) |
||||
r, _ := evalToFloat(n.right, vars) |
||||
switch n.op { |
||||
case "+": |
||||
return l + r, nil |
||||
case "-": |
||||
return l - r, nil |
||||
case "*": |
||||
return l * r, nil |
||||
case "/": |
||||
if r == 0 { |
||||
return 0, errors.New("除以零") |
||||
} |
||||
return l / r, nil |
||||
} |
||||
} |
||||
return 0, errors.New("无法求值为数字") |
||||
} |
||||
|
||||
func evalToBool(node Node, vars map[string]interface{}) (bool, error) { |
||||
return node.Eval(vars) |
||||
} |
||||
|
||||
// ====================== 使用示例 ======================
|
||||
|
||||
func Evaluate(expr string, vars map[string]interface{}) (bool, error) { |
||||
p, err := Parse(expr) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
ast, err := p.Parse() |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
return ast.Eval(vars) |
||||
} |
||||
@ -0,0 +1,35 @@
|
||||
package expression |
||||
|
||||
import ( |
||||
"fmt" |
||||
"testing" |
||||
) |
||||
|
||||
func TestExp(t *testing.T) { |
||||
result, err := Evaluate("price <= maxPrice || !isActive || hasPremium", map[string]interface{}{ |
||||
"price": 1200.0, |
||||
"maxPrice": 1000.0, |
||||
"isActive": false, |
||||
"hasPremium": true, |
||||
}) |
||||
if err != nil { |
||||
t.Error(err) |
||||
return |
||||
} |
||||
fmt.Println("r1:", result) |
||||
|
||||
vars := map[string]interface{}{ |
||||
"age": 35.0, |
||||
"bonus": 5000.0, |
||||
"salary": 120000.0, |
||||
"isActive": false, |
||||
"price": 899.0, |
||||
"maxPrice": 1000.0, |
||||
"hasPremium": true, |
||||
} |
||||
result, err = Evaluate("(age + bonus) > salary / 12 && !isActive || price <= maxPrice", vars) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
fmt.Println("r2:", result) |
||||
} |
||||
Loading…
Reference in new issue