diff --git a/README.md b/README.md index bf293c8..a245b75 100644 --- a/README.md +++ b/README.md @@ -134,12 +134,6 @@ RSI[1,2,3,4] -> RSI[0] [词法解析](https://github.com/alecthomas/participle) -```text -plot: [ - {state: "vector", type: Hist, props: {color: red}, exprs: [ - {expr: "vector < 0", props: {color: green}}, - ]}, - {state: "dif", type: Line, props: {color: red},}, - {state: "dea", type: Line, props: {color: red},}, -] -``` +因子挖掘 -> 策略挖掘 + +exchange_service.go: 100 task/一批, 批量成功后mark, 再发布下一波 diff --git a/api/pub.proto b/api/pub.proto index 2fc7cda..bd79a17 100644 --- a/api/pub.proto +++ b/api/pub.proto @@ -18,7 +18,7 @@ enum ExchangeType { enum TradeInstanceType { Unknow = 0; - Spot = 1; // 1现货 + Spot = 1; // 1现货 PerpetualContract = 2; // 2永续合约 } @@ -115,9 +115,9 @@ message Kline { double high = 5; double low = 6; double close = 7; - double vol = 8; // 交易量 + double vol = 8; // 交易量 double volQuote = 9; // 交易额 - bool confirm = 10; // k线是否完结 + bool confirm = 10; // k线是否完结 } // https://maicoin.github.io/max-websocket-docs/#/private_channels?id=snapshot @@ -146,7 +146,7 @@ message SeriesRange { uint32 count = 6; // k线条数,before或after其中一个为0时有效 bool open = 7; // 是否开区间, before/after不为0时不包含 bool live = 8; // 实时k线, before和after为0时是否追加实时k线 - bool desc = 9; // 是否降序, 默认升序 + bool desc = 9; // 是否降序, 默认升序 uint32 windowExtra = 10; // 需要额外拉取更早的k线条数 uint32 limit = 11; // 大于0时检查, 数据长度超过limit则返回错误 @@ -159,18 +159,18 @@ message Paging { bool asc = 3; // 升序排序 } +// 指标绘图 message IndicatorPlot { string indicator = 1; - IndicatorPlotSeries series = 2; - repeated IndicatorPlotSeries stateSeries = 3; + repeated IndicatorPlotSeries plots = 9; } message IndicatorPlotSeries { string state = 1; int32 type = 2; google.protobuf.Struct props = 3; - repeated IndicatorStateEnumProps stateEnumProps = 4; + repeated IndicatorPlotExp exps = 4; } -message IndicatorStateEnumProps { - string state = 1; - map enumProps = 2; +message IndicatorPlotExp { + string exp = 1; + google.protobuf.Struct props = 3; } diff --git a/internal/trading/backtest/account.go b/internal/trading/backtest/account.go index 18b4aaa..1a7227b 100644 --- a/internal/trading/backtest/account.go +++ b/internal/trading/backtest/account.go @@ -1,15 +1,10 @@ package backtest import ( - "fmt" "math" "sig-pub/pkg/trade" "sig-pub/pkg/types" - "sig-pub/pkg/types/decimals" - "sig-pub/pkg/utils/conver" - "sig-pub/pkg/utils/lang" "sort" - "time" ) type BacktestAccount struct { @@ -191,7 +186,7 @@ func (a *BacktestAccount) CurrentExposure() float64 { // } // 获取未平仓交易单 -func (a *BacktestAccount) OpenPositions() map[int64]*trade.Position { +func (a *BacktestAccount) GetOpenTrades() map[int64]*trade.Position { return a.positions } @@ -201,86 +196,86 @@ func (a *BacktestAccount) CountOpenPositions() int { } // 订单下单 -func (a *BacktestAccount) TradeOrder(ta trade.TradeArg) (ok bool, cause trade.Cause, err error) { - order, ok := a.simulator.ExecuteMarket(ta.Side, ta.Qty, ta.Price, ta.Time) - if !ok { - return - } +func (a *BacktestAccount) TradeOrder(ta trade.TradeTicket) (ok bool, cause trade.Cause, err error) { + // order, ok := a.simulator.ExecuteMarket(ta.Side, ta.Qty, ta.Price, ta.Time) + // if !ok { + // return + // } - // apply cash/position, short side also need earnest money - cost := order.Price*order.Qty + order.Fee - if cost > a.cash { - ok = false - return - } - a.cash -= cost - // open/add position - pos := &trade.Position{TradeId: order.Id, Side: order.Side, Qty: order.Qty, EntryPx: order.Price, EntryTs: order.Time, PeakPx: order.Price, Fee: order.Fee} - a.positions[order.Id] = pos - a.trades[order.Id] = order - ok = true + // // apply cash/position, short side also need earnest money + // cost := order.Price*order.Qty + order.Fee + // if cost > a.cash { + // ok = false + // return + // } + // a.cash -= cost + // // open/add position + // pos := &trade.Position{TradeId: order.Id, Side: order.Side, Qty: order.Qty, EntryPx: order.Price, EntryTs: order.Time, PeakPx: order.Price, Fee: order.Fee} + // a.positions[order.Id] = pos + // a.trades[order.Id] = order + // ok = true return } // 将仓位进行平仓 func (a *BacktestAccount) ClosePosition(pos *trade.Position, kline types.Kline, cause trade.Cause) (err error) { - closeSide := lang.Ternary(pos.Side == types.SideLong, types.SideShort, types.SideLong) + // closeSide := lang.Ternary(pos.Side == types.SideLong, types.SideShort, types.SideLong) - closePrice := decimals.MustToFloat64(kline.Close) - t, ok := a.simulator.ExecuteMarket(closeSide, pos.Qty, closePrice, kline.Ts) - if !ok { - err = fmt.Errorf("close position error: %#v", pos) - return - } - t.CloseCause = cause + // closePrice := decimals.MustToFloat64(kline.Close) + // t, ok := a.simulator.ExecuteMarket(closeSide, pos.Qty, closePrice, kline.Ts) + // if !ok { + // err = fmt.Errorf("close position error: %#v", pos) + // return + // } + // t.CloseCause = cause - // apply cash change - // calc profit - var receive, profit float64 - if pos.Side == types.SideLong { - profit = t.Qty*(t.Price-pos.EntryPx) - t.Fee - pos.Fee - receive = t.Price*pos.Qty - t.Fee - } else { - profit = (pos.EntryPx - t.Price) * t.Qty - receive = pos.EntryPx*pos.Qty + profit - t.Fee - profit = profit - t.Fee - pos.Fee - } - a.cash += receive - a.profit += profit + // // apply cash change + // // calc profit + // var receive, profit float64 + // if pos.Side == types.SideLong { + // profit = t.Qty*(t.Price-pos.EntryPx) - t.Fee - pos.Fee + // receive = t.Price*pos.Qty - t.Fee + // } else { + // profit = (pos.EntryPx - t.Price) * t.Qty + // receive = pos.EntryPx*pos.Qty + profit - t.Fee + // profit = profit - t.Fee - pos.Fee + // } + // a.cash += receive + // a.profit += profit - if profit > 0 { - a.winningTrades++ - } else { - a.losingTrades++ - } - a.fee += t.Fee + // if profit > 0 { + // a.winningTrades++ + // } else { + // a.losingTrades++ + // } + // a.fee += t.Fee - // remove position - delete(a.positions, pos.TradeId) - a.closeTrades[t.Id] = t + // // remove position + // delete(a.positions, pos.TradeId) + // a.closeTrades[t.Id] = t - if trade, ok := a.trades[pos.TradeId]; ok { - trade.ClosePrice = closePrice - trade.CloseFee = t.Fee - trade.CloseTime = kline.Ts - trade.CloseCause = cause - trade.Pnl = profit - trade.HoldTime = conver.TimeDurationFormat(time.Duration(trade.CloseTime-trade.Time)*time.Millisecond, ".") - trade.PeakPx = pos.PeakPx - trade.Cash = a.CurrentEquity(closePrice) - } + // if trade, ok := a.trades[pos.TradeId]; ok { + // trade.ClosePrice = closePrice + // trade.CloseFee = t.Fee + // trade.CloseTime = kline.Ts + // trade.CloseCause = cause + // trade.Pnl = profit + // trade.HoldTime = conver.TimeDurationFormat(time.Duration(trade.CloseTime-trade.Time)*time.Millisecond, ".") + // trade.PeakPx = pos.PeakPx + // trade.Cash = a.CurrentEquity(closePrice) + // } - // 记录最大回撤 [high, low, high, low] - currentEquity := a.CurrentEquity(closePrice) - if currentEquity < a.maxDrawdown[1] { - a.maxDrawdown[1] = currentEquity - } - if currentEquity > a.maxDrawdown[0] { - if a.maxDrawdown[0]-a.maxDrawdown[1] > a.maxDrawdown[2]-a.maxDrawdown[3] { - a.maxDrawdown[2], a.maxDrawdown[3] = a.maxDrawdown[0], a.maxDrawdown[1] - } - a.maxDrawdown[0] = currentEquity - a.maxDrawdown[1] = currentEquity - } + // // 记录最大回撤 [high, low, high, low] + // currentEquity := a.CurrentEquity(closePrice) + // if currentEquity < a.maxDrawdown[1] { + // a.maxDrawdown[1] = currentEquity + // } + // if currentEquity > a.maxDrawdown[0] { + // if a.maxDrawdown[0]-a.maxDrawdown[1] > a.maxDrawdown[2]-a.maxDrawdown[3] { + // a.maxDrawdown[2], a.maxDrawdown[3] = a.maxDrawdown[0], a.maxDrawdown[1] + // } + // a.maxDrawdown[0] = currentEquity + // a.maxDrawdown[1] = currentEquity + // } return } diff --git a/internal/trading/backtest/trade_account.go b/internal/trading/backtest/trade_account.go new file mode 100644 index 0000000..f2fd0cc --- /dev/null +++ b/internal/trading/backtest/trade_account.go @@ -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 +} diff --git a/internal/trading/backtest/trade_simulator.go b/internal/trading/backtest/trade_simulator.go index 2c9f11d..33d48c6 100644 --- a/internal/trading/backtest/trade_simulator.go +++ b/internal/trading/backtest/trade_simulator.go @@ -2,6 +2,8 @@ package backtest import ( "math" + "sig-pub/pkg/data" + "sig-pub/pkg/trade" "sig-pub/pkg/types" "sig-pub/pkg/types/decimals" ) @@ -10,7 +12,6 @@ type TradeSimulator struct { FeePct float64 // e.g. 0.0005 = 0.05% SlippagePct float64 // e.g. 0.001 = 0.1% - TradeId int64 } func NewTradeSimulator(feePct, slippagePct float64) *TradeSimulator { @@ -18,25 +19,34 @@ func NewTradeSimulator(feePct, slippagePct float64) *TradeSimulator { } // ExecuteMarket 执行市价单,使用kline信息决定成交价(使用close以及滑点) -func (s *TradeSimulator) ExecuteMarket(side types.Side, qty float64, closePrice float64, ts int64) (trd *Trade, ok bool) { - // base price use close - base := closePrice +func (s *TradeSimulator) ExecuteMarket(tradeId int64, symbol string, ticket trade.TradeTicket) (cost float64, order *trade.TradeOrder) { + price := ticket.Price slippage := s.SlippagePct - switch side { + switch ticket.Side { + default: + panic("invalid trade side") case types.SideLong: // buy: worse price higher - base = base * (1 + slippage) + price = price * (1 + slippage) case types.SideShort: // sell: worse price lower - base = base * (1 - slippage) - default: - return + price = price * (1 - slippage) + } + fee := math.Abs(price*ticket.Qty) * s.FeePct + order = &trade.TradeOrder{ + TradeId: tradeId, + TradeType: trade.TradeTypeOpen, + InstId: symbol, + Side: ticket.Side, + Qty: ticket.Qty, + Price: price, + Fee: fee, + Leverage: ticket.Leverage, + Ctime: ticket.Ctime, + Status: data.StatusOk, + PeakPx: ticket.Price, } - fee := math.Abs(base*qty) * s.FeePct - trd = &Trade{Side: side, Qty: qty, Price: base, Fee: fee, Time: ts} - s.TradeId++ - trd.Id = s.TradeId - ok = true + cost = order.Price*order.Qty/float64(order.Leverage) + order.Fee return } diff --git a/internal/trading/backtest/trading_plan_backtester.go b/internal/trading/backtest/trading_plan_backtester.go index 96d45d3..0ac9892 100644 --- a/internal/trading/backtest/trading_plan_backtester.go +++ b/internal/trading/backtest/trading_plan_backtester.go @@ -2,14 +2,15 @@ package backtest import ( "context" - "errors" "fmt" "sig-pub/api/pb" + "sig-pub/internal/trading/sig" "sig-pub/pkg/data/entity" "sig-pub/pkg/indicator" "sig-pub/pkg/strategy" "sig-pub/pkg/trade" "sig-pub/pkg/types" + "sig-pub/pkg/types/decimals" "sig-pub/pkg/utils/collect" "sig-pub/pkg/zlog" "time" @@ -17,23 +18,32 @@ import ( "github.com/bytedance/sonic" ) +// TradingPlanBacktester 交易计划回测 // todo 将backtest独立成单独服务横向扩展 type TradingPlanBacktester struct { indicatorReg *indicator.IndicatorRegistry sigStrategyReg *strategy.SigStrategyRegistry exchangeClient pb.ExchangeServiceClient - plan entity.TradePlan - account *BacktestAccount + plan entity.TradePlan + account *BacktestTradeAccount + // account *BacktestAccount sigStrategyType strategy.SigStrategyType sigStrategy strategy.ISigStrategy sigStrategyInput types.Input - closeStrategy *trade.CloseStrategy - riskStrategy *trade.RiskStrategy - tradeStrategy *trade.TradeStrategy + + tradeStrategyInput types.Input + closeStrategy trade.ICloseStrategy + riskStrategy trade.IRiskStrategy + tradeStrategy trade.ITradeStrategy + instanceIntervalSigStrategyContext strategy.IInstanceIntervalSigStrategyContext } -func NewTradingPlanBacktester(indicatorReg *indicator.IndicatorRegistry, sigStrategyReg *strategy.SigStrategyRegistry, exchangeClient pb.ExchangeServiceClient) *TradingPlanBacktester { +func NewTradingPlanBacktester( + indicatorReg *indicator.IndicatorRegistry, + sigStrategyReg *strategy.SigStrategyRegistry, + exchangeClient pb.ExchangeServiceClient, +) *TradingPlanBacktester { return &TradingPlanBacktester{ indicatorReg: indicatorReg, sigStrategyReg: sigStrategyReg, @@ -45,7 +55,10 @@ func (b *TradingPlanBacktester) Init(cash float64, plan entity.TradePlan) (err e b.plan = plan // trade account simulator := NewTradeSimulator(0.0005, 0.0008) - b.account = NewBacktestAccount(cash, simulator) + // b.account = NewBacktestAccount(cash, simulator) + _ = simulator + b.account = NewBacktestTradeAccount(cash) + // sig strategy ok := false b.sigStrategyType, b.sigStrategy, ok = b.sigStrategyReg.NewSigStrategy(plan.SigStrategy) @@ -60,32 +73,31 @@ func (b *TradingPlanBacktester) Init(cash float64, plan entity.TradePlan) (err e return } - closeStrategyParam, tradeStrategyParam, riskStrategyParam := new(trade.CloseStrategyParam), - new(trade.TradeStrategyParam), new(trade.RiskStrategyParam) - if err = sonic.UnmarshalString(plan.CloseStrategyParam, closeStrategyParam); err != nil { + // 交易策略参数 + var tradeStrategyInput, closeStrategyInput, riskStrategyInput types.Input + if err = sonic.UnmarshalString(plan.TradeStrategyParam, &tradeStrategyInput); err != nil { return } - if err = sonic.UnmarshalString(plan.TradeStrategyParam, tradeStrategyParam); err != nil { + if err = sonic.UnmarshalString(plan.CloseStrategyParam, &closeStrategyInput); err != nil { return } - if err = sonic.UnmarshalString(plan.RiskStrategyParam, riskStrategyParam); err != nil { + if err = sonic.UnmarshalString(plan.RiskStrategyParam, &riskStrategyInput); err != nil { return } - // 平仓策略 - b.closeStrategy, err = trade.NewCloseStrategy(*closeStrategyParam) - if err != nil { + b.tradeStrategyInput = make(types.Input) + b.tradeStrategyInput.Assign(tradeStrategyInput, closeStrategyInput, riskStrategyInput) + + // 交易策略 + tradeStrate := trade.NewSigTradeStrategy() + if err = tradeStrate.Init(b.tradeStrategyInput); err != nil { return } + // 平仓策略 + b.closeStrategy = tradeStrate // 风险管理策略 - b.riskStrategy, err = trade.NewRiskStrategy(*riskStrategyParam) - if err != nil { - return - } + b.riskStrategy = tradeStrate // 交易策略 - b.tradeStrategy, err = trade.NewTradeStrategy(*tradeStrategyParam) - if err != nil { - return - } + b.tradeStrategy = tradeStrate return } @@ -108,18 +120,20 @@ func (b *TradingPlanBacktester) Backtest(ctx context.Context, sr *pb.SeriesRange sigStrategyBacktester.SubKline(sr.InstId, types.Interval1m, func(instId string, interval types.Interval, k types.Kline) (err error) { // 检查仓位平仓 - return b.closeByKlineInterval1m(k) + return b.closeByKlineInterval1m(instId, k) }) iiks := types.NewInstanceIntervalKlineSeries() + b.instanceIntervalSigStrategyContext = sig.NewInstanceIntervalSigStrategyContext(b.tradeStrategyInput, iiks, b.indicatorReg) + err = sigStrategyBacktester.Backtest(ctx, b.sigStrategyInput, sr, iiks, func(instId string, sigSide types.Side, k types.Kline) (err error) { test.Singals++ // 根据交易信号检查仓位平仓 - if err = b.closeBySigSingal(sigSide, k); err != nil { + if err = b.closeBySigSingal(instId, sigSide, k); err != nil { return } // 交易下单 - return b.onSideSingal(sigSide, k) + return b.onSideSingal(instId, sigSide, k) }) if err != nil { return @@ -137,84 +151,124 @@ func (b *TradingPlanBacktester) Backtest(ctx context.Context, sr *pb.SeriesRange // 回测结果 test.Etime = time.Now().UnixMilli() test.EndCash = b.account.cash - test.Profit = b.account.profit - test.TotalTrades = len(b.account.trades) - test.WinningTrades = b.account.winningTrades - test.LosingTrades = b.account.losingTrades - test.Fee = b.account.fee + // test.Profit = b.account.profit + // test.TotalTrades = len(b.account.trades) + // test.WinningTrades = b.account.winningTrades + // test.LosingTrades = b.account.losingTrades + // test.Fee = b.account.fee // trades var trades []*Trade for _, trade := range b.account.trades { - trade.BacktestId = test.Id + // trade.BacktestId = test.Id trade.Ctime = test.Ctime - trades = append(trades, trade) + // trades = append(trades, trade) } collect.SortAsc(trades, func(t *Trade) int64 { return t.Id }) test.Trades = trades // 最大回撤 - drawdown := b.account.maxDrawdown - test.MaxDrawdown = max((drawdown[0]-drawdown[1])/drawdown[0], (drawdown[2]-drawdown[3])/drawdown[2]) + // drawdown := b.account.maxDrawdown + // test.MaxDrawdown = max((drawdown[0]-drawdown[1])/drawdown[0], (drawdown[2]-drawdown[3])/drawdown[2]) return } // forceCloseAllHoldingPosition 关闭所有未平仓仓位 func (b *TradingPlanBacktester) forceCloseAllHoldingPosition(k types.Kline) (err error) { - for _, pos := range b.account.positions { - err = b.account.ClosePosition(pos, k, trade.CauseCloseForced) + var closeTickets []trade.TradeTicket + for _, instId := range b.account.GetOpenTradeInsts() { + k := b.instanceIntervalSigStrategyContext.Get(instId, trade.PriceDriverInterval, 0) + price := decimals.MustToFloat64(k.Close) + trades := b.account.GetOpenTrades(instId) + for _, trd := range trades { + closeTickets = append(closeTickets, trade.TradeTicket{ + InstId: instId, + TradesId: []int64{trd.TradeId}, + 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: trade.CauseCloseForced, + }) + } + } + + for _, ticket := range closeTickets { + err = b.account.CloseTradeOrder(ticket) if err != nil { - return + zlog.Errorf("close trade order error") } } + return } // closeByKlineInterval1m k线更新时检查平仓 -func (b *TradingPlanBacktester) closeByKlineInterval1m(k types.Kline) (err error) { - var posErrs []error - positions := b.account.OpenPositions() - for _, pos := range positions { - closePos, cause := b.closeStrategy.OnKline(k, pos) - if closePos { - errc := b.account.ClosePosition(pos, k, cause) - if errc != nil { - posErrs = append(posErrs, errc) - zlog.Errorf("close position error: k=%#v, err=%v", k, errc) - } - } +func (b *TradingPlanBacktester) closeByKlineInterval1m(instId string, k types.Kline) (err error) { + price := decimals.MustToFloat64(k.Close) + closeTickets, err := b.closeStrategy.CloseAssessOnPrice(b.instanceIntervalSigStrategyContext, nil, instId, price) + if err != nil { + return } - if len(posErrs) > 0 { - err = errors.Join(posErrs...) + _ = closeTickets + if len(closeTickets) == 0 { return } + + // var posErrs []error + // positions := b.account.GetOpenTrades() + // for _, pos := range positions { + // closePos, cause := b.closeStrategy.OnKline(k, pos) + // if closePos { + // errc := b.account.ClosePosition(pos, k, cause) + // if errc != nil { + // posErrs = append(posErrs, errc) + // zlog.Errorf("close position error: k=%#v, err=%v", k, errc) + // } + // } + // } + // if len(posErrs) > 0 { + // err = errors.Join(posErrs...) + // return + // } return } // closeBySigSingal 交易信号出现时检查平仓 -func (b *TradingPlanBacktester) closeBySigSingal(sigSide types.Side, kline types.Kline) (err error) { - var posErrs []error - positions := b.account.OpenPositions() - for _, pos := range positions { - closePos, cause := b.closeStrategy.OnSigStrategySingal(sigSide, pos) - if closePos { - errc := b.account.ClosePosition(pos, kline, cause) - if errc != nil { - posErrs = append(posErrs, errc) - zlog.Error("close position error: ", errc) - } - } +func (b *TradingPlanBacktester) closeBySigSingal(instId string, sigSide types.Side, kline types.Kline) (err error) { + closeTickets, err := b.closeStrategy.CloseAssessOnSig(b.instanceIntervalSigStrategyContext, nil, instId, sigSide) + if err != nil { + return } - if len(posErrs) > 0 { - err = errors.Join(posErrs...) + _ = closeTickets + if len(closeTickets) == 0 { return } + + // var posErrs []error + // positions := b.account.GetOpenTrades() + // for _, pos := range positions { + // closePos, cause := b.closeStrategy.OnSigStrategySingal(sigSide, pos) + // if closePos { + // errc := b.account.ClosePosition(pos, kline, cause) + // if errc != nil { + // posErrs = append(posErrs, errc) + // zlog.Error("close position error: ", errc) + // } + // } + // } + // if len(posErrs) > 0 { + // err = errors.Join(posErrs...) + // return + // } return } // onSideSingal 出现买卖信号 -func (b *TradingPlanBacktester) onSideSingal(sigSide types.Side, k types.Kline) (err error) { +func (b *TradingPlanBacktester) onSideSingal(instId string, sigSide types.Side, k types.Kline) (err error) { // 买卖信号交易风险分析 - - doTrade, causes, err := b.riskStrategy.SigRiskAnalyze(b.account, sigSide) + doTrade, causes, err := b.riskStrategy.RishAssess(b.instanceIntervalSigStrategyContext, nil, instId, sigSide) if err != nil { return } @@ -222,16 +276,14 @@ func (b *TradingPlanBacktester) onSideSingal(sigSide types.Side, k types.Kline) _ = causes // todo 记录信号不交易原因分析 log db analyze return } - tradeArg, err := b.tradeStrategy.SigTrade(sigSide, k) + ticket, err := b.tradeStrategy.TradeAssess(b.instanceIntervalSigStrategyContext, nil, instId, sigSide) if err != nil { return } - ok, cause, err := b.account.TradeOrder(tradeArg) + order, err := b.account.MarketOrder(ticket) if err != nil { return } - if !ok { - _ = cause // todo 记录不交易原因 - } + _ = order return } diff --git a/internal/trading/trading_grpc_server.go b/internal/trading/trading_grpc_server.go index 4732c5d..ee91b23 100644 --- a/internal/trading/trading_grpc_server.go +++ b/internal/trading/trading_grpc_server.go @@ -3,7 +3,6 @@ package trading import ( "context" "sig-pub/api/pb" - "sig-pub/pkg/indicator" "sig-pub/pkg/types" "sig-pub/pkg/utils/times" @@ -26,52 +25,34 @@ func (svr *TradingGrpcServer) Init() (err error) { } func (svr *TradingGrpcServer) IndicatorPlots(ctx context.Context, req *pb.ReqIndicatorPlots) (rsp *pb.RspIndicatorPlots, err error) { - plots, err := svr.tradingService.IndicatorPlots(req.Indicators...) + indPlots, err := svr.tradingService.IndicatorPlots(req.Indicators...) if err != nil { return } rsp = new(pb.RspIndicatorPlots) - for name, plot := range plots { - p := &pb.IndicatorPlot{Indicator: name} - p.Series, err = mappingIndicatorPlotSeries(plot.Series) - if err != nil { - return nil, err - } - for _, series := range plot.StateSeries { - ps, err := mappingIndicatorPlotSeries(series) - if err != nil { - return rsp, err + for _, indName := range req.Indicators { + plots := indPlots[indName] + p := &pb.IndicatorPlot{Indicator: indName} + for _, plot := range plots { + ps := &pb.IndicatorPlotSeries{ + State: plot.State, + Type: int32(plot.Type), } - p.StateSeries = append(p.StateSeries, ps) - } - rsp.Plots = append(rsp.Plots, p) - } - return -} - -func mappingIndicatorPlotSeries(ps indicator.PlotSeries) (splot *pb.IndicatorPlotSeries, err error) { - seriesProps, err := structpb.NewStruct(ps.Props) - if err != nil { - return - } - splot = &pb.IndicatorPlotSeries{ - State: ps.State, - Type: int32(ps.Type), - Props: seriesProps, - StateEnumProps: nil, - } - for state, valueProps := range ps.State2Props { - s2p := &pb.IndicatorStateEnumProps{ - State: state, - EnumProps: make(map[int32]*structpb.Struct), - } - for v, props := range valueProps { - s2p.EnumProps[int32(v)], err = structpb.NewStruct(props) - if err != nil { + if ps.Props, err = structpb.NewStruct(plot.Props); err != nil { return } + for _, exp := range plot.Exps { + pe := &pb.IndicatorPlotExp{ + Exp: exp.Exp, + } + if pe.Props, err = structpb.NewStruct(exp.Props); err != nil { + return + } + ps.Exps = append(ps.Exps, pe) + } + p.Plots = append(p.Plots, ps) } - splot.StateEnumProps = append(splot.StateEnumProps, s2p) + rsp.Plots = append(rsp.Plots, p) } return } diff --git a/internal/trading/trading_service.go b/internal/trading/trading_service.go index 080755e..f86f641 100644 --- a/internal/trading/trading_service.go +++ b/internal/trading/trading_service.go @@ -199,15 +199,23 @@ func (svc *TradingService) fetchHistoryKlineSeries(ctx context.Context, sr *pb.S } // IndicatorPlots -func (svc *TradingService) IndicatorPlots(indicatorNames ...string) (plots map[string]indicator.Plot, err error) { - plots = make(map[string]indicator.Plot, len(indicatorNames)) +func (svc *TradingService) IndicatorPlots(indicatorNames ...string) (indPlots map[string][]indicator.Plot, err error) { + indPlots = make(map[string][]indicator.Plot, len(indicatorNames)) for _, indicatorName := range indicatorNames { - indicator, ok := svc.indicatorReg.Indicator(indicatorName) + ind, ok := svc.indicatorReg.Indicator(indicatorName) if !ok { err = fmt.Errorf("indicator %s not exists", indicatorName) return } - plots[indicatorName] = indicator.Meta().Plot + plots := ind.Meta().Plots + if len(plots) == 0 { + plots = append(plots, indicator.Plot{ + State: "vector", + Type: indicator.PlotLine, + Props: indicator.PlotProps{"color": indicator.ColorBlue}, + }) + } + indPlots[indicatorName] = plots } return } diff --git a/pkg/data/common.go b/pkg/data/common.go index 072a733..537571e 100644 --- a/pkg/data/common.go +++ b/pkg/data/common.go @@ -9,8 +9,8 @@ const ( StatusNone Status = 0 StatusOk Status = 1 StatusProcessing Status = 2 - StatusDeleted Status = 4 - StatusFailed Status = 5 + StatusDeleted Status = 3 + StatusFailed Status = 4 ) var ( diff --git a/pkg/indicator/indicator.go b/pkg/indicator/indicator.go index 49a9cab..792d959 100644 --- a/pkg/indicator/indicator.go +++ b/pkg/indicator/indicator.go @@ -15,7 +15,7 @@ type IndicatorMeta struct { Desc string `json:"desc"` // 指标描述 Input []types.InputArg `json:"input"` // 输入参数 State []string `json:"state"` // 向外暴露状态 - Plot Plot `json:"plot"` // 指标绘图属性 + Plots []Plot `json:"plots"` // 指标绘图属性 } // IIndicator 指标基础计算接口 diff --git a/pkg/indicator/indicator_plot.go b/pkg/indicator/indicator_plot.go index 7c7ed76..fa868e9 100644 --- a/pkg/indicator/indicator_plot.go +++ b/pkg/indicator/indicator_plot.go @@ -1,10 +1,10 @@ package indicator -type Plot0 struct { - State string `json:"state"` // vector, stateName - Type PlotSeriesType `json:"series"` // 绘图类型 线/柱 - Props PlotProps `json:"props"` // 绘图属性 - Exps []PlotExp `json:"exps"` // 绘图条件表达式 +type Plot struct { + State string `json:"state"` // vector, stateName + Type PlotType `json:"series"` // 绘图类型 线/柱 + Props PlotProps `json:"props"` // 绘图属性 + Exps []PlotExp `json:"exps"` // 绘图条件表达式 } type PlotExp struct { @@ -12,32 +12,17 @@ type PlotExp struct { Props PlotProps `json:"props"` // 条件表达式成立时绘图属性 } -// 指标绘图 -type Plot struct { - Props PlotProps // 长度颜色等属性 - Series PlotSeries - StateSeries []PlotSeries // 需要绘图的时间序列列表 -} - -// 指标绘图: 逻辑化 -> 配置化 -type PlotSeries struct { - State string `json:"state"` // vector, stateName - Type PlotSeriesType `json:"series"` // 绘图类型 线/柱 - Props PlotProps `json:"props"` // 绘图属性 - State2Props map[string]map[float64]PlotProps `json:"state2Props"` // state转绘图属性 -} - // 指标绘图 type PlotProps map[string]any -// PlotSeriesType 序列值绘图类型 -type PlotSeriesType int32 +// PlotType 序列值绘图类型 +type PlotType int32 const ( - PlotSeriesNone PlotSeriesType = iota - PlotSeriesLine - PlotSeriesHistogram - PlotSeriesArea + PlotNone PlotType = iota + PlotLine + PlotHistogram + PlotArea ) // Series 绘图颜色 @@ -47,8 +32,3 @@ const ( ColorYellow string = "yellow" ColorBlue string = "blue" ) - -const ( -// calc tokens: > >= == < <= + - * / % -// 大于等于小于 In, NotIn ... -) diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index 763f3ff..0a25b6d 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -14,17 +14,13 @@ func (c *MACD) Meta() IndicatorMeta { {Name: "singal", Type: types.InputTypeUInt, Desc: "信号线周期"}, // 9 }, State: []string{"dif", "dea"}, - Plot: Plot{ - Series: PlotSeries{Type: PlotSeriesHistogram, Props: PlotProps{"color": ColorGreen}, State2Props: map[string]map[float64]PlotProps{ - "vector": { - -1: {"vector >= 0": 1, "color": ColorRed}, - 1: {"vector < 0": 1, "color": ColorGreen}, - }, + Plots: []Plot{ + {State: "vector", Type: PlotHistogram, Props: PlotProps{"color": ColorGreen}, Exps: []PlotExp{ + {Exp: "vector < 0", Props: PlotProps{"color": ColorRed}}, + {Exp: "vector >= 0", Props: PlotProps{"color": ColorGreen}}, }}, - StateSeries: []PlotSeries{ - {State: "dif", Type: PlotSeriesLine, Props: PlotProps{"color": ColorYellow}}, - {State: "dea", Type: PlotSeriesLine, Props: PlotProps{"color": ColorBlue}}, - }, + {State: "dif", Type: PlotLine, Props: PlotProps{"color": ColorYellow}}, + {State: "dea", Type: PlotLine, Props: PlotProps{"color": ColorBlue}}, }, } } diff --git a/pkg/indicator/super_trend.go b/pkg/indicator/super_trend.go index d7e6e99..e706d8c 100644 --- a/pkg/indicator/super_trend.go +++ b/pkg/indicator/super_trend.go @@ -14,12 +14,10 @@ func (c SuperTrend) Meta() IndicatorMeta { {Name: "mul", Type: types.InputTypeUInt, Desc: "乘数(建议2-4)"}, }, State: []string{"direction"}, - Plot: Plot{ - Series: PlotSeries{Type: PlotSeriesLine, Props: PlotProps{"color": ColorGreen}, State2Props: map[string]map[float64]PlotProps{ - "direction": { - -1: {"color": ColorRed}, - 1: {"color": ColorGreen}, - }, + Plots: []Plot{ + {State: "vector", Type: PlotLine, Props: PlotProps{"color": ColorGreen}, Exps: []PlotExp{ + {Exp: "direction == -1", Props: PlotProps{"color": ColorRed}}, + {Exp: "direction == 1", Props: PlotProps{"color": ColorGreen}}, }}, }, } diff --git a/pkg/strategy/mul_inst_rank.go b/pkg/strategy/mul_inst_rank.go index 5bfaf33..65163d9 100644 --- a/pkg/strategy/mul_inst_rank.go +++ b/pkg/strategy/mul_inst_rank.go @@ -2,6 +2,7 @@ package strategy import ( "sig-pub/pkg/types" + "sig-pub/pkg/utils/collect" ) // 多币种多周期策略 @@ -40,3 +41,29 @@ func (s *MultiInstanceRank) CandlePeriods(ctx IInstanceIntervalSigStrategyContex iss.Set(types.Interval30m, 2) return } + +func Update(ctx IInstanceIntervalSigStrategyContext) (sides []types.SideInst) { + insts := []string{"BTC_USDT", "SOL_USDT"} + type instRank struct { + inst string + atr, score float64 + } + var ranks []instRank + for _, instId := range insts { + atr := ctx.Indicator(instId, types.Interval5m, "ATR", 7, 3).Get(0) + adx := ctx.Indicator(instId, types.Interval5m, "ADX", 7, 3).Get(0) + ranks = append(ranks, instRank{ + inst: instId, + atr: atr, + score: atr*1.5 + adx, + }) + } + + // score rank + collect.SortDesc(ranks, func(ir instRank) float64 { return ir.score }) + + st1 := ctx.Indicator("BTC_USDT", types.Interval5m, "SuperTrend", 7, 3) + st2 := ctx.Indicator("SOL_USDT", types.Interval5m, "SuperTrend", 7, 3) + _, _ = st1, st2 + return +} diff --git a/pkg/trade/close_strategy.go b/pkg/trade/close_strategy.go deleted file mode 100644 index 1b6c4f3..0000000 --- a/pkg/trade/close_strategy.go +++ /dev/null @@ -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 -} diff --git a/pkg/trade/risk_strategy.go b/pkg/trade/risk_strategy.go deleted file mode 100644 index fd5b2a7..0000000 --- a/pkg/trade/risk_strategy.go +++ /dev/null @@ -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 -} diff --git a/pkg/trade/sig_close_strategy.go b/pkg/trade/sig_close_strategy.go new file mode 100644 index 0000000..2daa70c --- /dev/null +++ b/pkg/trade/sig_close_strategy.go @@ -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 +} diff --git a/pkg/trade/sig_trade_strategy.go b/pkg/trade/sig_trade_strategy.go new file mode 100644 index 0000000..390b7c8 --- /dev/null +++ b/pkg/trade/sig_trade_strategy.go @@ -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 +} diff --git a/pkg/trade/trade_account.go b/pkg/trade/trade_account.go index b46e29a..8593424 100644 --- a/pkg/trade/trade_account.go +++ b/pkg/trade/trade_account.go @@ -1,23 +1,22 @@ package trade -import "sig-pub/pkg/types" - // sig -> close strategy // sig -> risk strategy -> trade strategy -> tarde account // position, trades type ITradeAccount interface { // 获取未平仓交易单 - OpenPositions() map[int64]*Position - - // 获取未平仓交易单数 - CountOpenPositions() int + GetOpenTrades() []*TradeOrder - // 订单下单 - TradeOrder(ta TradeArg) (ok bool, cause Cause, err error) + MarketOrder(symbol string, ticket TradeTicket) (order *TradeOrder, err error) - // 将仓位进行平仓 - ClosePosition(*Position, types.Kline, Cause) (err error) + CloseTradeOrder(order *TradeOrder) (err error) + // 获取未平仓交易单数 + // CountOpenPositions() int + // // 订单下单 + // TradeOrder(ta TradeTicket) (ok bool, cause Cause, err error) + // // 将仓位进行平仓 + // ClosePosition(*Position, types.Kline, Cause) (err error) // // 根据当前价格对仓位进行 mark-to-market,返回账户净值 // GetCurrentEquity() decimal.Decimal // // 返回当前仓位的名义总敞口(绝对值) diff --git a/pkg/trade/trade_strategy.go b/pkg/trade/trade_strategy.go index 1e585bb..34acf3f 100644 --- a/pkg/trade/trade_strategy.go +++ b/pkg/trade/trade_strategy.go @@ -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 { - Side types.Side // 开仓方向 - Price float64 // 开仓价格 - Leverage int32 // 杠杆倍数 - Qty float64 // 交易量 qty为基础货币数量 - KInterval string // 交易k线周期 - KTime int64 // 交易k线时间 - Time int64 // 交易时间 +type TradeTicket struct { + InstId string + Side types.Side // 开仓方向 + Price float64 // 开仓价格 + Leverage int32 // 杠杆倍数 + Qty float64 // 交易量 qty为交易产品数量 + Interval string // k线周期 + Ktime int64 // k线时间 + Ctime int64 // 创建时间(ctime-ktime=信号延迟) + Cause Cause + TradesId []int64 // 关联交易订单id (仅平仓使用) } diff --git a/pkg/trade/types.go b/pkg/trade/types.go index 0131dc2..5558e2f 100644 --- a/pkg/trade/types.go +++ b/pkg/trade/types.go @@ -1,6 +1,7 @@ package trade import ( + "sig-pub/pkg/data" "sig-pub/pkg/types" ) @@ -40,36 +41,65 @@ func (c Cause) String() string { // Position 持仓仓位 type Position struct { - TradeId int64 // 交易订单id - InstId string // 交易产品id - Status int32 // 1.交易中 2.持仓中 3.已平仓 - Side types.Side // 交易方向 - Qty float64 // 交易量 - EntryPx float64 // 入场价格 - EntryTs int64 // 入场时间 - PeakPx float64 // highest (for long) or lowest (for short) observed price since entry - AvgPx float64 // 平均持仓价格 - Fee float64 // 手续费 - FeeRate float64 // 手续费率 + InstId string // 交易产品id + Side types.Side // 交易方向 + Qty float64 // 持仓量 + Leverage int32 // 杠杆倍数 + EntryPx float64 // 入场价格(均价) + EntryTs int64 // 入场时间 + PeakPx float64 // 持仓最高价格(空单最低价格) } // TradeOrder 交易订单 type TradeOrder struct { - InstId string // 交易产品id - Status int32 // 1.交易中 2.持仓中 3.已平仓 - Ctime int64 // 创建时间 - Side types.Side // 交易方向 - Qty float64 // 交易量 - Price float64 // 开仓价格 - Fee float64 // 开仓手续费 - Leverage int32 // 杠杆倍数 - Time int64 // 开仓时间 - // ClosePrice float64 // 平仓价格 - // CloseFee float64 // 平仓手续费 - // CloseTime int64 // 平仓时间 - // CloseCause Cause // 平仓原因 - Pnl float64 // 盈利/亏损 pnl = (t.ClosePrice-t.Price)*t.Qty - t.Fee - t.CloseFee + TradeId int64 // 交易单id + TradeType TradeType // 交易类型: 1.开仓 2.平仓 + InstId string // 交易产品id + Side types.Side // 交易方向 + Qty float64 // 交易量 + Price float64 // 交易价格 + Fee float64 // 手续费 + Leverage int32 // 杠杆倍数 + Ctime int64 // 交易时间 + Status data.Status // 1.交易成功 2.交易中 4.交易失败 + PeakPx float64 // 持仓最高价格(空单最低价格) + // 平仓单信息 Cash float64 // 平仓后账户净值 HoldTime string // 持仓时间 - PeakPx float64 // highest (for long) or lowest (for short) observed price since entry + Pnl float64 // 浮盈 + Profit float64 // 利润 + EntryPx float64 // 开仓价格(均价) + EntryFee float64 // 开仓手续费(总计) + EntryTs int64 // 开仓时间 + Trades []int64 // 关联的平仓交易单 +} + +type TradeType int32 + +const ( + TradeTypeOpen TradeType = 1 // 开仓 + TradeTypeClose TradeType = 2 // 平仓 +) + +// TradeStrategyInput 交易策略参数 +// Kelly准则优化方法 +type TradeStrategyInput struct { + MaxPosPct float64 // 单笔交易最大仓位占比 + MaxExposurePct float64 // 最大总敞口占比 + MaxLots float64 // 最大手数/数量 (optional) +} + +// CloseStrategyInput 平仓策略参数 +type CloseStrategyInput 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"` // 计算止盈止损时是否包含手续费 +} + +// RiskStrategyInput 风险管理策略测试 +type RiskStrategyInput struct { + SkipOnSideOpposite bool // 当前持有反方向单时 + SkipOnSideSame bool // 当前持有相同方向单时 } diff --git a/pkg/types/input.go b/pkg/types/input.go index 0f95e82..ff8a5bc 100644 --- a/pkg/types/input.go +++ b/pkg/types/input.go @@ -2,7 +2,9 @@ package types import ( "fmt" + "maps" + "github.com/go-viper/mapstructure/v2" "github.com/spf13/cast" ) @@ -91,33 +93,49 @@ func (in Input) Int16(k string) (v int16) { } func (in Input) String(k string) (v string) { - // cacheK := "str:" + k - // if r, ok := in.getCache(cacheK); ok { - // if v, ok = r.(string); ok { - // return - // } - // } v, err := cast.ToStringE(in.get(k, "string")) if err != nil { panic(fmt.Errorf("input string parse error: %s", k)) } - // in.setCache(cacheK, v) return } +func (in Input) Decode(k string, point any) { + t := fmt.Sprintf("%T", point) + v := in.get(k, t) + if err := mapstructure.Decode(v, point); err != nil { + panic(fmt.Errorf("input %s decode error: %s, %v", t, k, err)) + } +} + +func (in Input) DecodeInput(point any) { + if err := mapstructure.Decode(in, point); err != nil { + panic(fmt.Errorf("input decode to %T error: %v", point, err)) + } +} + +// Assign 将other中的值赋给当前Input +func (in Input) Assign(others ...Input) { + for _, other := range others { + maps.Copy(in, other) + } +} + // 参数类型 type InputType int8 const ( _ InputType = iota InputTypeBool + InputTypeString InputTypeFloat InputTypeUFloat InputTypeInt InputTypeUInt - InputTypeString - InputTypeSelect // 单选 - InputTypeCheckBox // 多选 + InputTypeUFloats // float数组 + InputTypeUFloats2D // float二维数组 + InputTypeSelect // 单选 + InputTypeCheckBox // 多选 ) type InputArg struct { diff --git a/pkg/types/input_test.go b/pkg/types/input_test.go new file mode 100644 index 0000000..24d503e --- /dev/null +++ b/pkg/types/input_test.go @@ -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) +} diff --git a/pkg/types/signal.go b/pkg/types/signal.go index e2b4f74..abe064f 100644 --- a/pkg/types/signal.go +++ b/pkg/types/signal.go @@ -11,6 +11,18 @@ func (side Side) IsValid() bool { return side == SideLong || side == SideShort } +// Opposite 获取相反交易方向 +func (side Side) Opposite() Side { + switch side { + case SideLong: + return SideShort + case SideShort: + return SideLong + default: + return 0 + } +} + func (side Side) String() string { switch side { default: diff --git a/pkg/utils/collect/collect.go b/pkg/utils/collect/collect.go index 2cc9ea2..0a3c159 100644 --- a/pkg/utils/collect/collect.go +++ b/pkg/utils/collect/collect.go @@ -31,16 +31,34 @@ func Count[T any](slice []T, isCount func(int, T) bool) (count int) { return } -func Filter[T any](slice []T, predicate func(int, T) bool) []T { +// Filter 从切片过滤元素返回新切片 +// predicate 返回true保留 +func Filter[T any](slice []T, predicate func(T) bool) []T { var res []T - for i, item := range slice { - if predicate(i, item) { + for _, item := range slice { + if predicate(item) { res = append(res, item) } } return res } +// Remove 移除原切片元素 +// predicate 返回true移除 +func Remove[T any](slice *[]T, predicate func(T) bool) (removes int) { + writeIndex := 0 + for _, item := range *slice { + if predicate(item) { + removes++ + } else { + (*slice)[writeIndex] = item + writeIndex++ + } + } + *slice = (*slice)[:writeIndex] + return +} + func Uniq[T comparable, Slice ~[]T](collection Slice) Slice { result := make(Slice, 0, len(collection)) seen := make(map[T]struct{}, len(collection)) @@ -66,6 +84,15 @@ func Find[T any](slice []T, predicate func(T) bool) (r T, ok bool) { return } +func FindIndex[T any](slice []T, predicate func(T) bool) (i int, ok bool) { + for i, item := range slice { + if predicate(item) { + return i, true + } + } + return +} + // SortAsc 切片升序排序 func SortAsc[T any, C cmp.Ordered](slice []T, compare func(T) C) { if len(slice) < 2 { diff --git a/pkg/utils/collect/concurrent_map_test.go b/pkg/utils/collect/concurrent_map_test.go index b04d8a6..ce1064d 100644 --- a/pkg/utils/collect/concurrent_map_test.go +++ b/pkg/utils/collect/concurrent_map_test.go @@ -9,6 +9,18 @@ import ( "time" ) +func TestRemove(t *testing.T) { + m := map[string][]int{ + "name": {1, 2, 3, 4, 5, 6}, + } + arr := m["name"] + Remove(&arr, func(v int) bool { + return true + }) + fmt.Println(arr) + fmt.Println(m["name"]) +} + func TestConcurrentMap(t *testing.T) { cm := NewConcurrentMap[string, string](19, func(k string) string { return k }) concurrent := 1000 diff --git a/pkg/utils/expression/expression.go b/pkg/utils/expression/expression.go new file mode 100644 index 0000000..a32703d --- /dev/null +++ b/pkg/utils/expression/expression.go @@ -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) +} diff --git a/pkg/utils/expression/expression_test.go b/pkg/utils/expression/expression_test.go new file mode 100644 index 0000000..bb5272d --- /dev/null +++ b/pkg/utils/expression/expression_test.go @@ -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) +}