diff --git a/README.md b/README.md index 26abae2..f4c9fa8 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,16 @@ kline时间窗口 Dragonfly redis替换 +CCXT – [加密货币交易所交易库](https://github.com/ccxt/ccxt) + ### 绘图框架 tradingview advanced-charts: - https://cn.tradingview.com/advanced-charts/ tradingview lightweight-charts: - https://tradingview.github.io/lightweight-charts/ - https://github.com/tradingview/lightweight-charts + - widgets: https://www.tradingview.com/widget-docs/widgets/charts/symbol-overview/ + - free: https://www.tradingview.com/free-charting-libraries/ d3js: - https://d3js.org/ night-vision: diff --git a/api/pub.proto b/api/pub.proto index c4c4354..a30acb6 100644 --- a/api/pub.proto +++ b/api/pub.proto @@ -146,6 +146,6 @@ message SeriesRange { bool live = 8; // 实时k线, before和after为0时是否追加实时k线 bool desc = 9; // 是否降序, 默认升序 - uint32 window = 10; // 需要额外拉取更早的k线条数 + uint32 windowExtra = 10; // 需要额外拉取更早的k线条数 uint32 limit = 11; // 大于0时检查, 数据长度超过limit则返回错误 } diff --git a/api/trading.proto b/api/trading.proto index 0cfddcc..754a8fe 100644 --- a/api/trading.proto +++ b/api/trading.proto @@ -44,8 +44,6 @@ message ReqStrategySeries { message RspStrategySeries { repeated Side signal = 1; // 0.sell,1.buy repeated int64 times = 2; - repeated bool wins = 3; // 下一根k线价格方向是否正确 - double winRate = 4; } message ReqBacktest { diff --git a/internal/exchange/exchange_service.go b/internal/exchange/exchange_service.go index 0047b2d..1a15641 100644 --- a/internal/exchange/exchange_service.go +++ b/internal/exchange/exchange_service.go @@ -732,8 +732,8 @@ func (svc *ExchangeService) CalcSeriesRange(arg *pb.SeriesRange) (after, before, } } // 额外拉取 - if arg.Window > 0 { - before = max(intervalAdder(before, -int64(arg.Window)), KlineBefore0) + if arg.WindowExtra > 0 { + before = max(intervalAdder(before, -int64(arg.WindowExtra)), KlineBefore0) } if before > after { err = fmt.Errorf("time range invalid: before must less then after") diff --git a/internal/trading/backtest/backtest.go b/internal/trading/backtest/backtest.go index 11fafd9..7f49087 100644 --- a/internal/trading/backtest/backtest.go +++ b/internal/trading/backtest/backtest.go @@ -40,7 +40,7 @@ func (b *Backtest) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingP instId := plan.InstId sigStrategy := tradingPlan.GetSigStrategy() - maxWindow := sigStrategy.MaxWindow() + maxWindow := int(sigStrategy.RequiredSeries()) if maxWindow < 0 || maxWindow > indicator.MaxWindow { err = fmt.Errorf("invalid window %d 0-%d, planId=%d", maxWindow, indicator.MaxWindow, plan.Id) return @@ -53,15 +53,15 @@ func (b *Backtest) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingP closeManager.SetDynamicParams(0.1, 0.02, 0) seriesRange := &pb.SeriesRange{ - Exchange: exchange, - InstId: instId, - Interval: string(interval), - Before: stime, - After: etime, - Open: false, - Live: false, - Desc: false, - Window: uint32(maxWindow), + Exchange: exchange, + InstId: instId, + Interval: string(interval), + Before: stime, + After: etime, + Open: false, + Live: false, + Desc: false, + WindowExtra: uint32(maxWindow), } // fetch history klines via stream req := &pb.ReqHistoryKlineStream{Series: seriesRange} diff --git a/internal/trading/sig/indicator_context.go b/internal/trading/sig/indicator_context.go index b7d7121..ce45422 100644 --- a/internal/trading/sig/indicator_context.go +++ b/internal/trading/sig/indicator_context.go @@ -60,12 +60,13 @@ func (c *IndicatorContext) Series(offset, count int16) (klines series.Klines) { offset += c.offset ks, ok := c.kSeries.Series(offset, count) if !ok { - zlog.Warningf("get kline series offset out of range: offset=%d, count=%d, lastTs=%d", offset, count, c.kSeries.LastTs()) + zlog.Warningf("get kline series offset out of range: offset=%d, count=%d, length=%d, lastTs=%d", offset, count, c.kSeries.Length(), c.kSeries.LastTs()) panic(fmt.Errorf("get kline series offset out of range: offset=%d, count=%d", offset, count)) } return ks } +// Deprecated: 用流处理(exchange rpc stream) type HistoryIndicatorContext struct { IOffsetIndicatorContext exchangeClient pb.ExchangeServiceClient diff --git a/internal/trading/trading_grpc_server.go b/internal/trading/trading_grpc_server.go index 1ea4e17..6c7aa69 100644 --- a/internal/trading/trading_grpc_server.go +++ b/internal/trading/trading_grpc_server.go @@ -22,7 +22,7 @@ func (svr *TradingGrpcServer) Init() (err error) { } func (svr *TradingGrpcServer) IndicatorSeries(ctx context.Context, req *pb.ReqIndicatorSeries) (rsp *pb.RspIndicatorSeries, err error) { - matrix, times, err := svr.tradingService.IndicatorSeries(req.Indicator, req.Window, req.Series) + matrix, times, err := svr.tradingService.IndicatorSeries(ctx, req.Indicator, req.Window, req.Series) if err != nil { return } @@ -34,7 +34,7 @@ func (svr *TradingGrpcServer) IndicatorSeries(ctx context.Context, req *pb.ReqIn func (svr *TradingGrpcServer) StrategySeries(ctx context.Context, req *pb.ReqStrategySeries) (rsp *pb.RspStrategySeries, err error) { rsp = &pb.RspStrategySeries{} - err = svr.tradingService.StrategySeries(req, rsp) + err = svr.tradingService.StrategySeries(ctx, req, rsp) return } diff --git a/internal/trading/trading_service.go b/internal/trading/trading_service.go index 5aff713..ec3ecbf 100644 --- a/internal/trading/trading_service.go +++ b/internal/trading/trading_service.go @@ -3,6 +3,7 @@ package trading import ( "context" "fmt" + "io" "sig-pub/api/pb" "sig-pub/pkg/client" "sig-pub/pkg/data" @@ -13,12 +14,14 @@ import ( "sig-pub/pkg/types" "sig-pub/pkg/utils/collect" "sig-pub/pkg/utils/lang" + "sig-pub/pkg/utils/times" "sig-pub/pkg/zlog" "sig-pub/internal/trading/backtest" "sig-pub/internal/trading/sig" "github.com/bytedance/sonic" + "google.golang.org/grpc" ) type TradingService struct { @@ -153,8 +156,48 @@ func (svc *TradingService) getTradingPlan(plan *entity.TradePlan, sigKlineSeries // }() } +// fetchHistoryKlineSeries 请求k线数据流式处理 +func (svc *TradingService) fetchHistoryKlineSeries(ctx context.Context, sr *pb.SeriesRange, recvFn func(k *types.Kline) error) (err error) { + // fetch history klines via stream + req := &pb.ReqHistoryKlineStream{Series: sr} + stream, err := svc.exchangeClient.HistoryKlineStream(ctx, req, grpc.UseCompressor("snappy")) + if err != nil { + return + } + var msg *pb.RspHistoryKlineStream + recvTimes, recvTotal := 0, 0 + watch := times.NewWatch() + for { + select { + case <-ctx.Done(): + err = ctx.Err() + return + default: + } + msg, err = stream.Recv() + if err == io.EOF { + err = nil + break + } + if err != nil { + return + } + recvTimes++ + recvTotal += len(msg.Klines) + for _, k := range msg.Klines { + kline := new(types.Kline) + kline.ParsePBKline(sr.Exchange, k) + if err = recvFn(kline); err != nil { + return + } + } + } + zlog.Debugf("fetch history kline series: recv=%d, total=%d, use %s", recvTimes, recvTotal, watch.ElapsedFmt(".")) + return +} + // IndicatorSeries 获取指标实时或历史序列数据, 闭区间 -func (svc *TradingService) IndicatorSeries(indicatorName string, window uint32, sr *pb.SeriesRange) (matrix []float64, times []int64, err error) { +func (svc *TradingService) IndicatorSeries(ctx context.Context, indicatorName string, window uint32, sr *pb.SeriesRange) (matrix []float64, times []int64, err error) { // indicatorName string, exchange pb.ExchangeType, instId string, interval types.Interval, window int indicator, ok := svc.indicatorReg.IndicatorW(indicatorName) if !ok { @@ -168,42 +211,35 @@ func (svc *TradingService) IndicatorSeries(indicatorName string, window uint32, return } - // before, after, count := sr.Before, sr.After, sr.Count - // var indCtx IOffsetIndicatorContext - // // 查询实时指标数据 - // if before == 0 && after == 0 { - // klineSeries, err1 := svc.klineStore.GetKlineSeires(sr.Exchange, sr.InstId, interval) - // if err1 != nil { - // err = err1 - // return - // } - // // recover todo out of range - // indCtx = NewIndicatorContext(klineSeries) - // } - // 查询历史指标数据 - sr.Window = window - indCtx := sig.NewHistoryIndicatorContext(svc.exchangeClient) - totalK := 0 - if totalK, err = indCtx.Init(sr); err != nil { - return - } - count := totalK - int(window) + requiredSeries := int(indicator.RequiredSeries(int16(window))) + sr.WindowExtra = uint32(requiredSeries) + kSeries := sig.NewKlineSeries(sr.Exchange, sr.InstId, types.Interval(sr.Interval)) + indicatorContext := sig.NewIndicatorContext(kSeries) - matrix = make([]float64, 0, sr.Count) - times = make([]int64, 0, sr.Count) - for i := count - 1; i >= 0; i-- { - indCtx.SetOffset(int16(i)) - vector := indicator.Calculate(indCtx, int16(window)) + matrix = make([]float64, 0, 200) + times = make([]int64, 0, 200) + err = svc.fetchHistoryKlineSeries(ctx, sr, func(k *types.Kline) (err error) { + if lastTs, serial := kSeries.Update(k); !serial { + err = fmt.Errorf("kline not series: %s(%s), interval=%s, lastTs=%d", sr.InstId, sr.Exchange, interval, lastTs) + return + } + if kSeries.Length() < requiredSeries { + return + } + vector := indicator.Calculate(indicatorContext, int16(window)) matrix = append(matrix, vector) - times = append(times, indCtx.Get(0).Ts) + times = append(times, indicatorContext.Get(0).Ts) + return + }) + if err != nil { + return } return } // StrategySeries 简单策略信号测试 -// todo 去掉 HistoryIndicatorContext, 像backtest使用stream来一个算一个 -func (svc *TradingService) StrategySeries(req *pb.ReqStrategySeries, rsp *pb.RspStrategySeries) (err error) { +func (svc *TradingService) StrategySeries(ctx context.Context, req *pb.ReqStrategySeries, rsp *pb.RspStrategySeries) (err error) { // sigStrategy sigStrategy, ok := svc.strategyReg.NewSigStrategy(req.SigStrategy) if !ok { @@ -222,54 +258,31 @@ func (svc *TradingService) StrategySeries(req *pb.ReqStrategySeries, rsp *pb.Rsp return } - // klineSeries, err1 := svc.klineStore.GetKlineSeires(req.Exchange, req.InstId, interval) - // if err1 != nil { - // err = err1 - // return - // } - // recover todo out of range + requiredSeries := int(sigStrategy.RequiredSeries()) sr := req.Series - exchange := sr.Exchange - series := sig.NewKlineSeries(exchange, sr.InstId, interval) - - count, totalK := 0, 0 - indctx := sig.NewIndicatorContext(series) - _ = indctx - - indicatorContext := sig.NewHistoryIndicatorContext(svc.exchangeClient) - req.Series.Window += indicator.MaxWindow - if totalK, err = indicatorContext.Init(req.Series); err != nil { - return - } - count = totalK - indicator.MaxWindow - + sr.WindowExtra = uint32(requiredSeries) + kSeries := sig.NewKlineSeries(sr.Exchange, sr.InstId, interval) + indicatorContext := sig.NewIndicatorContext(kSeries) strategyContext := sig.NewStrategyContext(indicatorContext, svc.indicatorReg) - for i := count - 1; i >= 0; i-- { - strategyContext.SetOffset(int16(i)) + err = svc.fetchHistoryKlineSeries(ctx, sr, func(k *types.Kline) (err error) { + if lastTs, serial := kSeries.Update(k); !serial { + err = fmt.Errorf("kline not series: %s(%s), interval=%s, lastTs=%d", sr.InstId, sr.Exchange, interval, lastTs) + return + } + if kSeries.Length() < requiredSeries { + return + } sigSide := sigStrategy.Update(strategyContext) - if sigSide == types.SideLong || sigSide == types.SideShort { + if sigSide.IsValid() { side := lang.Ternary(sigSide == types.SideLong, pb.Side_BUY, pb.Side_SELL) - signalK := strategyContext.Get(0) rsp.Signal = append(rsp.Signal, side) - rsp.Times = append(rsp.Times, signalK.Ts) - { - // todo delete - win := false - if indicatorContext.GetOffset() > 0 { - nextK := indicatorContext.Get(-1) - if side == pb.Side_BUY { - win = nextK.Close.Cmp(signalK.Close) > 0 - } else { - win = nextK.Close.Cmp(signalK.Close) < 0 - } - } - rsp.Wins = append(rsp.Wins, win) - } + rsp.Times = append(rsp.Times, k.Ts) } + return + }) + if err != nil { + return } - // 信号点胜率判断 - wins := collect.Filter(rsp.Wins, func(_ int, win bool) bool { return win }) - rsp.WinRate = float64(len(wins)) / float64(len(rsp.Wins)) return } diff --git a/pkg/indicator/atr.go b/pkg/indicator/atr.go index 027c614..be30ecb 100644 --- a/pkg/indicator/atr.go +++ b/pkg/indicator/atr.go @@ -12,6 +12,10 @@ func (c *ATR) Name() string { return "atr" } +func (c *ATR) RequiredSeries(window int16) int16 { + return window + 1 +} + // Calculate 计算单根k线rsi指标 func (c *ATR) Calculate(ctx IIndicatorContext, window int16) (vector float64) { klineSeries := ctx.Series(0, int16(window)+1) diff --git a/pkg/indicator/base.go b/pkg/indicator/indicator.go similarity index 82% rename from pkg/indicator/base.go rename to pkg/indicator/indicator.go index 6172c67..4418cf1 100644 --- a/pkg/indicator/base.go +++ b/pkg/indicator/indicator.go @@ -17,7 +17,11 @@ type IIndicator interface { // IIndicator 窗口指标基础计算接口 type IWindowIndicator interface { + // Name 指标名称 Name() string + // RequiredSeries 计算窗口大小的指标值需要的K线数量 + RequiredSeries(window int16) int16 + // Calculate 计算窗口大小的指标值 Calculate(ctx IIndicatorContext, window int16) (vector float64) } diff --git a/pkg/indicator/rsi.go b/pkg/indicator/rsi.go index 3bcad19..f944a17 100644 --- a/pkg/indicator/rsi.go +++ b/pkg/indicator/rsi.go @@ -11,11 +11,14 @@ import ( type RSI struct { } -// indicator interface func (c *RSI) Name() string { return "rsi" } +func (c *RSI) RequiredSeries(window int16) int16 { + return window +} + // Calculate 计算单根k线rsi指标 func (c *RSI) Calculate(ctx IIndicatorContext, window int16) (vector float64) { // 读k线, 计算 diff --git a/pkg/indicator/sam.go b/pkg/indicator/sam.go index 800e8f6..35ff432 100644 --- a/pkg/indicator/sam.go +++ b/pkg/indicator/sam.go @@ -14,6 +14,10 @@ func (c *SMA) Name() string { return "sma" } +func (c *SMA) RequiredSeries(window int16) int16 { + return window +} + // Calculate 计算单根k线sma指标 func (c *SMA) Calculate(ctx IIndicatorContext, window int16) (vector float64) { closeSeries := ctx.Series(0, window).Close() diff --git a/pkg/strategy/cross_star.go b/pkg/strategy/cross_star.go index d490642..60d7ae7 100644 --- a/pkg/strategy/cross_star.go +++ b/pkg/strategy/cross_star.go @@ -39,8 +39,8 @@ func (s *CrossStar) Init(param StrategyParam) (err error) { // 校验参数, 并 return } -func (s *CrossStar) MaxWindow() int { - return int(1) +func (s *CrossStar) RequiredSeries() int16 { + return 1 } func (s *CrossStar) Update(ctx ISigStrategyContext) (side types.Side) { diff --git a/pkg/strategy/gold_x.go b/pkg/strategy/gold_x.go index 81b6763..c083801 100644 --- a/pkg/strategy/gold_x.go +++ b/pkg/strategy/gold_x.go @@ -41,8 +41,8 @@ func (s *GoldX) Init(param StrategyParam) (err error) { // 校验参数, 并根 return } -func (s *GoldX) MaxWindow() int { - return int(max(s.long, s.short)) +func (s *GoldX) RequiredSeries() int16 { + return max(s.long, s.short) + 1 } func (s *GoldX) Update(ctx ISigStrategyContext) (side types.Side) { diff --git a/pkg/strategy/sig_strategy.go b/pkg/strategy/sig_strategy.go index 6c7f0ff..98eb87a 100644 --- a/pkg/strategy/sig_strategy.go +++ b/pkg/strategy/sig_strategy.go @@ -10,7 +10,7 @@ import ( type ISigStrategy interface { New() ISigStrategy Meta() StrategyMeta - MaxWindow() int // 需要的最大数据窗口数, 回测时用, 若不定义则取最大窗口值 + RequiredSeries() int16 // 需要的最小数据k线数, 回测时用, 若不定义则取最大窗口值 Init(param StrategyParam) (err error) // 校验参数, 并根据参数初始化策略 Update(ctx ISigStrategyContext) (side types.Side) } diff --git a/pkg/strategy/super_trend.go b/pkg/strategy/super_trend.go index 6904d58..8f22aed 100644 --- a/pkg/strategy/super_trend.go +++ b/pkg/strategy/super_trend.go @@ -64,8 +64,8 @@ func (s *SupertrendBOSWaves) Init(param StrategyParam) (err error) { // 校验 return } -func (s *SupertrendBOSWaves) MaxWindow() int { - return int(s.atrLength + 1) +func (s *SupertrendBOSWaves) RequiredSeries() int16 { + return s.atrLength + 1 } func (s *SupertrendBOSWaves) Update(ctx ISigStrategyContext) (side types.Side) { diff --git a/pkg/trade/account.go b/pkg/trade/account.go deleted file mode 100644 index b4b63ed..0000000 --- a/pkg/trade/account.go +++ /dev/null @@ -1,14 +0,0 @@ -package trade - -import ( - "sig-pub/api/pb" - - "github.com/govalues/decimal" -) - -type IAccountHolder interface { - Exchange() pb.ExchangeType - GetValueCash() decimal.Decimal // 获取账户总市值 - GetTradeCash() decimal.Decimal // 获取可交易空闲资金 - GetOpenTrades() []int64 // 获取未平仓交易单 -} diff --git a/pkg/trade/close_strategy.go b/pkg/trade/close_strategy.go index 3973486..9b2f049 100644 --- a/pkg/trade/close_strategy.go +++ b/pkg/trade/close_strategy.go @@ -1,24 +1,84 @@ package trade +import ( + "sig-pub/pkg/types" + "sig-pub/pkg/types/decimals" +) + +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 // 固定止损 static stoploss - TakeProfitPct float64 // 固定止盈 static take profit - - TrailMinProfit float64 // 启动移动止损的最小盈利阈值(例如达到 1% 后才开始追踪) minimum profit (fraction) before trailing activates (e.g. 0.01 = 1%); - TrailingPct float64 // 移动止损百分比(例如 0.02 表示从最高价回撤 2% 时触发追踪止损) trailing stop percent (e.g. 0.02 = 2%); - ProfitRetracePct float64 // close when profit retraces more than this fraction of peak profit;基于最高利润回撤触发平仓(例如从最高利润回撤超过 30% 则平仓)。 + StopLossPct float64 `json:"stopLossPct"` // 固定止损 static stoploss + TakeProfitPct float64 `json:"takeProfitPct"` // 固定止盈 static take profit + TrailMinProfit float64 `json:"trailMinProfit"` // 启动移动止损的最小盈利阈值(例如达到 1% 后才开始追踪) minimum profit (fraction) before trailing activates (e.g. 0.01 = 1%); + TrailingPct float64 `json:"trailingPct"` // 移动止损百分比(例如 0.02 表示从最高价回撤 2% 时触发追踪止损) trailing stop percent (e.g. 0.02 = 2%); + ProfitRetracePct float64 `json:"profitRetracePct"` // close when profit retraces more than this fraction of peak profit;基于最高利润回撤触发平仓(例如从最高利润回撤超过 30% 则平仓)。 + CloseOnSideReverse bool `json:"closeOnSideReverse"` // 交易信号和持单方向相反时是否进行平仓 + Fee bool `json:"fee"` // 计算止盈止损时是否包含手续费 } +// CloseStrategy 平仓策略 type CloseStrategy struct { + CloseStrategyParam } -func NewCloseStrategy(param CloseStrategyParam) { +func NewCloseStrategy(param CloseStrategyParam) *CloseStrategy { + return &CloseStrategy{ + CloseStrategyParam: param, + } +} +// Update 当k线更新判断是否关闭仓位 +func (s *CloseStrategy) OnKline(k types.Kline, pos *Position) (closePos bool, cause Cause) { + closePrice := decimals.MustToFloat64(k.Close) + // update peak px + if pos.Side == types.SideLong && closePrice > pos.PeakPx { + pos.PeakPx = closePrice + } + if pos.Side == types.SideShort && closePrice < pos.PeakPx { + pos.PeakPx = closePrice + } + return s.OnPrice(closePrice, pos) } -// OnPriceUpdate 当价格更新判断是否关闭仓位 -func (s *CloseStrategy) OnPriceUpdate(price float64, position *string) (closePos bool) { +// OnPrice 当k线更新判断是否关闭仓位 +func (s *CloseStrategy) OnPrice(price float64, pos *Position) (closePos bool, cause Cause) { + if !pos.Side.IsValid() { + return + } + // side long: + if pos.Side == types.SideLong { + // 固定止损 + if s.StopLossPct > 0 && price <= pos.EntryPx*(1-s.StopLossPct) { + return true, CauseStoploss + } + // 固定止盈 + if s.TakeProfitPct > 0 && price >= pos.EntryPx*(1+s.TakeProfitPct) { + return true, CauseTakeprofit + } + // todo dynamic trailing + return + } + // side short: + if s.StopLossPct > 0 && price >= pos.EntryPx*(1+s.StopLossPct) { + return true, CauseStoploss + } + if s.TakeProfitPct > 0 && price <= pos.EntryPx*(1-s.TakeProfitPct) { + return true, CauseTakeprofit + } 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, CauseStoploss +} diff --git a/pkg/trade/error.go b/pkg/trade/error.go deleted file mode 100644 index 1de626e..0000000 --- a/pkg/trade/error.go +++ /dev/null @@ -1,10 +0,0 @@ -package trade - -type ErrorCode int32 - -const ( - _ ErrorCode = iota - ErrorCodeArgs // 参数有误 - ErrorCodeValue // 值有误 - ErrorCodeKline // 错误k线 -) diff --git a/pkg/trade/trade_account.go b/pkg/trade/trade_account.go new file mode 100644 index 0000000..621fa1f --- /dev/null +++ b/pkg/trade/trade_account.go @@ -0,0 +1,40 @@ +package trade + +import ( + "sig-pub/pkg/data/entity" + "sig-pub/pkg/types" + + "github.com/govalues/decimal" +) + +// sig -> close strategy +// sig -> risk strategy -> trade strategy -> tarde account +type ITradeAccount interface { + // 根据当前价格对仓位进行 mark-to-market,返回账户净值 + GetCurrentEquity() decimal.Decimal + + // 返回当前仓位的名义总敞口(绝对值) + GetCurrentExposure() decimal.Decimal + + // 获取可交易空闲资金 + GetCash() decimal.Decimal + + // 获取未平仓交易单 + ListOpenPosition() []*Position + + // 获取交易订单 + GetTradeOrder(tradeId int64) Trade + + // 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct) + CanOpen(instId string, side types.Side, qty, price float64) bool + + // 直接用市价下单(简化),qty为基础货币数量 + ApplyMarketOrder(instId string, side types.Side, qty float64, price float64, ts int64) (t *entity.TradeOrder, ok bool) + + // 将仓位进行平仓 + ClosePosition() +} + +type OkxTradeAccount struct { + ITradeAccount +} diff --git a/pkg/trade/trade_strategy.go b/pkg/trade/trade_strategy.go new file mode 100644 index 0000000..c28377f --- /dev/null +++ b/pkg/trade/trade_strategy.go @@ -0,0 +1,12 @@ +package trade + +// ITradeStrategy 下单策略 +// 根据购买信号和账户信息生成下单参数 +type ITradeStrategy interface { +} + +type TradeStrategyParam struct { +} + +type TradeStrategy struct { +} diff --git a/pkg/trade/types.go b/pkg/trade/types.go new file mode 100644 index 0000000..2acd09e --- /dev/null +++ b/pkg/trade/types.go @@ -0,0 +1,51 @@ +package trade + +import "sig-pub/pkg/types" + +// Position 持仓仓位 +type Position struct { + TradeId int64 // 交易订单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 + Fee float64 // 手续费 + FeeRate float64 // 手续费率 +} + +type Trade struct { + Id int64 // 交易id + Side types.Side // 交易方向 + Qty float64 // 交易量 + Price float64 // 开仓价格 + Fee float64 // 开仓手续费 + Ts int64 // 开仓时间 + ClosePrice float64 // 平仓价格 + CloseFee float64 // 平仓手续费 + CloseTs int64 // 平仓时间 + CloseCause string // 平仓原因 ["stoploss", "takeprofit", "trailing", "retrace", "signal"](“止损”、“止盈”、“动态跟踪”、“回撤”、“信号”) + Pnl float64 // 盈利/亏损 pnl = (t.ClosePrice-t.Price)*t.Qty - t.Fee - t.CloseFee + HoldTime string // 持仓时间 +} + +type Cause int32 + +const ( + _ Cause = iota + CauseStoploss // 固定止损 + CauseTakeprofit // 固定止盈 + CauseReverseSingal // 策略反向信号 +) + +func (c Cause) String() string { + switch c { + default: + return "" + case CauseStoploss: + return "stoploss" + case CauseTakeprofit: + return "takeprofit" + } +}