Browse Source

trade strategy,account

main
strange 8 months ago
parent
commit
97d567b844
  1. 12
      README.md
  2. 20
      api/pub.proto
  3. 145
      internal/trading/backtest/account.go
  4. 175
      internal/trading/backtest/trade_account.go
  5. 38
      internal/trading/backtest/trade_simulator.go
  6. 202
      internal/trading/backtest/trading_plan_backtester.go
  7. 59
      internal/trading/trading_grpc_server.go
  8. 16
      internal/trading/trading_service.go
  9. 4
      pkg/data/common.go
  10. 2
      pkg/indicator/indicator.go
  11. 42
      pkg/indicator/indicator_plot.go
  12. 16
      pkg/indicator/macd.go
  13. 10
      pkg/indicator/super_trend.go
  14. 27
      pkg/strategy/mul_inst_rank.go
  15. 134
      pkg/trade/close_strategy.go
  16. 42
      pkg/trade/risk_strategy.go
  17. 146
      pkg/trade/sig_close_strategy.go
  18. 88
      pkg/trade/sig_trade_strategy.go
  19. 19
      pkg/trade/trade_account.go
  20. 87
      pkg/trade/trade_strategy.go
  21. 82
      pkg/trade/types.go
  22. 38
      pkg/types/input.go
  23. 39
      pkg/types/input_test.go
  24. 12
      pkg/types/signal.go
  25. 33
      pkg/utils/collect/collect.go
  26. 12
      pkg/utils/collect/concurrent_map_test.go
  27. 387
      pkg/utils/expression/expression.go
  28. 35
      pkg/utils/expression/expression_test.go

12
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, 再发布下一波

20
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<int32, google.protobuf.Struct> enumProps = 2;
message IndicatorPlotExp {
string exp = 1;
google.protobuf.Struct props = 3;
}

145
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
}

175
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
}

38
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
}

202
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
}

59
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
}

16
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
}

4
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 (

2
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 指标基础计算接口

42
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 ...
)

16
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}},
},
}
}

10
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}},
}},
},
}

27
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
}

134
pkg/trade/close_strategy.go

@ -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
}

42
pkg/trade/risk_strategy.go

@ -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
}

146
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
}

88
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
}

19
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
// // 返回当前仓位的名义总敞口(绝对值)

87
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 (仅平仓使用)
}

82
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 // 当前持有相同方向单时
}

38
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 {

39
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)
}

12
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:

33
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 {

12
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

387
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)
}

35
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)
}
Loading…
Cancel
Save