From 5fb26c17dc163dcc5cff92488f58d8b48e63a8f7 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Tue, 13 Jan 2026 10:33:02 +0800 Subject: [PATCH] feat: add AI grid trading and market regime classification - Add GridTrader interface with PlaceLimitOrder, CancelOrder, GetOrderBook - Implement GridTrader for all exchanges (Binance, Bybit, OKX, Bitget, Hyperliquid, Aster, Lighter) - Add grid engine with ATR-based boundary calculation and fund distribution - Add market regime classification documents (Chinese/English) - Add GridConfigEditor component for frontend configuration --- docs/market-regime-classification-en.md | 281 +++++++++ docs/market-regime-classification-zh.md | 281 +++++++++ kernel/engine.go | 9 +- kernel/grid_engine.go | 514 ++++++++++++++++ store/grid.go | 548 +++++++++++++++++ store/store.go | 14 + store/strategy.go | 36 ++ trader/aster_trader.go | 141 +++++ trader/auto_trader.go | 49 +- trader/auto_trader_grid.go | 579 ++++++++++++++++++ trader/binance_futures.go | 155 +++++ trader/bitget_trader.go | 131 ++++ trader/bybit_trader.go | 156 +++++ trader/hyperliquid_trader.go | 115 ++++ trader/interface.go | 92 +++ trader/lighter_trader_v2_account.go | 77 ++- trader/lighter_trader_v2_trading.go | 42 ++ trader/okx_trader.go | 152 +++++ .../components/strategy/GridConfigEditor.tsx | 424 +++++++++++++ web/src/pages/StrategyStudioPage.tsx | 94 ++- web/src/pages/TraderDashboardPage.tsx | 7 + web/src/types.ts | 36 ++ 22 files changed, 3901 insertions(+), 32 deletions(-) create mode 100644 docs/market-regime-classification-en.md create mode 100644 docs/market-regime-classification-zh.md create mode 100644 kernel/grid_engine.go create mode 100644 store/grid.go create mode 100644 trader/auto_trader_grid.go create mode 100644 web/src/components/strategy/GridConfigEditor.tsx diff --git a/docs/market-regime-classification-en.md b/docs/market-regime-classification-en.md new file mode 100644 index 00000000..ddc83c89 --- /dev/null +++ b/docs/market-regime-classification-en.md @@ -0,0 +1,281 @@ +# Market Regime Classification Framework + +> A comprehensive market state identification system for quantitative trading strategy matching + +--- + +## 1. Classification Dimensions Overview + +Market state identification requires analysis across multiple dimensions: + +| Dimension | Sub-dimensions | Description | +|-----------|---------------|-------------| +| **Trend** | Direction, Strength | Determine market movement direction and momentum | +| **Volatility** | Amplitude, Frequency | Measure price fluctuation characteristics | +| **Structure** | Pattern, Phase | Identify market structure and cycle position | + +--- + +## 2. Primary Classification (5 Categories) + +### 2.1 Classification Overview + +| Code | Name | Key Characteristics | Suitable Strategies | +|------|------|---------------------|---------------------| +| `TREND_UP` | Uptrend | Higher highs & higher lows | Trend following, Breakout | +| `TREND_DOWN` | Downtrend | Lower highs & lower lows | Trend following, Short selling | +| `RANGE` | Range-bound | Price oscillates within bounds | Grid trading, Mean reversion | +| `TRANSITION` | Transition | Uncertain directional period | Wait & watch, Small positions | +| `BREAKOUT` | Breakout | Price breaks key levels | Breakout trading | + +### 2.2 Identification Indicators + +- **ADX (Average Directional Index)**: Measures trend strength + - ADX > 25: Clear trend exists + - ADX < 20: Range-bound market +- **EMA Alignment**: Determines trend direction + - EMA20 > EMA50 > EMA200: Bullish alignment + - EMA20 < EMA50 < EMA200: Bearish alignment + +--- + +## 3. Secondary Classification (18 Sub-categories) + +### 3.1 Uptrend Sub-categories (5 Types) + +| Code | Name | Technical Features | Quantitative Indicators | +|------|------|-------------------|------------------------| +| `TU_STRONG_LOW_VOL` | Strong Uptrend · Low Vol | Steady rise, shallow pullbacks | ADX>40, ATR%<2%, Pullback<38.2% | +| `TU_STRONG_HIGH_VOL` | Strong Uptrend · High Vol | Rapid surge, high volatility | ADX>40, ATR%>4%, MACD histogram expanding | +| `TU_WEAK_CHOPPY` | Weak Uptrend · Choppy | Two steps forward, one back | ADX 20-30, RSI oscillating 50-70 | +| `TU_PARABOLIC` | Parabolic Acceleration | Exponential price increase | Price far from MA, RSI>80, Volume surge | +| `TU_EXHAUSTION` | Uptrend Exhaustion | New highs but weakening momentum | Price new high + MACD/RSI divergence | + +**Strategy Matching:** +- Strong Low Vol: Heavy trend following, pyramid adding +- Strong High Vol: Medium position, trailing stops +- Weak Choppy: Light swing trading +- Parabolic: Cautious, prepare to exit +- Exhaustion: Reduce positions, prepare for reversal + +### 3.2 Downtrend Sub-categories (5 Types) + +| Code | Name | Technical Features | Quantitative Indicators | +|------|------|-------------------|------------------------| +| `TD_STRONG_LOW_VOL` | Strong Downtrend · Low Vol | Steady decline, weak bounces | ADX>40, ATR%<2%, Bounce<38.2% | +| `TD_STRONG_HIGH_VOL` | Strong Downtrend · High Vol | Panic selling, wild swings | ADX>40, ATR%>5%, VIX spike | +| `TD_WEAK_CHOPPY` | Weak Downtrend · Choppy | Grinding lower with bounces | ADX 20-30, RSI oscillating 30-50 | +| `TD_CAPITULATION` | Capitulation | High volume crash, extreme fear | RSI<20, Volume>3x average | +| `TD_EXHAUSTION` | Downtrend Exhaustion | New lows but selling pressure fading | Price new low + MACD/RSI divergence | + +**Strategy Matching:** +- Strong Low Vol: Short trend following +- Strong High Vol: Stay flat or light hedge +- Weak Choppy: Wait for stabilization +- Capitulation: Light bottom fishing possible +- Exhaustion: Gradually build long positions + +### 3.3 Range Sub-categories (4 Types) + +| Code | Name | Technical Features | Quantitative Indicators | +|------|------|-------------------|------------------------| +| `RG_TIGHT_LOW_VOL` | Tight Range · Low Vol | Extreme contraction, coiling | BB Width<2%, ATR at new lows | +| `RG_TIGHT_HIGH_VOL` | Tight Range · High Vol | Violent swings within range | BB Width<3%, ATR%>3% | +| `RG_WIDE_LOW_VOL` | Wide Range · Low Vol | Large range, slow movement | BB Width>5%, ATR%<2% | +| `RG_WIDE_HIGH_VOL` | Wide Range · High Vol | Large range, fast movement | BB Width>5%, ATR%>3% | + +**Strategy Matching:** +- Tight Low Vol: Dense grid, wait for breakout +- Tight High Vol: Fast grid, small frequent profits +- Wide Low Vol: Sparse grid, patient holding +- Wide High Vol: Swing trading, high profit targets + +### 3.4 Transition (2 Types) + +| Code | Name | Technical Features | Quantitative Indicators | +|------|------|-------------------|------------------------| +| `TR_BOTTOM_FORMING` | Bottom Forming | Decline slowing, testing support | Price stabilizing + Volume drying up + RSI divergence | +| `TR_TOP_FORMING` | Top Forming | Rally slowing, testing resistance | Price stalling + Volume drying up + RSI divergence | + +### 3.5 Breakout (2 Types) + +| Code | Name | Technical Features | Quantitative Indicators | +|------|------|-------------------|------------------------| +| `BK_UPWARD` | Upward Breakout | Breaking resistance with volume | Price>Previous high, Volume>2x, BB breakout | +| `BK_DOWNWARD` | Downward Breakout | Breaking support with volume | Price2x, BB breakdown | + +--- + +## 4. Tertiary Classification (36 Ultra-fine Categories) + +### 4.1 Trend Phase Classification + +Uptrend lifecycle consists of 5 phases: + +| Phase Code | Name | Description | Quantitative Criteria | +|------------|------|-------------|----------------------| +| `TU_S1_INITIATION` | Uptrend Initiation | First break above MA or previous high | MACD bullish cross, Price>EMA20 | +| `TU_S2_ACCELERATION` | Uptrend Acceleration | Momentum increasing, slope steepening | MACD histogram expanding, ADX rising | +| `TU_S3_MAIN_WAVE` | Main Wave | Sustained rise, shallow pullbacks | RSI 60-80, Pullbacks hold EMA20 | +| `TU_S4_EXHAUSTION` | Uptrend Exhaustion | Slowing momentum, divergences appearing | RSI divergence, MACD divergence | +| `TU_S5_REVERSAL` | Trend Reversal | Breakdown, trend ending | Break below EMA50, MACD bearish cross | + +Downtrend phases follow same pattern: `TD_S1` through `TD_S5` + +### 4.2 Range Position Classification + +| Position Code | Name | Description | Strategy Suggestion | +|---------------|------|-------------|---------------------| +| `RG_UPPER` | Upper Range | Price near resistance | Bias toward short | +| `RG_MIDDLE` | Mid Range | Price near middle band | Neutral grid trading | +| `RG_LOWER` | Lower Range | Price near support | Bias toward long | +| `RG_SQUEEZE` | Squeeze Pattern | Highs and lows converging | Wait for direction | +| `RG_EXPAND` | Expanding Pattern | Highs and lows diverging | Boundary reversal | + +### 4.3 Volatility Grades + +| Code | Name | ATR% | BB Width | Strategy Suggestion | +|------|------|------|----------|---------------------| +| `VOL_EXTREME_LOW` | Extreme Low Vol | <1% | <1.5% | Option selling | +| `VOL_LOW` | Low Volatility | 1-2% | 1.5-2.5% | Grid / Mean reversion | +| `VOL_NORMAL` | Normal Volatility | 2-3% | 2.5-4% | Trend following | +| `VOL_HIGH` | High Volatility | 3-5% | 4-6% | Momentum / Breakout | +| `VOL_EXTREME_HIGH` | Extreme High Vol | >5% | >6% | Reduce exposure / Hedge | + +--- + +## 5. Complete State Encoding Rules + +### 5.1 Encoding Format + +``` +{Primary}_{Volatility}_{Phase}_{Position} +``` + +### 5.2 Encoding Examples + +| Full Code | Interpretation | +|-----------|----------------| +| `TU_LV_S3_M` | Uptrend_LowVol_MainWave_Middle | +| `TD_HV_S2_L` | Downtrend_HighVol_Acceleration_Lower | +| `RG_NV_SQ_U` | Range_NormalVol_Squeeze_Upper | +| `BK_HV_UP_M` | Breakout_HighVol_Upward_Middle | + +--- + +## 6. Core Identification Indicators + +### 6.1 Trend Indicators + +| Indicator | Calculation | Criteria | +|-----------|-------------|----------| +| ADX | 14-period Average Directional Index | >40 Strong, 25-40 Medium, <25 Weak/Range | +| Trend Score | Composite EMA/MACD/Price structure | -100 to +100, Positive=Bullish, Negative=Bearish | +| EMA Alignment | Relative position of EMA20/50/200 | Bullish/Bearish/Mixed alignment | + +### 6.2 Volatility Indicators + +| Indicator | Calculation | Purpose | +|-----------|-------------|---------| +| ATR Percent | ATR(14) / Current Price × 100% | Measure relative volatility | +| BB Width | (Upper - Lower) / Middle × 100% | Measure price range | +| Volatility Rank | Current vol percentile in history | Determine vol level | + +### 6.3 Momentum Indicators + +| Indicator | Calculation | Criteria | +|-----------|-------------|----------| +| RSI | 14-period Relative Strength Index | >70 Overbought, <30 Oversold, 50 Neutral | +| MACD Histogram | MACD - Signal | Positive=Bullish momentum, Negative=Bearish | +| Momentum Score | Composite RSI/MACD/Volume | Measure current momentum | + +### 6.4 Structure Indicators + +| Indicator | Description | Purpose | +|-----------|-------------|---------| +| Swing Structure | HH/HL/LH/LL sequence | Determine trend structure | +| Support/Resistance | Key price levels | Define trading range | +| Volume Profile | Volume-price relationship | Validate price action | + +--- + +## 7. Strategy Matching Matrix + +### 7.1 Regime-Strategy Mapping + +| Regime Type | Recommended Strategy | Position Size | Stop Loss | +|-------------|---------------------|---------------|-----------| +| Strong Uptrend · Low Vol | Trend following + Pyramid | 60-80% | ATR×2 | +| Strong Uptrend · High Vol | Momentum + Quick profit | 40-60% | ATR×1.5 | +| Uptrend Exhaustion | Reduce + Reversal short | 20-30% | Previous high | +| Panic Decline | Wait or light bottom fish | 10-20% | Wide stop | +| Low Vol Range | Grid trading | 50-70% | Range boundary | +| High Vol Range | Swing trading | 30-50% | ATR×2 | +| Squeeze Pattern | Wait for breakout | 10-20% | - | +| Upward Breakout | Chase + Add on pullback | 50-70% | Breakout level | +| Bottom Formation | Scale in gradually | 20-40% | New low | + +### 7.2 Grid Strategy Parameter Matching + +| Range Type | Grid Levels | Grid Spacing | Other Parameters | +|------------|-------------|--------------|------------------| +| Tight Low Vol | 30-50 levels | Small spacing | Enable Maker Only | +| Tight High Vol | 15-25 levels | Small spacing | Fast execution mode | +| Wide Low Vol | 10-20 levels | Large spacing | Patient execution | +| Wide High Vol | 15-25 levels | Large spacing | High profit targets | +| Squeeze Pattern | Pause grid | - | Wait for breakout signal | +| Upper Range | Short bias | Medium | Increase sell weight | +| Lower Range | Long bias | Medium | Increase buy weight | + +--- + +## 8. Real-time Monitoring Guidelines + +### 8.1 State Transition Triggers + +| Current State | Trigger Condition | Transitions To | +|---------------|-------------------|----------------| +| Range | Price breakout + Volume + ADX rising | Breakout | +| Uptrend | RSI divergence + Volume decline | Exhaustion | +| Downtrend | RSI divergence + Volume decline | Exhaustion | +| Breakout | Failed breakout, price returns | Range | +| Exhaustion | Confirmed reversal breakout | Opposite trend | + +### 8.2 Risk Control Rules + +| Regime State | Max Position | Risk Per Trade | Special Rules | +|--------------|--------------|----------------|---------------| +| Strong Trend | 80% | 2% | Adding allowed | +| Weak Trend | 50% | 1.5% | No adding | +| Range | 60% | 1% | Diversified holding | +| Transition | 30% | 1% | Reduce activity | +| High Volatility | 40% | 0.5% | Wide stops | + +--- + +## 9. Appendix + +### 9.1 Abbreviation Reference + +| Abbrev | Full Form | Description | +|--------|-----------|-------------| +| TU | Trend Up | Upward trend | +| TD | Trend Down | Downward trend | +| RG | Range | Range-bound market | +| TR | Transition | Trend transition | +| BK | Breakout | Breakout pattern | +| LV | Low Volatility | Low volatility regime | +| HV | High Volatility | High volatility regime | +| NV | Normal Volatility | Normal volatility regime | +| XLV | Extreme Low Vol | Extremely low volatility | +| XHV | Extreme High Vol | Extremely high volatility | + +### 9.2 Document Information + +- Version: v1.0 +- Created: January 2026 +- Applicable: Cryptocurrency, Forex, Stocks, and other financial markets + +--- + +*This document is designed for market state identification and strategy matching in quantitative trading systems* diff --git a/docs/market-regime-classification-zh.md b/docs/market-regime-classification-zh.md new file mode 100644 index 00000000..36e0092d --- /dev/null +++ b/docs/market-regime-classification-zh.md @@ -0,0 +1,281 @@ +# 市场行情精细分类体系 + +> 用于量化交易策略匹配的市场状态识别框架 + +--- + +## 一、分类维度概览 + +市场状态识别需要从多个维度进行分析: + +| 维度 | 子维度 | 说明 | +|------|--------|------| +| **趋势维度** | 方向、强度 | 判断市场运动方向和力度 | +| **波动维度** | 幅度、频率 | 衡量价格波动特征 | +| **结构维度** | 形态、阶段 | 识别市场结构和所处周期 | + +--- + +## 二、一级分类(5大类) + +### 2.1 分类总览 + +| 代码 | 名称 | 核心特征 | 适合策略 | +|------|------|----------|----------| +| `TREND_UP` | 上涨趋势 | 高点/低点持续抬升 | 趋势跟踪、突破追涨 | +| `TREND_DOWN` | 下跌趋势 | 高点/低点持续降低 | 趋势跟踪、做空策略 | +| `RANGE` | 震荡区间 | 价格在区间内波动 | 网格交易、均值回归 | +| `TRANSITION` | 趋势转换 | 方向不明确的过渡期 | 观望、小仓位试探 | +| `BREAKOUT` | 突破行情 | 价格突破关键位置 | 突破追踪策略 | + +### 2.2 识别指标 + +- **ADX(平均方向指数)**:衡量趋势强度 + - ADX > 25:存在明确趋势 + - ADX < 20:震荡市场 +- **EMA排列**:判断趋势方向 + - EMA20 > EMA50 > EMA200:多头排列 + - EMA20 < EMA50 < EMA200:空头排列 + +--- + +## 三、二级分类(18细分类) + +### 3.1 上涨趋势细分(5种) + +| 代码 | 名称 | 技术特征 | 量化指标 | +|------|------|----------|----------| +| `TU_STRONG_LOW_VOL` | 强势上涨·低波动 | 稳步上涨,回调幅度小 | ADX>40, ATR%<2%, 回调<38.2% | +| `TU_STRONG_HIGH_VOL` | 强势上涨·高波动 | 快速拉升,波动剧烈 | ADX>40, ATR%>4%, MACD柱放大 | +| `TU_WEAK_CHOPPY` | 弱势上涨·震荡 | 涨三退二,反复磨蹭 | ADX 20-30, RSI在50-70震荡 | +| `TU_PARABOLIC` | 抛物线加速 | 指数级加速上涨 | 价格远离均线, RSI>80, 成交量放大 | +| `TU_EXHAUSTION` | 上涨衰竭 | 创新高但动能减弱 | 价格新高 + MACD/RSI顶背离 | + +**策略匹配:** +- 强势低波动:重仓趋势跟踪,金字塔加仓 +- 强势高波动:中等仓位,设置移动止盈 +- 弱势震荡:轻仓波段,高抛低吸 +- 抛物线加速:谨慎追涨,准备离场 +- 上涨衰竭:减仓观望,准备反转做空 + +### 3.2 下跌趋势细分(5种) + +| 代码 | 名称 | 技术特征 | 量化指标 | +|------|------|----------|----------| +| `TD_STRONG_LOW_VOL` | 强势下跌·低波动 | 稳步下跌,反弹无力 | ADX>40, ATR%<2%, 反弹<38.2% | +| `TD_STRONG_HIGH_VOL` | 强势下跌·高波动 | 恐慌抛售,波动剧烈 | ADX>40, ATR%>5%, 恐慌指数飙升 | +| `TD_WEAK_CHOPPY` | 弱势下跌·震荡 | 跌跌涨涨,磨底过程 | ADX 20-30, RSI在30-50震荡 | +| `TD_CAPITULATION` | 恐慌投降 | 放量暴跌,情绪极端 | RSI<20, 成交量>3倍均量 | +| `TD_EXHAUSTION` | 下跌衰竭 | 创新低但卖压减弱 | 价格新低 + MACD/RSI底背离 | + +**策略匹配:** +- 强势低波动:空头趋势跟踪 +- 强势高波动:观望或轻仓对冲 +- 弱势震荡:等待企稳信号 +- 恐慌投降:极端情况可轻仓抄底 +- 下跌衰竭:逐步建立多头仓位 + +### 3.3 震荡区间细分(4种) + +| 代码 | 名称 | 技术特征 | 量化指标 | +|------|------|----------|----------| +| `RG_TIGHT_LOW_VOL` | 窄幅震荡·低波动 | 极度收敛,蓄势待发 | 布林带宽度<2%, ATR创新低 | +| `RG_TIGHT_HIGH_VOL` | 窄幅震荡·高波动 | 区间内剧烈波动 | 布林带宽度<3%, ATR%>3% | +| `RG_WIDE_LOW_VOL` | 宽幅震荡·低波动 | 大区间慢速波动 | 布林带宽度>5%, ATR%<2% | +| `RG_WIDE_HIGH_VOL` | 宽幅震荡·高波动 | 大区间快速波动 | 布林带宽度>5%, ATR%>3% | + +**策略匹配:** +- 窄幅低波动:密集网格,等待突破 +- 窄幅高波动:快速网格,小利润多次 +- 宽幅低波动:稀疏网格,耐心持有 +- 宽幅高波动:波段交易,高利润目标 + +### 3.4 转换过渡(2种) + +| 代码 | 名称 | 技术特征 | 量化指标 | +|------|------|----------|----------| +| `TR_BOTTOM_FORMING` | 底部形成中 | 下跌放缓,试探支撑 | 价格止跌 + 成交量萎缩 + RSI底背离 | +| `TR_TOP_FORMING` | 顶部形成中 | 上涨放缓,试探压力 | 价格滞涨 + 成交量萎缩 + RSI顶背离 | + +### 3.5 突破行情(2种) + +| 代码 | 名称 | 技术特征 | 量化指标 | +|------|------|----------|----------| +| `BK_UPWARD` | 向上突破 | 突破阻力位并放量 | 价格>前高, 成交量>2倍, 布林带突破 | +| `BK_DOWNWARD` | 向下突破 | 跌破支撑位并放量 | 价格<前低, 成交量>2倍, 布林带跌破 | + +--- + +## 四、三级分类(36超细分类) + +### 4.1 趋势阶段细分 + +上涨趋势生命周期分为5个阶段: + +| 阶段代码 | 名称 | 特征描述 | 量化判断标准 | +|----------|------|----------|--------------| +| `TU_S1_INITIATION` | 上涨启动期 | 首次突破均线或前高 | MACD金叉, 价格突破EMA20 | +| `TU_S2_ACCELERATION` | 上涨加速期 | 动能增强,斜率加大 | MACD柱持续增大, ADX上升 | +| `TU_S3_MAIN_WAVE` | 主升浪阶段 | 持续上涨,回调幅度浅 | RSI维持60-80, 回调不破EMA20 | +| `TU_S4_EXHAUSTION` | 上涨衰竭期 | 涨速放缓,出现背离 | RSI顶背离, MACD顶背离 | +| `TU_S5_REVERSAL` | 趋势反转期 | 破位下跌,趋势结束 | 跌破EMA50, MACD死叉 | + +下跌趋势同理,代码为 `TD_S1` 至 `TD_S5` + +### 4.2 震荡位置细分 + +| 位置代码 | 名称 | 特征描述 | 策略建议 | +|----------|------|----------|----------| +| `RG_UPPER` | 区间上沿震荡 | 价格接近阻力位 | 偏空操作为主 | +| `RG_MIDDLE` | 区间中部震荡 | 价格在中轨附近 | 双向网格交易 | +| `RG_LOWER` | 区间下沿震荡 | 价格接近支撑位 | 偏多操作为主 | +| `RG_SQUEEZE` | 收敛三角震荡 | 高低点逐渐收窄 | 等待方向选择 | +| `RG_EXPAND` | 扩散三角震荡 | 高低点逐渐扩张 | 边界反转操作 | + +### 4.3 波动率等级 + +| 代码 | 名称 | ATR百分比 | 布林带宽度 | 策略建议 | +|------|------|-----------|------------|----------| +| `VOL_EXTREME_LOW` | 极低波动 | <1% | <1.5% | 期权卖方策略 | +| `VOL_LOW` | 低波动 | 1-2% | 1.5-2.5% | 网格/均值回归 | +| `VOL_NORMAL` | 正常波动 | 2-3% | 2.5-4% | 趋势跟踪 | +| `VOL_HIGH` | 高波动 | 3-5% | 4-6% | 动量/突破 | +| `VOL_EXTREME_HIGH` | 极高波动 | >5% | >6% | 减仓/对冲 | + +--- + +## 五、完整状态编码规则 + +### 5.1 编码格式 + +``` +{一级分类}_{波动等级}_{阶段}_{位置} +``` + +### 5.2 编码示例 + +| 完整代码 | 含义解释 | +|----------|----------| +| `TU_LV_S3_M` | 上涨趋势_低波动_主升浪_中部位置 | +| `TD_HV_S2_L` | 下跌趋势_高波动_加速期_下部位置 | +| `RG_NV_SQ_U` | 震荡区间_正常波动_收敛形态_上沿位置 | +| `BK_HV_UP_M` | 突破行情_高波动_向上突破_中部位置 | + +--- + +## 六、核心识别指标 + +### 6.1 趋势指标 + +| 指标 | 计算方法 | 判断标准 | +|------|----------|----------| +| ADX | 14周期平均方向指数 | >40强趋势, 25-40中等, <25弱/震荡 | +| 趋势评分 | 综合EMA/MACD/价格结构 | -100到+100, 正数多头,负数空头 | +| EMA排列 | EMA20/50/200相对位置 | 多头排列/空头排列/混乱 | + +### 6.2 波动指标 + +| 指标 | 计算方法 | 用途 | +|------|----------|------| +| ATR百分比 | ATR(14) / 当前价格 × 100% | 衡量相对波动幅度 | +| 布林带宽度 | (上轨-下轨) / 中轨 × 100% | 衡量价格波动区间 | +| 波动率排名 | 当前波动在历史中的分位 | 判断波动率高低 | + +### 6.3 动量指标 + +| 指标 | 计算方法 | 判断标准 | +|------|----------|----------| +| RSI | 14周期相对强弱指数 | >70超买, <30超卖, 50中性 | +| MACD柱 | MACD - Signal | 正数多头动能,负数空头动能 | +| 动量评分 | 综合RSI/MACD/成交量 | 衡量当前动能强弱 | + +### 6.4 结构指标 + +| 指标 | 说明 | 用途 | +|------|------|------| +| 高低点结构 | HH/HL/LH/LL序列 | 判断趋势结构 | +| 支撑阻力位 | 关键价格水平 | 确定交易区间 | +| 成交量形态 | 量价配合关系 | 验证价格走势 | + +--- + +## 七、策略匹配矩阵 + +### 7.1 行情类型与策略对应 + +| 行情类型 | 推荐策略 | 建议仓位 | 止损设置 | +|----------|----------|----------|----------| +| 强势上涨·低波动 | 趋势跟踪+金字塔加仓 | 60-80% | ATR×2 | +| 强势上涨·高波动 | 动量突破+快速止盈 | 40-60% | ATR×1.5 | +| 上涨衰竭期 | 减仓+反转信号做空 | 20-30% | 前高 | +| 恐慌下跌 | 观望或轻仓抄底 | 10-20% | 宽止损 | +| 低波动震荡 | 网格交易 | 50-70% | 区间边界 | +| 高波动震荡 | 波段高抛低吸 | 30-50% | ATR×2 | +| 收敛等待 | 蓄势等突破 | 10-20% | - | +| 向上突破 | 追涨+回踩加仓 | 50-70% | 突破位 | +| 底部形成 | 分批建仓 | 20-40% | 新低 | + +### 7.2 网格策略参数匹配 + +| 震荡类型 | 网格层数 | 网格间距 | 其他参数 | +|----------|----------|----------|----------| +| 窄幅低波动 | 30-50层 | 小间距 | 启用Maker Only | +| 窄幅高波动 | 15-25层 | 小间距 | 快速成交模式 | +| 宽幅低波动 | 10-20层 | 大间距 | 耐心等待成交 | +| 宽幅高波动 | 15-25层 | 大间距 | 高利润目标 | +| 收敛形态 | 暂停网格 | - | 等待突破信号 | +| 区间上沿 | 偏空配置 | 中等 | 卖单权重增加 | +| 区间下沿 | 偏多配置 | 中等 | 买单权重增加 | + +--- + +## 八、实时监控建议 + +### 8.1 状态转换触发条件 + +| 当前状态 | 触发条件 | 转换到 | +|----------|----------|--------| +| 震荡区间 | 价格突破+放量+ADX上升 | 突破行情 | +| 上涨趋势 | RSI顶背离+成交量萎缩 | 上涨衰竭 | +| 下跌趋势 | RSI底背离+成交量萎缩 | 下跌衰竭 | +| 突破行情 | 突破失败回落 | 震荡区间 | +| 趋势衰竭 | 反向突破确认 | 反向趋势 | + +### 8.2 风险控制规则 + +| 行情状态 | 最大仓位 | 单笔风险 | 特殊规则 | +|----------|----------|----------|----------| +| 强趋势 | 80% | 2% | 可加仓 | +| 弱趋势 | 50% | 1.5% | 不加仓 | +| 震荡 | 60% | 1% | 分散持仓 | +| 转换期 | 30% | 1% | 减少操作 | +| 高波动 | 40% | 0.5% | 宽止损 | + +--- + +## 九、附录 + +### 9.1 缩写对照表 + +| 缩写 | 英文全称 | 中文含义 | +|------|----------|----------| +| TU | Trend Up | 上涨趋势 | +| TD | Trend Down | 下跌趋势 | +| RG | Range | 震荡区间 | +| TR | Transition | 趋势转换 | +| BK | Breakout | 突破行情 | +| LV | Low Volatility | 低波动 | +| HV | High Volatility | 高波动 | +| NV | Normal Volatility | 正常波动 | +| XLV | Extreme Low Vol | 极低波动 | +| XHV | Extreme High Vol | 极高波动 | + +### 9.2 版本信息 + +- 文档版本:v1.0 +- 创建日期:2026年1月 +- 适用范围:加密货币、外汇、股票等金融市场 + +--- + +*本文档用于量化交易系统的市场状态识别和策略匹配* diff --git a/kernel/engine.go b/kernel/engine.go index e452c730..6ce5aa52 100644 --- a/kernel/engine.go +++ b/kernel/engine.go @@ -130,7 +130,8 @@ type Context struct { // Decision AI trading decision type Decision struct { Symbol string `json:"symbol"` - Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "hold", "wait" + Action string `json:"action"` // Standard: "open_long", "open_short", "close_long", "close_short", "hold", "wait" + // Grid actions: "place_buy_limit", "place_sell_limit", "cancel_order", "cancel_all_orders", "pause_grid", "resume_grid", "adjust_grid" // Opening position parameters Leverage int `json:"leverage,omitempty"` @@ -138,6 +139,12 @@ type Decision struct { StopLoss float64 `json:"stop_loss,omitempty"` TakeProfit float64 `json:"take_profit,omitempty"` + // Grid trading parameters + Price float64 `json:"price,omitempty"` // Limit order price (for grid) + Quantity float64 `json:"quantity,omitempty"` // Order quantity (for grid) + LevelIndex int `json:"level_index,omitempty"` // Grid level index + OrderID string `json:"order_id,omitempty"` // Order ID (for cancel) + // Common parameters Confidence int `json:"confidence,omitempty"` // Confidence level (0-100) RiskUSD float64 `json:"risk_usd,omitempty"` // Maximum USD risk diff --git a/kernel/grid_engine.go b/kernel/grid_engine.go new file mode 100644 index 00000000..80e5eb28 --- /dev/null +++ b/kernel/grid_engine.go @@ -0,0 +1,514 @@ +package kernel + +import ( + "encoding/json" + "fmt" + "nofx/logger" + "nofx/market" + "nofx/mcp" + "nofx/store" + "strings" + "time" +) + +// ============================================================================ +// Grid Trading Context and Types +// ============================================================================ + +// GridLevelInfo represents a single grid level's current state +type GridLevelInfo struct { + Index int `json:"index"` // Level index (0 = lowest) + Price float64 `json:"price"` // Target price for this level + State string `json:"state"` // "empty", "pending", "filled" + Side string `json:"side"` // "buy" or "sell" + OrderID string `json:"order_id"` // Current order ID (if pending) + OrderQuantity float64 `json:"order_quantity"` // Order quantity + PositionSize float64 `json:"position_size"` // Position size (if filled) + PositionEntry float64 `json:"position_entry"` // Entry price (if filled) + AllocatedUSD float64 `json:"allocated_usd"` // USD allocated to this level + UnrealizedPnL float64 `json:"unrealized_pnl"` // Unrealized P&L (if filled) +} + +// GridContext contains all information needed for AI grid decision making +type GridContext struct { + // Basic info + Symbol string `json:"symbol"` + CurrentTime string `json:"current_time"` + CurrentPrice float64 `json:"current_price"` + + // Grid configuration + GridCount int `json:"grid_count"` + TotalInvestment float64 `json:"total_investment"` + Leverage int `json:"leverage"` + UpperPrice float64 `json:"upper_price"` + LowerPrice float64 `json:"lower_price"` + GridSpacing float64 `json:"grid_spacing"` + Distribution string `json:"distribution"` + + // Grid state + Levels []GridLevelInfo `json:"levels"` + ActiveOrderCount int `json:"active_order_count"` + FilledLevelCount int `json:"filled_level_count"` + IsPaused bool `json:"is_paused"` + + // Market data + ATR14 float64 `json:"atr14"` + BollingerUpper float64 `json:"bollinger_upper"` + BollingerMiddle float64 `json:"bollinger_middle"` + BollingerLower float64 `json:"bollinger_lower"` + BollingerWidth float64 `json:"bollinger_width"` // Percentage + EMA20 float64 `json:"ema20"` + EMA50 float64 `json:"ema50"` + EMADistance float64 `json:"ema_distance"` // Percentage + RSI14 float64 `json:"rsi14"` + MACD float64 `json:"macd"` + MACDSignal float64 `json:"macd_signal"` + MACDHistogram float64 `json:"macd_histogram"` + FundingRate float64 `json:"funding_rate"` + Volume24h float64 `json:"volume_24h"` + PriceChange1h float64 `json:"price_change_1h"` + PriceChange4h float64 `json:"price_change_4h"` + + // Account info + TotalEquity float64 `json:"total_equity"` + AvailableBalance float64 `json:"available_balance"` + CurrentPosition float64 `json:"current_position"` // Net position size + UnrealizedPnL float64 `json:"unrealized_pnl"` + + // Performance + TotalProfit float64 `json:"total_profit"` + TotalTrades int `json:"total_trades"` + WinningTrades int `json:"winning_trades"` + MaxDrawdown float64 `json:"max_drawdown"` + DailyPnL float64 `json:"daily_pnl"` +} + +// ============================================================================ +// Grid Prompt Building +// ============================================================================ + +// BuildGridSystemPrompt builds the system prompt for grid trading AI +func BuildGridSystemPrompt(config *store.GridStrategyConfig, lang string) string { + if lang == "zh" { + return buildGridSystemPromptZh(config) + } + return buildGridSystemPromptEn(config) +} + +func buildGridSystemPromptZh(config *store.GridStrategyConfig) string { + return fmt.Sprintf(`# 你是一个专业的网格交易AI + +## 角色定义 +你是一个经验丰富的网格交易专家,负责管理 %s 的网格交易策略。你的任务是: +1. 判断当前市场状态(震荡/趋势/高波动) +2. 决定是否需要调整网格或暂停交易 +3. 管理每个网格层级的订单 + +## 网格配置 +- 交易对: %s +- 网格层数: %d +- 总投资: %.2f USDT +- 杠杆: %dx +- 价格分布: %s + +## 决策规则 + +### 市场状态判断 +- **震荡市场** (适合网格): 布林带宽度 < 3%%, EMA20/50 距离 < 1%%, 价格在布林带中轨附近 +- **趋势市场** (暂停网格): 布林带宽度 > 4%%, EMA20/50 距离 > 2%%, 价格持续突破布林带 +- **高波动市场** (谨慎): ATR异常放大, 价格剧烈波动 + +### 可执行的操作 +- place_buy_limit: 在指定价格下买入限价单 +- place_sell_limit: 在指定价格下卖出限价单 +- cancel_order: 取消指定订单 +- cancel_all_orders: 取消所有订单 +- pause_grid: 暂停网格交易(趋势市场时) +- resume_grid: 恢复网格交易(震荡市场时) +- adjust_grid: 调整网格边界 +- hold: 保持当前状态不操作 + +## 输出格式 +输出JSON数组,每个决策包含: +- symbol: 交易对 +- action: 操作类型 +- price: 价格(限价单用) +- quantity: 数量 +- level_index: 网格层级索引 +- order_id: 订单ID(取消订单用) +- confidence: 置信度 0-100 +- reasoning: 决策理由 + +示例: +[ + {"symbol": "BTCUSDT", "action": "place_buy_limit", "price": 94000, "quantity": 0.01, "level_index": 2, "confidence": 85, "reasoning": "第2层价格接近,下买单"}, + {"symbol": "BTCUSDT", "action": "hold", "confidence": 90, "reasoning": "市场震荡,保持当前网格"} +] +`, config.Symbol, config.Symbol, config.GridCount, config.TotalInvestment, config.Leverage, config.Distribution) +} + +func buildGridSystemPromptEn(config *store.GridStrategyConfig) string { + return fmt.Sprintf(`# You are a Professional Grid Trading AI + +## Role Definition +You are an experienced grid trading expert managing a grid strategy for %s. Your tasks are: +1. Assess current market regime (ranging/trending/volatile) +2. Decide whether to adjust grid or pause trading +3. Manage orders at each grid level + +## Grid Configuration +- Symbol: %s +- Grid Levels: %d +- Total Investment: %.2f USDT +- Leverage: %dx +- Distribution: %s + +## Decision Rules + +### Market Regime Assessment +- **Ranging Market** (ideal for grid): Bollinger width < 3%%, EMA20/50 distance < 1%%, price near middle band +- **Trending Market** (pause grid): Bollinger width > 4%%, EMA20/50 distance > 2%%, price breaking bands +- **High Volatility** (caution): ATR spike, erratic price movement + +### Available Actions +- place_buy_limit: Place buy limit order at specified price +- place_sell_limit: Place sell limit order at specified price +- cancel_order: Cancel specific order +- cancel_all_orders: Cancel all orders +- pause_grid: Pause grid trading (in trending market) +- resume_grid: Resume grid trading (in ranging market) +- adjust_grid: Adjust grid boundaries +- hold: Maintain current state + +## Output Format +Output JSON array, each decision contains: +- symbol: Trading pair +- action: Action type +- price: Price (for limit orders) +- quantity: Quantity +- level_index: Grid level index +- order_id: Order ID (for cancel) +- confidence: Confidence 0-100 +- reasoning: Decision reason + +Example: +[ + {"symbol": "BTCUSDT", "action": "place_buy_limit", "price": 94000, "quantity": 0.01, "level_index": 2, "confidence": 85, "reasoning": "Level 2 price approaching, place buy order"}, + {"symbol": "BTCUSDT", "action": "hold", "confidence": 90, "reasoning": "Market ranging, maintain current grid"} +] +`, config.Symbol, config.Symbol, config.GridCount, config.TotalInvestment, config.Leverage, config.Distribution) +} + +// BuildGridUserPrompt builds the user prompt with current grid context +func BuildGridUserPrompt(ctx *GridContext, lang string) string { + if lang == "zh" { + return buildGridUserPromptZh(ctx) + } + return buildGridUserPromptEn(ctx) +} + +func buildGridUserPromptZh(ctx *GridContext) string { + var sb strings.Builder + + sb.WriteString(fmt.Sprintf("## 当前时间: %s\n\n", ctx.CurrentTime)) + + // Market data section + sb.WriteString("## 市场数据\n") + sb.WriteString(fmt.Sprintf("- 当前价格: $%.2f\n", ctx.CurrentPrice)) + sb.WriteString(fmt.Sprintf("- 1小时涨跌: %.2f%%\n", ctx.PriceChange1h)) + sb.WriteString(fmt.Sprintf("- 4小时涨跌: %.2f%%\n", ctx.PriceChange4h)) + sb.WriteString(fmt.Sprintf("- ATR14: $%.2f (%.2f%%)\n", ctx.ATR14, ctx.ATR14/ctx.CurrentPrice*100)) + sb.WriteString(fmt.Sprintf("- 布林带: 上轨 $%.2f, 中轨 $%.2f, 下轨 $%.2f\n", ctx.BollingerUpper, ctx.BollingerMiddle, ctx.BollingerLower)) + sb.WriteString(fmt.Sprintf("- 布林带宽度: %.2f%%\n", ctx.BollingerWidth)) + sb.WriteString(fmt.Sprintf("- EMA20: $%.2f, EMA50: $%.2f, 距离: %.2f%%\n", ctx.EMA20, ctx.EMA50, ctx.EMADistance)) + sb.WriteString(fmt.Sprintf("- RSI14: %.1f\n", ctx.RSI14)) + sb.WriteString(fmt.Sprintf("- MACD: %.4f, Signal: %.4f, Histogram: %.4f\n", ctx.MACD, ctx.MACDSignal, ctx.MACDHistogram)) + sb.WriteString(fmt.Sprintf("- 资金费率: %.4f%%\n", ctx.FundingRate*100)) + sb.WriteString("\n") + + // Account section + sb.WriteString("## 账户状态\n") + sb.WriteString(fmt.Sprintf("- 总权益: $%.2f\n", ctx.TotalEquity)) + sb.WriteString(fmt.Sprintf("- 可用余额: $%.2f\n", ctx.AvailableBalance)) + sb.WriteString(fmt.Sprintf("- 当前持仓: %.4f (净头寸)\n", ctx.CurrentPosition)) + sb.WriteString(fmt.Sprintf("- 未实现盈亏: $%.2f\n", ctx.UnrealizedPnL)) + sb.WriteString("\n") + + // Grid state section + sb.WriteString("## 网格状态\n") + sb.WriteString(fmt.Sprintf("- 网格范围: $%.2f - $%.2f\n", ctx.LowerPrice, ctx.UpperPrice)) + sb.WriteString(fmt.Sprintf("- 网格间距: $%.2f\n", ctx.GridSpacing)) + sb.WriteString(fmt.Sprintf("- 活跃订单数: %d\n", ctx.ActiveOrderCount)) + sb.WriteString(fmt.Sprintf("- 已成交层数: %d\n", ctx.FilledLevelCount)) + sb.WriteString(fmt.Sprintf("- 网格已暂停: %v\n", ctx.IsPaused)) + sb.WriteString("\n") + + // Grid levels detail + sb.WriteString("## 网格层级详情\n") + sb.WriteString("| 层级 | 价格 | 状态 | 方向 | 订单数量 | 持仓数量 | 未实现盈亏 |\n") + sb.WriteString("|------|------|------|------|----------|----------|------------|\n") + for _, level := range ctx.Levels { + sb.WriteString(fmt.Sprintf("| %d | $%.2f | %s | %s | %.4f | %.4f | $%.2f |\n", + level.Index, level.Price, level.State, level.Side, + level.OrderQuantity, level.PositionSize, level.UnrealizedPnL)) + } + sb.WriteString("\n") + + // Performance section + sb.WriteString("## 绩效统计\n") + sb.WriteString(fmt.Sprintf("- 总利润: $%.2f\n", ctx.TotalProfit)) + sb.WriteString(fmt.Sprintf("- 总交易次数: %d\n", ctx.TotalTrades)) + sb.WriteString(fmt.Sprintf("- 胜率: %.1f%%\n", float64(ctx.WinningTrades)/float64(max(ctx.TotalTrades, 1))*100)) + sb.WriteString(fmt.Sprintf("- 最大回撤: %.2f%%\n", ctx.MaxDrawdown)) + sb.WriteString(fmt.Sprintf("- 今日盈亏: $%.2f\n", ctx.DailyPnL)) + sb.WriteString("\n") + + sb.WriteString("## 请分析以上数据,做出网格交易决策\n") + sb.WriteString("输出JSON数组格式的决策列表。\n") + + return sb.String() +} + +func buildGridUserPromptEn(ctx *GridContext) string { + var sb strings.Builder + + sb.WriteString(fmt.Sprintf("## Current Time: %s\n\n", ctx.CurrentTime)) + + // Market data section + sb.WriteString("## Market Data\n") + sb.WriteString(fmt.Sprintf("- Current Price: $%.2f\n", ctx.CurrentPrice)) + sb.WriteString(fmt.Sprintf("- 1h Change: %.2f%%\n", ctx.PriceChange1h)) + sb.WriteString(fmt.Sprintf("- 4h Change: %.2f%%\n", ctx.PriceChange4h)) + sb.WriteString(fmt.Sprintf("- ATR14: $%.2f (%.2f%%)\n", ctx.ATR14, ctx.ATR14/ctx.CurrentPrice*100)) + sb.WriteString(fmt.Sprintf("- Bollinger Bands: Upper $%.2f, Middle $%.2f, Lower $%.2f\n", ctx.BollingerUpper, ctx.BollingerMiddle, ctx.BollingerLower)) + sb.WriteString(fmt.Sprintf("- Bollinger Width: %.2f%%\n", ctx.BollingerWidth)) + sb.WriteString(fmt.Sprintf("- EMA20: $%.2f, EMA50: $%.2f, Distance: %.2f%%\n", ctx.EMA20, ctx.EMA50, ctx.EMADistance)) + sb.WriteString(fmt.Sprintf("- RSI14: %.1f\n", ctx.RSI14)) + sb.WriteString(fmt.Sprintf("- MACD: %.4f, Signal: %.4f, Histogram: %.4f\n", ctx.MACD, ctx.MACDSignal, ctx.MACDHistogram)) + sb.WriteString(fmt.Sprintf("- Funding Rate: %.4f%%\n", ctx.FundingRate*100)) + sb.WriteString("\n") + + // Account section + sb.WriteString("## Account Status\n") + sb.WriteString(fmt.Sprintf("- Total Equity: $%.2f\n", ctx.TotalEquity)) + sb.WriteString(fmt.Sprintf("- Available Balance: $%.2f\n", ctx.AvailableBalance)) + sb.WriteString(fmt.Sprintf("- Current Position: %.4f (net)\n", ctx.CurrentPosition)) + sb.WriteString(fmt.Sprintf("- Unrealized PnL: $%.2f\n", ctx.UnrealizedPnL)) + sb.WriteString("\n") + + // Grid state section + sb.WriteString("## Grid Status\n") + sb.WriteString(fmt.Sprintf("- Grid Range: $%.2f - $%.2f\n", ctx.LowerPrice, ctx.UpperPrice)) + sb.WriteString(fmt.Sprintf("- Grid Spacing: $%.2f\n", ctx.GridSpacing)) + sb.WriteString(fmt.Sprintf("- Active Orders: %d\n", ctx.ActiveOrderCount)) + sb.WriteString(fmt.Sprintf("- Filled Levels: %d\n", ctx.FilledLevelCount)) + sb.WriteString(fmt.Sprintf("- Grid Paused: %v\n", ctx.IsPaused)) + sb.WriteString("\n") + + // Grid levels detail + sb.WriteString("## Grid Levels Detail\n") + sb.WriteString("| Level | Price | State | Side | Order Qty | Position | Unrealized PnL |\n") + sb.WriteString("|-------|-------|-------|------|-----------|----------|----------------|\n") + for _, level := range ctx.Levels { + sb.WriteString(fmt.Sprintf("| %d | $%.2f | %s | %s | %.4f | %.4f | $%.2f |\n", + level.Index, level.Price, level.State, level.Side, + level.OrderQuantity, level.PositionSize, level.UnrealizedPnL)) + } + sb.WriteString("\n") + + // Performance section + sb.WriteString("## Performance Stats\n") + sb.WriteString(fmt.Sprintf("- Total Profit: $%.2f\n", ctx.TotalProfit)) + sb.WriteString(fmt.Sprintf("- Total Trades: %d\n", ctx.TotalTrades)) + sb.WriteString(fmt.Sprintf("- Win Rate: %.1f%%\n", float64(ctx.WinningTrades)/float64(max(ctx.TotalTrades, 1))*100)) + sb.WriteString(fmt.Sprintf("- Max Drawdown: %.2f%%\n", ctx.MaxDrawdown)) + sb.WriteString(fmt.Sprintf("- Daily PnL: $%.2f\n", ctx.DailyPnL)) + sb.WriteString("\n") + + sb.WriteString("## Please analyze the data above and make grid trading decisions\n") + sb.WriteString("Output a JSON array of decisions.\n") + + return sb.String() +} + +// ============================================================================ +// Grid Decision Functions +// ============================================================================ + +// GetGridDecisions gets AI decisions for grid trading +func GetGridDecisions(ctx *GridContext, mcpClient mcp.AIClient, config *store.GridStrategyConfig, lang string) (*FullDecision, error) { + startTime := time.Now() + + // Build prompts + systemPrompt := BuildGridSystemPrompt(config, lang) + userPrompt := BuildGridUserPrompt(ctx, lang) + + logger.Infof("🤖 [Grid] Calling AI for grid decisions...") + + // Call AI + response, err := mcpClient.CallWithMessages(systemPrompt, userPrompt) + if err != nil { + return nil, fmt.Errorf("AI call failed: %w", err) + } + + // Parse decisions from response + decisions, err := parseGridDecisions(response, ctx.Symbol) + if err != nil { + logger.Warnf("Failed to parse grid decisions: %v", err) + // Return hold decision as fallback + decisions = []Decision{{ + Symbol: ctx.Symbol, + Action: "hold", + Confidence: 50, + Reasoning: "Failed to parse AI response, holding current state", + }} + } + + duration := time.Since(startTime).Milliseconds() + logger.Infof("⏱️ [Grid] AI call duration: %d ms, decisions: %d", duration, len(decisions)) + + // Extract chain of thought from response + cotTrace := extractCoTTrace(response) + + return &FullDecision{ + SystemPrompt: systemPrompt, + UserPrompt: userPrompt, + CoTTrace: cotTrace, + Decisions: decisions, + RawResponse: response, + AIRequestDurationMs: duration, + Timestamp: time.Now(), + }, nil +} + +// parseGridDecisions parses AI response into grid decisions +func parseGridDecisions(response string, symbol string) ([]Decision, error) { + // Try to find JSON array in response + jsonStr := extractJSONArray(response) + if jsonStr == "" { + return nil, fmt.Errorf("no JSON array found in response") + } + + var decisions []Decision + if err := json.Unmarshal([]byte(jsonStr), &decisions); err != nil { + return nil, fmt.Errorf("failed to parse JSON: %w", err) + } + + // Validate and set default symbol + for i := range decisions { + if decisions[i].Symbol == "" { + decisions[i].Symbol = symbol + } + // Validate action + if !isValidGridAction(decisions[i].Action) { + logger.Warnf("Invalid grid action: %s", decisions[i].Action) + } + } + + return decisions, nil +} + +// extractJSONArray extracts JSON array from AI response +func extractJSONArray(response string) string { + // Try to find ```json code block first + matches := reJSONFence.FindStringSubmatch(response) + if len(matches) > 1 { + return matches[1] + } + + // Try to find raw JSON array + matches = reJSONArray.FindStringSubmatch(response) + if len(matches) > 0 { + return matches[0] + } + + return "" +} + +// isValidGridAction checks if action is a valid grid action +func isValidGridAction(action string) bool { + validActions := map[string]bool{ + "place_buy_limit": true, + "place_sell_limit": true, + "cancel_order": true, + "cancel_all_orders": true, + "pause_grid": true, + "resume_grid": true, + "adjust_grid": true, + "hold": true, + // Also support standard actions for compatibility + "open_long": true, + "open_short": true, + "close_long": true, + "close_short": true, + } + return validActions[action] +} + +// ============================================================================ +// Grid Context Builder Helpers +// ============================================================================ + +// BuildGridContextFromMarketData builds grid context from market data +func BuildGridContextFromMarketData(mktData *market.Data, config *store.GridStrategyConfig) *GridContext { + ctx := &GridContext{ + Symbol: config.Symbol, + CurrentTime: time.Now().Format("2006-01-02 15:04:05"), + CurrentPrice: mktData.CurrentPrice, + + // Grid config + GridCount: config.GridCount, + TotalInvestment: config.TotalInvestment, + Leverage: config.Leverage, + Distribution: config.Distribution, + + // Market data + PriceChange1h: mktData.PriceChange1h, + PriceChange4h: mktData.PriceChange4h, + FundingRate: mktData.FundingRate, + } + + // Extract indicators from timeframe data + if mktData.TimeframeData != nil { + if tf5m, ok := mktData.TimeframeData["5m"]; ok { + if len(tf5m.BOLLUpper) > 0 { + ctx.BollingerUpper = tf5m.BOLLUpper[len(tf5m.BOLLUpper)-1] + ctx.BollingerMiddle = tf5m.BOLLMiddle[len(tf5m.BOLLMiddle)-1] + ctx.BollingerLower = tf5m.BOLLLower[len(tf5m.BOLLLower)-1] + if ctx.BollingerMiddle > 0 { + ctx.BollingerWidth = (ctx.BollingerUpper - ctx.BollingerLower) / ctx.BollingerMiddle * 100 + } + } + ctx.ATR14 = tf5m.ATR14 + if len(tf5m.RSI14Values) > 0 { + ctx.RSI14 = tf5m.RSI14Values[len(tf5m.RSI14Values)-1] + } + } + } + + // Extract longer term context + if mktData.LongerTermContext != nil { + if ctx.ATR14 == 0 { + ctx.ATR14 = mktData.LongerTermContext.ATR14 + } + ctx.EMA50 = mktData.LongerTermContext.EMA50 + } + + ctx.EMA20 = mktData.CurrentEMA20 + ctx.MACD = mktData.CurrentMACD + + // Calculate EMA distance + if ctx.EMA50 > 0 { + ctx.EMADistance = (ctx.EMA20 - ctx.EMA50) / ctx.EMA50 * 100 + } + + return ctx +} + +// Helper function for max +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/store/grid.go b/store/grid.go new file mode 100644 index 00000000..2e0af6d3 --- /dev/null +++ b/store/grid.go @@ -0,0 +1,548 @@ +package store + +import ( + "fmt" + "time" + + "gorm.io/gorm" +) + +// ==================== Grid Store Models ==================== +// These models mirror the grid package types but are defined here +// to avoid import cycles between store and grid packages. + +// GridConfigModel GORM model for grid_configs table +type GridConfigModel struct { + ID string `json:"id" gorm:"primaryKey"` + UserID string `json:"user_id" gorm:"index"` + TraderID string `json:"trader_id" gorm:"index"` + Symbol string `json:"symbol" gorm:"not null"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + + GridCount int `json:"grid_count" gorm:"default:10"` + TotalInvestment float64 `json:"total_investment" gorm:"not null"` + Leverage int `json:"leverage" gorm:"default:5"` + UpperPrice float64 `json:"upper_price"` + LowerPrice float64 `json:"lower_price"` + UseATRBounds bool `json:"use_atr_bounds" gorm:"default:true"` + ATRMultiplier float64 `json:"atr_multiplier" gorm:"default:2.0"` + Distribution string `json:"distribution" gorm:"default:gaussian"` + + MaxDrawdownPct float64 `json:"max_drawdown_pct" gorm:"default:15.0"` + StopLossPct float64 `json:"stop_loss_pct" gorm:"default:5.0"` + DailyLossLimitPct float64 `json:"daily_loss_limit_pct" gorm:"default:10"` + MaxPositionSizePct float64 `json:"max_position_size_pct" gorm:"default:30"` + + RegimeCheckInterval int `json:"regime_check_interval" gorm:"default:30"` + AutoPauseOnTrend bool `json:"auto_pause_on_trend" gorm:"default:true"` + MinRangingScore int `json:"min_ranging_score" gorm:"default:60"` + TrendResumeThreshold int `json:"trend_resume_threshold" gorm:"default:70"` + + OrderRefreshSec int `json:"order_refresh_sec" gorm:"default:300"` + UseMakerOnly bool `json:"use_maker_only" gorm:"default:true"` + SlippageTolerPct float64 `json:"slippage_toler_pct" gorm:"default:0.1"` + + AIProvider string `json:"ai_provider" gorm:"default:deepseek"` + AIModel string `json:"ai_model" gorm:"default:deepseek-chat"` + IsActive bool `json:"is_active" gorm:"default:false"` +} + +func (GridConfigModel) TableName() string { + return "grid_configs" +} + +// GridInstanceModel GORM model for grid_instances table +type GridInstanceModel struct { + ID string `json:"id" gorm:"primaryKey"` + ConfigID string `json:"config_id" gorm:"index;not null"` + Symbol string `json:"symbol" gorm:"not null"` + State string `json:"state" gorm:"not null"` + StartedAt time.Time `json:"started_at"` + StoppedAt *time.Time `json:"stopped_at,omitempty"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + + CurrentUpperPrice float64 `json:"current_upper_price"` + CurrentLowerPrice float64 `json:"current_lower_price"` + CurrentGridSpacing float64 `json:"current_grid_spacing"` + ActiveLevelCount int `json:"active_level_count"` + CurrentRegime string `json:"current_regime"` + RegimeScore int `json:"regime_score"` + LastRegimeCheck time.Time `json:"last_regime_check"` + ConsecutiveTrending int `json:"consecutive_trending"` + + TotalProfit float64 `json:"total_profit" gorm:"default:0"` + TotalFees float64 `json:"total_fees" gorm:"default:0"` + TotalTrades int `json:"total_trades" gorm:"default:0"` + WinningTrades int `json:"winning_trades" gorm:"default:0"` + MaxDrawdown float64 `json:"max_drawdown" gorm:"default:0"` + CurrentDrawdown float64 `json:"current_drawdown" gorm:"default:0"` + PeakEquity float64 `json:"peak_equity" gorm:"default:0"` + DailyProfit float64 `json:"daily_profit" gorm:"default:0"` + DailyLoss float64 `json:"daily_loss" gorm:"default:0"` + LastDailyReset time.Time `json:"last_daily_reset"` +} + +func (GridInstanceModel) TableName() string { + return "grid_instances" +} + +// GridLevelModel GORM model for grid_levels table +type GridLevelModel struct { + ID string `json:"id" gorm:"primaryKey"` + InstanceID string `json:"instance_id" gorm:"index;not null"` + LevelIndex int `json:"level_index" gorm:"not null"` + Price float64 `json:"price" gorm:"not null"` + State string `json:"state" gorm:"not null"` + Side string `json:"side"` + OrderID string `json:"order_id,omitempty"` + OrderPrice float64 `json:"order_price,omitempty"` + OrderQuantity float64 `json:"order_quantity,omitempty"` + OrderCreatedAt *time.Time `json:"order_created_at,omitempty"` + PositionSize float64 `json:"position_size,omitempty"` + PositionEntry float64 `json:"position_entry,omitempty"` + PositionOpenAt *time.Time `json:"position_open_at,omitempty"` + AllocationWeight float64 `json:"allocation_weight"` + AllocatedUSD float64 `json:"allocated_usd"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` +} + +func (GridLevelModel) TableName() string { + return "grid_levels" +} + +// GridEventModel GORM model for grid_events table +type GridEventModel struct { + ID string `json:"id" gorm:"primaryKey"` + InstanceID string `json:"instance_id" gorm:"index;not null"` + LevelID string `json:"level_id,omitempty" gorm:"index"` + EventType string `json:"event_type" gorm:"not null"` + EventTime time.Time `json:"event_time" gorm:"autoCreateTime"` + Price float64 `json:"price,omitempty"` + Quantity float64 `json:"quantity,omitempty"` + Side string `json:"side,omitempty"` + PnL float64 `json:"pnl,omitempty"` + Fee float64 `json:"fee,omitempty"` + Message string `json:"message,omitempty"` + OldRegime string `json:"old_regime,omitempty"` + NewRegime string `json:"new_regime,omitempty"` + TriggerType string `json:"trigger_type,omitempty"` + RawData string `json:"raw_data,omitempty" gorm:"type:text"` +} + +func (GridEventModel) TableName() string { + return "grid_events" +} + +// GridRegimeAssessmentModel GORM model for grid_regime_assessments table +type GridRegimeAssessmentModel struct { + ID string `json:"id" gorm:"primaryKey"` + InstanceID string `json:"instance_id" gorm:"index;not null"` + AssessedAt time.Time `json:"assessed_at" gorm:"autoCreateTime"` + Regime string `json:"regime" gorm:"not null"` + Score int `json:"score" gorm:"not null"` + Confidence float64 `json:"confidence"` + BollingerSignal int `json:"bollinger_signal"` + EMASignal int `json:"ema_signal"` + MACDSignal int `json:"macd_signal"` + VolumeSignal int `json:"volume_signal"` + OISignal int `json:"oi_signal"` + FundingSignal int `json:"funding_signal"` + CandleSignal int `json:"candle_signal"` + ATR14 float64 `json:"atr14"` + BollingerWidth float64 `json:"bollinger_width"` + EMADistance float64 `json:"ema_distance"` + CurrentPrice float64 `json:"current_price"` + AIReasoning string `json:"ai_reasoning" gorm:"type:text"` +} + +func (GridRegimeAssessmentModel) TableName() string { + return "grid_regime_assessments" +} + +// ==================== Grid Store ==================== + +// GridStore provides database operations for grid trading +type GridStore struct { + db *gorm.DB +} + +// NewGridStore creates a new grid store +func NewGridStore(db *gorm.DB) *GridStore { + return &GridStore{db: db} +} + +// InitTables initializes grid-related tables +func (s *GridStore) InitTables() error { + // For PostgreSQL with existing tables, skip AutoMigrate to avoid type conflicts + if s.db.Dialector.Name() == "postgres" { + var tableExists int64 + s.db.Raw(`SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'grid_configs'`).Scan(&tableExists) + + if tableExists > 0 { + // Tables exist, just ensure indexes + s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_grid_configs_user_id ON grid_configs(user_id)`) + s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_grid_configs_trader_id ON grid_configs(trader_id)`) + s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_grid_instances_config_id ON grid_instances(config_id)`) + s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_grid_levels_instance_id ON grid_levels(instance_id)`) + s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_grid_events_instance_id ON grid_events(instance_id)`) + s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_grid_events_level_id ON grid_events(level_id)`) + s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_grid_regime_assessments_instance_id ON grid_regime_assessments(instance_id)`) + return nil + } + } + + // AutoMigrate all grid tables + if err := s.db.AutoMigrate( + &GridConfigModel{}, + &GridInstanceModel{}, + &GridLevelModel{}, + &GridEventModel{}, + &GridRegimeAssessmentModel{}, + ); err != nil { + return fmt.Errorf("failed to migrate grid tables: %w", err) + } + + return nil +} + +// ==================== Config Operations ==================== + +// SaveGridConfig saves or updates a grid configuration +func (s *GridStore) SaveGridConfig(config *GridConfigModel) error { + config.UpdatedAt = time.Now() + if config.CreatedAt.IsZero() { + config.CreatedAt = time.Now() + } + return s.db.Save(config).Error +} + +// LoadGridConfig loads a grid configuration by ID +func (s *GridStore) LoadGridConfig(id string) (*GridConfigModel, error) { + var config GridConfigModel + err := s.db.Where("id = ?", id).First(&config).Error + if err != nil { + return nil, err + } + return &config, nil +} + +// LoadGridConfigByTrader loads a grid configuration by trader ID +func (s *GridStore) LoadGridConfigByTrader(traderID string) (*GridConfigModel, error) { + var config GridConfigModel + err := s.db.Where("trader_id = ? AND is_active = true", traderID).First(&config).Error + if err != nil { + return nil, err + } + return &config, nil +} + +// ListGridConfigs lists all grid configurations for a user +func (s *GridStore) ListGridConfigs(userID string) ([]GridConfigModel, error) { + var configs []GridConfigModel + err := s.db.Where("user_id = ?", userID).Order("created_at DESC").Find(&configs).Error + if err != nil { + return nil, err + } + return configs, nil +} + +// DeleteGridConfig deletes a grid configuration and all related data +func (s *GridStore) DeleteGridConfig(id string) error { + return s.db.Transaction(func(tx *gorm.DB) error { + // Get all instances for this config + var instances []GridInstanceModel + if err := tx.Where("config_id = ?", id).Find(&instances).Error; err != nil { + return err + } + + // Delete related data for each instance + for _, instance := range instances { + if err := tx.Where("instance_id = ?", instance.ID).Delete(&GridLevelModel{}).Error; err != nil { + return err + } + if err := tx.Where("instance_id = ?", instance.ID).Delete(&GridEventModel{}).Error; err != nil { + return err + } + if err := tx.Where("instance_id = ?", instance.ID).Delete(&GridRegimeAssessmentModel{}).Error; err != nil { + return err + } + } + + // Delete instances + if err := tx.Where("config_id = ?", id).Delete(&GridInstanceModel{}).Error; err != nil { + return err + } + + // Delete config + return tx.Where("id = ?", id).Delete(&GridConfigModel{}).Error + }) +} + +// ==================== Instance Operations ==================== + +// SaveGridInstance saves or updates a grid instance +func (s *GridStore) SaveGridInstance(instance *GridInstanceModel) error { + instance.UpdatedAt = time.Now() + return s.db.Save(instance).Error +} + +// LoadGridInstance loads a grid instance by config ID +func (s *GridStore) LoadGridInstance(configID string) (*GridInstanceModel, error) { + var instance GridInstanceModel + err := s.db.Where("config_id = ?", configID). + Order("started_at DESC"). + First(&instance).Error + if err != nil { + return nil, err + } + return &instance, nil +} + +// LoadGridInstanceByID loads a grid instance by ID +func (s *GridStore) LoadGridInstanceByID(id string) (*GridInstanceModel, error) { + var instance GridInstanceModel + err := s.db.Where("id = ?", id).First(&instance).Error + if err != nil { + return nil, err + } + return &instance, nil +} + +// ListGridInstances lists all instances for a config +func (s *GridStore) ListGridInstances(configID string) ([]GridInstanceModel, error) { + var instances []GridInstanceModel + err := s.db.Where("config_id = ?", configID). + Order("started_at DESC"). + Find(&instances).Error + if err != nil { + return nil, err + } + return instances, nil +} + +// ==================== Level Operations ==================== + +// SaveGridLevel saves or updates a grid level +func (s *GridStore) SaveGridLevel(level *GridLevelModel) error { + level.UpdatedAt = time.Now() + return s.db.Save(level).Error +} + +// SaveGridLevels saves multiple grid levels +func (s *GridStore) SaveGridLevels(levels []GridLevelModel) error { + if len(levels) == 0 { + return nil + } + now := time.Now() + for i := range levels { + levels[i].UpdatedAt = now + } + return s.db.Save(&levels).Error +} + +// LoadGridLevels loads all levels for an instance +func (s *GridStore) LoadGridLevels(instanceID string) ([]GridLevelModel, error) { + var levels []GridLevelModel + err := s.db.Where("instance_id = ?", instanceID). + Order("level_index ASC"). + Find(&levels).Error + if err != nil { + return nil, err + } + return levels, nil +} + +// DeleteGridLevels deletes all levels for an instance +func (s *GridStore) DeleteGridLevels(instanceID string) error { + return s.db.Where("instance_id = ?", instanceID).Delete(&GridLevelModel{}).Error +} + +// ==================== Event Operations ==================== + +// SaveGridEvent saves a grid event +func (s *GridStore) SaveGridEvent(event *GridEventModel) error { + if event.EventTime.IsZero() { + event.EventTime = time.Now() + } + return s.db.Create(event).Error +} + +// LoadRecentGridEvents loads recent events for an instance +func (s *GridStore) LoadRecentGridEvents(instanceID string, limit int) ([]GridEventModel, error) { + var events []GridEventModel + query := s.db.Where("instance_id = ?", instanceID). + Order("event_time DESC") + if limit > 0 { + query = query.Limit(limit) + } + err := query.Find(&events).Error + if err != nil { + return nil, err + } + return events, nil +} + +// LoadGridEventsByType loads events of a specific type +func (s *GridStore) LoadGridEventsByType(instanceID, eventType string, limit int) ([]GridEventModel, error) { + var events []GridEventModel + query := s.db.Where("instance_id = ? AND event_type = ?", instanceID, eventType). + Order("event_time DESC") + if limit > 0 { + query = query.Limit(limit) + } + err := query.Find(&events).Error + if err != nil { + return nil, err + } + return events, nil +} + +// CountGridEvents counts events for an instance +func (s *GridStore) CountGridEvents(instanceID string) (int64, error) { + var count int64 + err := s.db.Model(&GridEventModel{}). + Where("instance_id = ?", instanceID). + Count(&count).Error + return count, err +} + +// ==================== Regime Assessment Operations ==================== + +// SaveGridRegimeAssessment saves a regime assessment +func (s *GridStore) SaveGridRegimeAssessment(assessment *GridRegimeAssessmentModel) error { + if assessment.AssessedAt.IsZero() { + assessment.AssessedAt = time.Now() + } + return s.db.Create(assessment).Error +} + +// LoadLatestGridRegime loads the latest regime assessment +func (s *GridStore) LoadLatestGridRegime(instanceID string) (*GridRegimeAssessmentModel, error) { + var assessment GridRegimeAssessmentModel + err := s.db.Where("instance_id = ?", instanceID). + Order("assessed_at DESC"). + First(&assessment).Error + if err != nil { + return nil, err + } + return &assessment, nil +} + +// LoadGridRegimeHistory loads regime assessment history +func (s *GridStore) LoadGridRegimeHistory(instanceID string, limit int) ([]GridRegimeAssessmentModel, error) { + var assessments []GridRegimeAssessmentModel + query := s.db.Where("instance_id = ?", instanceID). + Order("assessed_at DESC") + if limit > 0 { + query = query.Limit(limit) + } + err := query.Find(&assessments).Error + if err != nil { + return nil, err + } + return assessments, nil +} + +// ==================== Statistics Operations ==================== + +// GetGridInstanceStatistics returns statistics for an instance +func (s *GridStore) GetGridInstanceStatistics(instanceID string) (map[string]interface{}, error) { + var instance GridInstanceModel + if err := s.db.Where("id = ?", instanceID).First(&instance).Error; err != nil { + return nil, err + } + + // Count events by type + var eventCounts []struct { + EventType string + Count int64 + } + s.db.Model(&GridEventModel{}). + Select("event_type, count(*) as count"). + Where("instance_id = ?", instanceID). + Group("event_type"). + Find(&eventCounts) + + eventCountMap := make(map[string]int64) + for _, ec := range eventCounts { + eventCountMap[ec.EventType] = ec.Count + } + + // Get latest regime + var latestRegime GridRegimeAssessmentModel + s.db.Where("instance_id = ?", instanceID). + Order("assessed_at DESC"). + First(&latestRegime) + + winRate := 0.0 + if instance.TotalTrades > 0 { + winRate = float64(instance.WinningTrades) / float64(instance.TotalTrades) * 100 + } + + return map[string]interface{}{ + "instance_id": instance.ID, + "state": instance.State, + "started_at": instance.StartedAt, + "stopped_at": instance.StoppedAt, + "total_profit": instance.TotalProfit, + "total_fees": instance.TotalFees, + "total_trades": instance.TotalTrades, + "winning_trades": instance.WinningTrades, + "win_rate": winRate, + "max_drawdown": instance.MaxDrawdown, + "current_drawdown": instance.CurrentDrawdown, + "peak_equity": instance.PeakEquity, + "active_level_count": instance.ActiveLevelCount, + "current_regime": instance.CurrentRegime, + "regime_score": instance.RegimeScore, + "event_counts": eventCountMap, + "latest_regime_score": latestRegime.Score, + }, nil +} + +// GetGridPerformanceMetrics returns performance metrics for a time period +func (s *GridStore) GetGridPerformanceMetrics(instanceID string, from, to time.Time) (map[string]interface{}, error) { + // Count trades in period + var tradeCounts struct { + TotalFills int64 + BuyFills int64 + SellFills int64 + } + s.db.Model(&GridEventModel{}). + Select("count(*) as total_fills, "+ + "sum(case when side = 'buy' then 1 else 0 end) as buy_fills, "+ + "sum(case when side = 'sell' then 1 else 0 end) as sell_fills"). + Where("instance_id = ? AND event_type = 'order_filled' AND event_time BETWEEN ? AND ?", + instanceID, from, to). + Scan(&tradeCounts) + + // Sum profit/loss + var pnlSum struct { + TotalPnL float64 + TotalFee float64 + } + s.db.Model(&GridEventModel{}). + Select("coalesce(sum(pnl), 0) as total_pnl, coalesce(sum(fee), 0) as total_fee"). + Where("instance_id = ? AND event_time BETWEEN ? AND ?", instanceID, from, to). + Scan(&pnlSum) + + // Count regime changes + var regimeChanges int64 + s.db.Model(&GridEventModel{}). + Where("instance_id = ? AND event_type = 'regime_change' AND event_time BETWEEN ? AND ?", + instanceID, from, to). + Count(®imeChanges) + + return map[string]interface{}{ + "period_start": from, + "period_end": to, + "total_fills": tradeCounts.TotalFills, + "buy_fills": tradeCounts.BuyFills, + "sell_fills": tradeCounts.SellFills, + "total_pnl": pnlSum.TotalPnL, + "total_fees": pnlSum.TotalFee, + "net_pnl": pnlSum.TotalPnL - pnlSum.TotalFee, + "regime_changes": regimeChanges, + }, nil +} diff --git a/store/store.go b/store/store.go index 21c15813..8119b935 100644 --- a/store/store.go +++ b/store/store.go @@ -28,6 +28,7 @@ type Store struct { strategy *StrategyStore equity *EquityStore order *OrderStore + grid *GridStore mu sync.RWMutex } @@ -156,6 +157,9 @@ func (s *Store) initTables() error { if err := s.Order().InitTables(); err != nil { return fmt.Errorf("failed to initialize order tables: %w", err) } + if err := s.Grid().InitTables(); err != nil { + return fmt.Errorf("failed to initialize grid tables: %w", err) + } return nil } @@ -279,6 +283,16 @@ func (s *Store) Order() *OrderStore { return s.order } +// Grid gets grid trading storage +func (s *Store) Grid() *GridStore { + s.mu.Lock() + defer s.mu.Unlock() + if s.grid == nil { + s.grid = NewGridStore(s.gdb) + } + return s.grid +} + // Close closes database connection func (s *Store) Close() error { if s.driver != nil { diff --git a/store/strategy.go b/store/strategy.go index 1b3f5b11..be009851 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -32,6 +32,9 @@ func (Strategy) TableName() string { return "strategies" } // StrategyConfig strategy configuration details (JSON structure) type StrategyConfig struct { + // Strategy type: "ai_trading" (default) or "grid_trading" + StrategyType string `json:"strategy_type,omitempty"` + // language setting: "zh" for Chinese, "en" for English // This determines the language used for data formatting and prompt generation Language string `json:"language,omitempty"` @@ -45,6 +48,39 @@ type StrategyConfig struct { RiskControl RiskControlConfig `json:"risk_control"` // editable sections of System Prompt PromptSections PromptSectionsConfig `json:"prompt_sections,omitempty"` + + // Grid trading configuration (only used when StrategyType == "grid_trading") + GridConfig *GridStrategyConfig `json:"grid_config,omitempty"` +} + +// GridStrategyConfig grid trading specific configuration +type GridStrategyConfig struct { + // Trading pair (e.g., "BTCUSDT") + Symbol string `json:"symbol"` + // Number of grid levels (5-50) + GridCount int `json:"grid_count"` + // Total investment in USDT + TotalInvestment float64 `json:"total_investment"` + // Leverage (1-20) + Leverage int `json:"leverage"` + // Upper price boundary (0 = auto-calculate from ATR) + UpperPrice float64 `json:"upper_price"` + // Lower price boundary (0 = auto-calculate from ATR) + LowerPrice float64 `json:"lower_price"` + // Use ATR to auto-calculate bounds + UseATRBounds bool `json:"use_atr_bounds"` + // ATR multiplier for bound calculation (default 2.0) + ATRMultiplier float64 `json:"atr_multiplier"` + // Position distribution: "uniform" | "gaussian" | "pyramid" + Distribution string `json:"distribution"` + // Maximum drawdown percentage before emergency exit + MaxDrawdownPct float64 `json:"max_drawdown_pct"` + // Stop loss percentage per position + StopLossPct float64 `json:"stop_loss_pct"` + // Daily loss limit percentage + DailyLossLimitPct float64 `json:"daily_loss_limit_pct"` + // Use maker-only orders for lower fees + UseMakerOnly bool `json:"use_maker_only"` } // PromptSectionsConfig editable sections of System Prompt diff --git a/trader/aster_trader.go b/trader/aster_trader.go index 2c1bbe7d..f46fb5f6 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -1420,3 +1420,144 @@ func (t *AsterTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { // TODO: Implement Aster open orders return []OpenOrder{}, nil } + +// PlaceLimitOrder places a limit order for grid trading +func (t *AsterTrader) PlaceLimitOrder(req *LimitOrderRequest) (*LimitOrderResult, error) { + // Format price and quantity to correct precision + formattedPrice, err := t.formatPrice(req.Symbol, req.Price) + if err != nil { + return nil, fmt.Errorf("failed to format price: %w", err) + } + formattedQty, err := t.formatQuantity(req.Symbol, req.Quantity) + if err != nil { + return nil, fmt.Errorf("failed to format quantity: %w", err) + } + + // Get precision information + prec, err := t.getPrecision(req.Symbol) + if err != nil { + return nil, fmt.Errorf("failed to get precision: %w", err) + } + + // Convert to string with correct precision format + priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) + qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) + + // Determine side + side := "BUY" + if req.Side == "SELL" || req.Side == "Sell" || req.Side == "sell" { + side = "SELL" + } + + params := map[string]interface{}{ + "symbol": req.Symbol, + "positionSide": "BOTH", + "type": "LIMIT", + "side": side, + "timeInForce": "GTC", + "quantity": qtyStr, + "price": priceStr, + } + + // Add reduceOnly if specified + if req.ReduceOnly { + params["reduceOnly"] = "true" + } + + body, err := t.request("POST", "/fapi/v3/order", params) + if err != nil { + return nil, fmt.Errorf("failed to place limit order: %w", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to parse order response: %w", err) + } + + // Extract order ID + orderID := "" + if id, ok := result["orderId"].(float64); ok { + orderID = fmt.Sprintf("%.0f", id) + } else if id, ok := result["orderId"].(string); ok { + orderID = id + } + + // Extract client order ID + clientOrderID := "" + if cid, ok := result["clientOrderId"].(string); ok { + clientOrderID = cid + } + + return &LimitOrderResult{ + OrderID: orderID, + ClientID: clientOrderID, + Symbol: req.Symbol, + Side: side, + Price: formattedPrice, + Quantity: formattedQty, + Status: "NEW", + }, nil +} + +// CancelOrder cancels a specific order by order ID +func (t *AsterTrader) CancelOrder(symbol, orderID string) error { + params := map[string]interface{}{ + "symbol": symbol, + "orderId": orderID, + } + + _, err := t.request("DELETE", "/fapi/v3/order", params) + if err != nil { + return fmt.Errorf("failed to cancel order %s: %w", orderID, err) + } + + return nil +} + +// GetOrderBook gets the order book for a symbol +func (t *AsterTrader) GetOrderBook(symbol string, depth int) (bids, asks [][]float64, err error) { + if depth <= 0 { + depth = 20 + } + + // Aster uses public endpoint (no signature required) + resp, err := t.client.Get(fmt.Sprintf("%s/fapi/v3/depth?symbol=%s&limit=%d", t.baseURL, symbol, depth)) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch order book: %w", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) + } + + var result struct { + Bids [][]string `json:"bids"` // [[price, qty], ...] + Asks [][]string `json:"asks"` // [[price, qty], ...] + } + if err := json.Unmarshal(body, &result); err != nil { + return nil, nil, fmt.Errorf("failed to parse order book: %w", err) + } + + // Convert string arrays to float64 arrays + bids = make([][]float64, len(result.Bids)) + for i, bid := range result.Bids { + if len(bid) >= 2 { + price, _ := strconv.ParseFloat(bid[0], 64) + qty, _ := strconv.ParseFloat(bid[1], 64) + bids[i] = []float64{price, qty} + } + } + + asks = make([][]float64, len(result.Asks)) + for i, ask := range result.Asks { + if len(ask) >= 2 { + price, _ := strconv.ParseFloat(ask[0], 64) + qty, _ := strconv.ParseFloat(ask[1], 64) + asks[i] = []float64{price, qty} + } + } + + return bids, asks, nil +} diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 2f0daee0..3ec70635 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -123,6 +123,7 @@ type AutoTrader struct { peakPnLCacheMutex sync.RWMutex // Cache read-write lock lastBalanceSyncTime time.Time // Last balance sync time userID string // User ID + gridState *GridState // Grid trading state (only used when StrategyType == "grid_trading") } // NewAutoTrader creates an automatic trader @@ -419,9 +420,25 @@ func (at *AutoTrader) Run() error { ticker := time.NewTicker(at.config.ScanInterval) defer ticker.Stop() + // Check if this is a grid trading strategy + isGridStrategy := at.IsGridStrategy() + if isGridStrategy { + logger.Infof("🔲 [%s] Grid trading strategy detected, initializing grid...", at.name) + if err := at.InitializeGrid(); err != nil { + logger.Errorf("❌ [%s] Failed to initialize grid: %v", at.name, err) + return fmt.Errorf("grid initialization failed: %w", err) + } + } + // Execute immediately on first run - if err := at.runCycle(); err != nil { - logger.Infof("❌ Execution failed: %v", err) + if isGridStrategy { + if err := at.RunGridCycle(); err != nil { + logger.Infof("❌ Grid execution failed: %v", err) + } + } else { + if err := at.runCycle(); err != nil { + logger.Infof("❌ Execution failed: %v", err) + } } for { @@ -435,8 +452,14 @@ func (at *AutoTrader) Run() error { select { case <-ticker.C: - if err := at.runCycle(); err != nil { - logger.Infof("❌ Execution failed: %v", err) + if isGridStrategy { + if err := at.RunGridCycle(); err != nil { + logger.Infof("❌ Grid execution failed: %v", err) + } + } else { + if err := at.runCycle(); err != nil { + logger.Infof("❌ Execution failed: %v", err) + } } case <-at.stopMonitorCh: logger.Infof("[%s] ⏹ Stop signal received, exiting automatic trading main loop", at.name) @@ -1365,6 +1388,12 @@ func (at *AutoTrader) GetID() string { return at.id } +// GetUnderlyingTrader returns the underlying Trader interface implementation +// This is used by grid trading and other components that need direct exchange access +func (at *AutoTrader) GetUnderlyingTrader() Trader { + return at.trader +} + // GetName gets trader name func (at *AutoTrader) GetName() string { return at.name @@ -1471,7 +1500,7 @@ func (at *AutoTrader) GetStatus() map[string]interface{} { isRunning := at.isRunning at.isRunningMutex.RUnlock() - return map[string]interface{}{ + result := map[string]interface{}{ "trader_id": at.id, "trader_name": at.name, "ai_model": at.aiModel, @@ -1486,6 +1515,16 @@ func (at *AutoTrader) GetStatus() map[string]interface{} { "last_reset_time": at.lastResetTime.Format(time.RFC3339), "ai_provider": aiProvider, } + + // Add strategy info + if at.config.StrategyConfig != nil { + result["strategy_type"] = at.config.StrategyConfig.StrategyType + if at.config.StrategyConfig.GridConfig != nil { + result["grid_symbol"] = at.config.StrategyConfig.GridConfig.Symbol + } + } + + return result } // GetAccountInfo gets account information (for API) diff --git a/trader/auto_trader_grid.go b/trader/auto_trader_grid.go new file mode 100644 index 00000000..7ed74730 --- /dev/null +++ b/trader/auto_trader_grid.go @@ -0,0 +1,579 @@ +package trader + +import ( + "encoding/json" + "fmt" + "math" + "nofx/kernel" + "nofx/logger" + "nofx/market" + "nofx/store" + "sync" + "time" +) + +// ============================================================================ +// Grid Trading State Management +// ============================================================================ + +// GridState holds the runtime state for grid trading +type GridState struct { + mu sync.RWMutex + + // Configuration + Config *store.GridStrategyConfig + + // Grid levels + Levels []kernel.GridLevelInfo + + // Calculated bounds + UpperPrice float64 + LowerPrice float64 + GridSpacing float64 + + // State flags + IsPaused bool + IsInitialized bool + + // Performance tracking + TotalProfit float64 + TotalTrades int + WinningTrades int + MaxDrawdown float64 + PeakEquity float64 + DailyPnL float64 + LastDailyReset time.Time + + // Order tracking + OrderBook map[string]int // OrderID -> LevelIndex +} + +// NewGridState creates a new grid state +func NewGridState(config *store.GridStrategyConfig) *GridState { + return &GridState{ + Config: config, + Levels: make([]kernel.GridLevelInfo, 0), + OrderBook: make(map[string]int), + } +} + +// ============================================================================ +// AutoTrader Grid Methods +// ============================================================================ + +// InitializeGrid initializes the grid state and calculates levels +func (at *AutoTrader) InitializeGrid() error { + if at.config.StrategyConfig == nil || at.config.StrategyConfig.GridConfig == nil { + return fmt.Errorf("grid configuration not found") + } + + gridConfig := at.config.StrategyConfig.GridConfig + at.gridState = NewGridState(gridConfig) + + // Get current market price + price, err := at.trader.GetMarketPrice(gridConfig.Symbol) + if err != nil { + return fmt.Errorf("failed to get market price: %w", err) + } + + // Calculate grid bounds + if gridConfig.UseATRBounds { + // Get ATR for bound calculation + mktData, err := market.GetWithTimeframes(gridConfig.Symbol, []string{"4h"}, "4h", 20) + if err != nil { + logger.Warnf("Failed to get market data for ATR: %v, using default bounds", err) + at.calculateDefaultBounds(price, gridConfig) + } else { + at.calculateATRBounds(price, mktData, gridConfig) + } + } else { + // Use manual bounds + at.gridState.UpperPrice = gridConfig.UpperPrice + at.gridState.LowerPrice = gridConfig.LowerPrice + } + + // Calculate grid spacing + at.gridState.GridSpacing = (at.gridState.UpperPrice - at.gridState.LowerPrice) / float64(gridConfig.GridCount-1) + + // Initialize grid levels + at.initializeGridLevels(price, gridConfig) + + at.gridState.IsInitialized = true + logger.Infof("📊 [Grid] Initialized: %d levels, $%.2f - $%.2f, spacing $%.2f", + gridConfig.GridCount, at.gridState.LowerPrice, at.gridState.UpperPrice, at.gridState.GridSpacing) + + return nil +} + +// calculateDefaultBounds calculates default bounds based on price +func (at *AutoTrader) calculateDefaultBounds(price float64, config *store.GridStrategyConfig) { + // Default: ±3% from current price + multiplier := 0.03 * float64(config.GridCount) / 10 + at.gridState.UpperPrice = price * (1 + multiplier) + at.gridState.LowerPrice = price * (1 - multiplier) +} + +// calculateATRBounds calculates bounds using ATR +func (at *AutoTrader) calculateATRBounds(price float64, mktData *market.Data, config *store.GridStrategyConfig) { + atr := 0.0 + if mktData.LongerTermContext != nil { + atr = mktData.LongerTermContext.ATR14 + } + + if atr <= 0 { + at.calculateDefaultBounds(price, config) + return + } + + multiplier := config.ATRMultiplier + if multiplier <= 0 { + multiplier = 2.0 + } + + halfRange := atr * multiplier + at.gridState.UpperPrice = price + halfRange + at.gridState.LowerPrice = price - halfRange +} + +// initializeGridLevels creates the grid level structure +func (at *AutoTrader) initializeGridLevels(currentPrice float64, config *store.GridStrategyConfig) { + levels := make([]kernel.GridLevelInfo, config.GridCount) + totalWeight := 0.0 + weights := make([]float64, config.GridCount) + + // Calculate weights based on distribution + for i := 0; i < config.GridCount; i++ { + switch config.Distribution { + case "gaussian": + // Gaussian distribution - more weight in the middle + center := float64(config.GridCount-1) / 2 + sigma := float64(config.GridCount) / 4 + weights[i] = math.Exp(-math.Pow(float64(i)-center, 2) / (2 * sigma * sigma)) + case "pyramid": + // Pyramid - more weight at bottom + weights[i] = float64(config.GridCount - i) + default: // uniform + weights[i] = 1.0 + } + totalWeight += weights[i] + } + + // Create levels + for i := 0; i < config.GridCount; i++ { + price := at.gridState.LowerPrice + float64(i)*at.gridState.GridSpacing + allocatedUSD := config.TotalInvestment * weights[i] / totalWeight + + // Determine initial side (below current price = buy, above = sell) + side := "buy" + if price > currentPrice { + side = "sell" + } + + levels[i] = kernel.GridLevelInfo{ + Index: i, + Price: price, + State: "empty", + Side: side, + AllocatedUSD: allocatedUSD, + } + } + + at.gridState.Levels = levels +} + +// RunGridCycle executes one grid trading cycle +func (at *AutoTrader) RunGridCycle() error { + if at.gridState == nil || !at.gridState.IsInitialized { + if err := at.InitializeGrid(); err != nil { + return fmt.Errorf("failed to initialize grid: %w", err) + } + } + + gridConfig := at.config.StrategyConfig.GridConfig + lang := at.config.StrategyConfig.Language + if lang == "" { + lang = "en" + } + + // Build grid context + gridCtx, err := at.buildGridContext() + if err != nil { + return fmt.Errorf("failed to build grid context: %w", err) + } + + // Get AI decisions + decision, err := kernel.GetGridDecisions(gridCtx, at.mcpClient, gridConfig, lang) + if err != nil { + return fmt.Errorf("failed to get grid decisions: %w", err) + } + + // Execute decisions + for _, d := range decision.Decisions { + if err := at.executeGridDecision(&d); err != nil { + logger.Warnf("[Grid] Failed to execute decision %s: %v", d.Action, err) + } + } + + // Sync state with exchange + at.syncGridState() + + // Save decision record + at.saveGridDecisionRecord(decision) + + return nil +} + +// buildGridContext builds the context for AI grid decisions +func (at *AutoTrader) buildGridContext() (*kernel.GridContext, error) { + gridConfig := at.config.StrategyConfig.GridConfig + + // Get market data + mktData, err := market.GetWithTimeframes(gridConfig.Symbol, []string{"5m", "4h"}, "5m", 50) + if err != nil { + return nil, fmt.Errorf("failed to get market data: %w", err) + } + + // Build base context from market data + ctx := kernel.BuildGridContextFromMarketData(mktData, gridConfig) + + // Add grid state + at.gridState.mu.RLock() + ctx.Levels = at.gridState.Levels + ctx.UpperPrice = at.gridState.UpperPrice + ctx.LowerPrice = at.gridState.LowerPrice + ctx.GridSpacing = at.gridState.GridSpacing + ctx.IsPaused = at.gridState.IsPaused + ctx.TotalProfit = at.gridState.TotalProfit + ctx.TotalTrades = at.gridState.TotalTrades + ctx.WinningTrades = at.gridState.WinningTrades + ctx.MaxDrawdown = at.gridState.MaxDrawdown + ctx.DailyPnL = at.gridState.DailyPnL + + // Count active orders and filled levels + for _, level := range at.gridState.Levels { + if level.State == "pending" { + ctx.ActiveOrderCount++ + } else if level.State == "filled" { + ctx.FilledLevelCount++ + } + } + at.gridState.mu.RUnlock() + + // Get account info + balance, err := at.trader.GetBalance() + if err == nil { + if equity, ok := balance["total_equity"].(float64); ok { + ctx.TotalEquity = equity + } + if available, ok := balance["availableBalance"].(float64); ok { + ctx.AvailableBalance = available + } + if unrealized, ok := balance["totalUnrealizedProfit"].(float64); ok { + ctx.UnrealizedPnL = unrealized + } + } + + // Get current position + positions, err := at.trader.GetPositions() + if err == nil { + for _, pos := range positions { + if sym, ok := pos["symbol"].(string); ok && sym == gridConfig.Symbol { + if size, ok := pos["positionAmt"].(float64); ok { + ctx.CurrentPosition = size + } + } + } + } + + return ctx, nil +} + +// executeGridDecision executes a single grid decision +func (at *AutoTrader) executeGridDecision(d *kernel.Decision) error { + switch d.Action { + case "place_buy_limit": + return at.placeGridLimitOrder(d, "BUY") + case "place_sell_limit": + return at.placeGridLimitOrder(d, "SELL") + case "cancel_order": + return at.cancelGridOrder(d) + case "cancel_all_orders": + return at.cancelAllGridOrders() + case "pause_grid": + return at.pauseGrid(d.Reasoning) + case "resume_grid": + return at.resumeGrid() + case "adjust_grid": + return at.adjustGrid(d) + case "hold": + logger.Infof("[Grid] Holding current state: %s", d.Reasoning) + return nil + // Support standard actions for closing positions + case "close_long": + _, err := at.trader.CloseLong(d.Symbol, d.Quantity) + return err + case "close_short": + _, err := at.trader.CloseShort(d.Symbol, d.Quantity) + return err + default: + logger.Warnf("[Grid] Unknown action: %s", d.Action) + return nil + } +} + +// placeGridLimitOrder places a limit order for grid trading +func (at *AutoTrader) placeGridLimitOrder(d *kernel.Decision, side string) error { + // Check if trader supports GridTrader interface + gridTrader, ok := at.trader.(GridTrader) + if !ok { + // Fallback to adapter + gridTrader = NewGridTraderAdapter(at.trader) + } + + gridConfig := at.config.StrategyConfig.GridConfig + + req := &LimitOrderRequest{ + Symbol: d.Symbol, + Side: side, + Price: d.Price, + Quantity: d.Quantity, + Leverage: gridConfig.Leverage, + PostOnly: gridConfig.UseMakerOnly, + ReduceOnly: false, + ClientID: fmt.Sprintf("grid-%d-%d", d.LevelIndex, time.Now().UnixNano()%1000000), + } + + result, err := gridTrader.PlaceLimitOrder(req) + if err != nil { + return fmt.Errorf("failed to place limit order: %w", err) + } + + // Update grid level state + at.gridState.mu.Lock() + if d.LevelIndex >= 0 && d.LevelIndex < len(at.gridState.Levels) { + at.gridState.Levels[d.LevelIndex].State = "pending" + at.gridState.Levels[d.LevelIndex].OrderID = result.OrderID + at.gridState.Levels[d.LevelIndex].OrderQuantity = d.Quantity + at.gridState.OrderBook[result.OrderID] = d.LevelIndex + } + at.gridState.mu.Unlock() + + logger.Infof("[Grid] Placed %s limit order at $%.2f, qty=%.4f, level=%d, orderID=%s", + side, d.Price, d.Quantity, d.LevelIndex, result.OrderID) + + return nil +} + +// cancelGridOrder cancels a specific grid order +func (at *AutoTrader) cancelGridOrder(d *kernel.Decision) error { + gridTrader, ok := at.trader.(GridTrader) + if !ok { + gridTrader = NewGridTraderAdapter(at.trader) + } + + if err := gridTrader.CancelOrder(d.Symbol, d.OrderID); err != nil { + return fmt.Errorf("failed to cancel order: %w", err) + } + + // Update state + at.gridState.mu.Lock() + if levelIdx, ok := at.gridState.OrderBook[d.OrderID]; ok { + if levelIdx >= 0 && levelIdx < len(at.gridState.Levels) { + at.gridState.Levels[levelIdx].State = "empty" + at.gridState.Levels[levelIdx].OrderID = "" + at.gridState.Levels[levelIdx].OrderQuantity = 0 + } + delete(at.gridState.OrderBook, d.OrderID) + } + at.gridState.mu.Unlock() + + logger.Infof("[Grid] Cancelled order: %s", d.OrderID) + return nil +} + +// cancelAllGridOrders cancels all grid orders +func (at *AutoTrader) cancelAllGridOrders() error { + gridConfig := at.config.StrategyConfig.GridConfig + + if err := at.trader.CancelAllOrders(gridConfig.Symbol); err != nil { + return fmt.Errorf("failed to cancel all orders: %w", err) + } + + // Reset all pending levels + at.gridState.mu.Lock() + for i := range at.gridState.Levels { + if at.gridState.Levels[i].State == "pending" { + at.gridState.Levels[i].State = "empty" + at.gridState.Levels[i].OrderID = "" + at.gridState.Levels[i].OrderQuantity = 0 + } + } + at.gridState.OrderBook = make(map[string]int) + at.gridState.mu.Unlock() + + logger.Infof("[Grid] Cancelled all orders") + return nil +} + +// pauseGrid pauses grid trading +func (at *AutoTrader) pauseGrid(reason string) error { + at.cancelAllGridOrders() + + at.gridState.mu.Lock() + at.gridState.IsPaused = true + at.gridState.mu.Unlock() + + logger.Infof("[Grid] Paused: %s", reason) + return nil +} + +// resumeGrid resumes grid trading +func (at *AutoTrader) resumeGrid() error { + at.gridState.mu.Lock() + at.gridState.IsPaused = false + at.gridState.mu.Unlock() + + logger.Infof("[Grid] Resumed") + return nil +} + +// adjustGrid adjusts grid parameters +func (at *AutoTrader) adjustGrid(d *kernel.Decision) error { + // Cancel existing orders first + at.cancelAllGridOrders() + + gridConfig := at.config.StrategyConfig.GridConfig + + // Get current price + price, err := at.trader.GetMarketPrice(gridConfig.Symbol) + if err != nil { + return fmt.Errorf("failed to get market price: %w", err) + } + + // Reinitialize grid levels + at.initializeGridLevels(price, gridConfig) + + logger.Infof("[Grid] Adjusted grid bounds around price $%.2f", price) + return nil +} + +// syncGridState syncs grid state with exchange +func (at *AutoTrader) syncGridState() { + gridConfig := at.config.StrategyConfig.GridConfig + + // Get open orders from exchange + openOrders, err := at.trader.GetOpenOrders(gridConfig.Symbol) + if err != nil { + logger.Warnf("[Grid] Failed to get open orders: %v", err) + return + } + + // Build set of active order IDs + activeOrderIDs := make(map[string]bool) + for _, order := range openOrders { + activeOrderIDs[order.OrderID] = true + } + + // Update levels based on order status + at.gridState.mu.Lock() + for i := range at.gridState.Levels { + level := &at.gridState.Levels[i] + if level.State == "pending" && level.OrderID != "" { + if !activeOrderIDs[level.OrderID] { + // Order no longer exists - might be filled or cancelled + // Mark as filled (we'll need to verify with position data) + level.State = "filled" + level.PositionEntry = level.Price + at.gridState.TotalTrades++ + logger.Infof("[Grid] Level %d order filled at $%.2f", i, level.Price) + } + } + } + at.gridState.mu.Unlock() + + // Update position info + positions, err := at.trader.GetPositions() + if err != nil { + return + } + + var totalPosition float64 + for _, pos := range positions { + if sym, ok := pos["symbol"].(string); ok && sym == gridConfig.Symbol { + if size, ok := pos["positionAmt"].(float64); ok { + totalPosition = size + } + if pnl, ok := pos["unRealizedProfit"].(float64); ok { + // Update unrealized PnL for filled levels + at.gridState.mu.Lock() + for i := range at.gridState.Levels { + if at.gridState.Levels[i].State == "filled" { + // Distribute PnL (simplified - in production, track per-level) + at.gridState.Levels[i].UnrealizedPnL = pnl / float64(at.gridState.TotalTrades) + } + } + at.gridState.mu.Unlock() + } + } + } + + logger.Debugf("[Grid] Synced state: position=%.4f, orders=%d", totalPosition, len(openOrders)) +} + +// saveGridDecisionRecord saves the grid decision to database +func (at *AutoTrader) saveGridDecisionRecord(decision *kernel.FullDecision) { + if at.store == nil { + return + } + + at.cycleNumber++ + + record := &store.DecisionRecord{ + TraderID: at.id, + CycleNumber: at.cycleNumber, + Timestamp: time.Now().UTC(), + SystemPrompt: decision.SystemPrompt, + InputPrompt: decision.UserPrompt, + CoTTrace: decision.CoTTrace, + RawResponse: decision.RawResponse, + AIRequestDurationMs: decision.AIRequestDurationMs, + Success: true, + } + + if len(decision.Decisions) > 0 { + decisionJSON, _ := json.MarshalIndent(decision.Decisions, "", " ") + record.DecisionJSON = string(decisionJSON) + + // Convert kernel.Decision to store.DecisionAction for frontend display + for _, d := range decision.Decisions { + actionRecord := store.DecisionAction{ + Action: d.Action, + Symbol: d.Symbol, + Quantity: d.Quantity, + Leverage: d.Leverage, + Price: d.Price, + StopLoss: d.StopLoss, + TakeProfit: d.TakeProfit, + Confidence: d.Confidence, + Reasoning: d.Reasoning, + Timestamp: time.Now().UTC(), + Success: true, // Grid decisions are executed inline + } + record.Decisions = append(record.Decisions, actionRecord) + } + } + + record.ExecutionLog = append(record.ExecutionLog, fmt.Sprintf("Grid cycle completed with %d decisions", len(decision.Decisions))) + + if err := at.store.Decision().LogDecision(record); err != nil { + logger.Warnf("[Grid] Failed to save decision record: %v", err) + } +} + +// IsGridStrategy returns true if current strategy is grid trading +func (at *AutoTrader) IsGridStrategy() bool { + if at.config.StrategyConfig == nil { + return false + } + return at.config.StrategyConfig.StrategyType == "grid_trading" && at.config.StrategyConfig.GridConfig != nil +} diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 5a54db04..a7ef6dd0 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -716,6 +716,125 @@ func (t *FuturesTrader) CancelAllOrders(symbol string) error { return nil } +// PlaceLimitOrder places a limit order for grid trading +// This implements the GridTrader interface for FuturesTrader +func (t *FuturesTrader) PlaceLimitOrder(req *LimitOrderRequest) (*LimitOrderResult, error) { + // Format quantity to correct precision + quantityStr, err := t.FormatQuantity(req.Symbol, req.Quantity) + if err != nil { + return nil, fmt.Errorf("failed to format quantity: %w", err) + } + + // Format price to correct precision + priceStr, err := t.FormatPrice(req.Symbol, req.Price) + if err != nil { + return nil, fmt.Errorf("failed to format price: %w", err) + } + + // Set leverage if specified + if req.Leverage > 0 { + if err := t.SetLeverage(req.Symbol, req.Leverage); err != nil { + logger.Warnf("Failed to set leverage: %v", err) + } + } + + // Determine side and position side + var side futures.SideType + var positionSide futures.PositionSideType + + if req.Side == "BUY" { + side = futures.SideTypeBuy + positionSide = futures.PositionSideTypeLong + } else { + side = futures.SideTypeSell + positionSide = futures.PositionSideTypeShort + } + + // Build order service with broker ID + orderService := t.client.NewCreateOrderService(). + Symbol(req.Symbol). + Side(side). + PositionSide(positionSide). + Type(futures.OrderTypeLimit). + TimeInForce(futures.TimeInForceTypeGTC). + Quantity(quantityStr). + Price(priceStr). + NewClientOrderID(getBrOrderID()) + + // Execute order + order, err := orderService.Do(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to place limit order: %w", err) + } + + logger.Infof("✓ [Grid] Placed limit order: %s %s %s @ %s, qty=%s, orderID=%d", + req.Symbol, req.Side, positionSide, priceStr, quantityStr, order.OrderID) + + return &LimitOrderResult{ + OrderID: fmt.Sprintf("%d", order.OrderID), + ClientID: order.ClientOrderID, + Symbol: order.Symbol, + Side: string(order.Side), + PositionSide: string(order.PositionSide), + Price: req.Price, + Quantity: req.Quantity, + Status: string(order.Status), + }, nil +} + +// CancelOrder cancels a specific order by ID +// This implements the GridTrader interface for FuturesTrader +func (t *FuturesTrader) CancelOrder(symbol, orderID string) error { + // Parse order ID to int64 + orderIDInt, err := strconv.ParseInt(orderID, 10, 64) + if err != nil { + return fmt.Errorf("invalid order ID: %w", err) + } + + _, err = t.client.NewCancelOrderService(). + Symbol(symbol). + OrderID(orderIDInt). + Do(context.Background()) + + if err != nil { + return fmt.Errorf("failed to cancel order: %w", err) + } + + logger.Infof("✓ [Grid] Cancelled order: %s/%s", symbol, orderID) + return nil +} + +// GetOrderBook gets the order book for a symbol +// This implements the GridTrader interface for FuturesTrader +func (t *FuturesTrader) GetOrderBook(symbol string, depth int) (bids, asks [][]float64, err error) { + book, err := t.client.NewDepthService(). + Symbol(symbol). + Limit(depth). + Do(context.Background()) + + if err != nil { + return nil, nil, fmt.Errorf("failed to get order book: %w", err) + } + + // Convert bids + bids = make([][]float64, len(book.Bids)) + for i, bid := range book.Bids { + price, _ := strconv.ParseFloat(bid.Price, 64) + qty, _ := strconv.ParseFloat(bid.Quantity, 64) + bids[i] = []float64{price, qty} + } + + // Convert asks + asks = make([][]float64, len(book.Asks)) + for i, ask := range book.Asks { + price, _ := strconv.ParseFloat(ask.Price, 64) + qty, _ := strconv.ParseFloat(ask.Quantity, 64) + asks[i] = []float64{price, qty} + } + + return bids, asks, nil +} + // CancelStopOrders cancels take-profit/stop-loss orders for this symbol (used to adjust TP/SL positions) // Now uses both legacy API and new Algo Order API (Binance migrated stop orders to Algo system) func (t *FuturesTrader) CancelStopOrders(symbol string) error { @@ -1035,6 +1154,42 @@ func (t *FuturesTrader) FormatQuantity(symbol string, quantity float64) (string, return fmt.Sprintf(format, quantity), nil } +// GetSymbolPricePrecision gets the price precision for a trading pair +func (t *FuturesTrader) GetSymbolPricePrecision(symbol string) (int, error) { + exchangeInfo, err := t.client.NewExchangeInfoService().Do(context.Background()) + if err != nil { + return 0, fmt.Errorf("failed to get trading rules: %w", err) + } + + for _, s := range exchangeInfo.Symbols { + if s.Symbol == symbol { + // Get precision from PRICE_FILTER filter + for _, filter := range s.Filters { + if filter["filterType"] == "PRICE_FILTER" { + tickSize := filter["tickSize"].(string) + precision := calculatePrecision(tickSize) + return precision, nil + } + } + } + } + + // Default to 2 decimal places for price + return 2, nil +} + +// FormatPrice formats price to correct precision +func (t *FuturesTrader) FormatPrice(symbol string, price float64) (string, error) { + precision, err := t.GetSymbolPricePrecision(symbol) + if err != nil { + // If retrieval fails, use default format + return fmt.Sprintf("%.2f", price), nil + } + + format := fmt.Sprintf("%%.%df", precision) + return fmt.Sprintf(format, price), nil +} + // Helper functions func contains(s, substr string) bool { return len(s) >= len(substr) && stringContains(s, substr) diff --git a/trader/bitget_trader.go b/trader/bitget_trader.go index 41f42f4a..d758c1d8 100644 --- a/trader/bitget_trader.go +++ b/trader/bitget_trader.go @@ -1102,3 +1102,134 @@ func (t *BitgetTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { // TODO: Implement Bitget open orders return []OpenOrder{}, nil } + +// PlaceLimitOrder places a limit order for grid trading +// Implements GridTrader interface +func (t *BitgetTrader) PlaceLimitOrder(req *LimitOrderRequest) (*LimitOrderResult, error) { + symbol := t.convertSymbol(req.Symbol) + + // Set leverage if specified + if req.Leverage > 0 { + if err := t.SetLeverage(symbol, req.Leverage); err != nil { + logger.Warnf("[Bitget] Failed to set leverage: %v", err) + } + } + + // Format quantity + qtyStr, _ := t.FormatQuantity(symbol, req.Quantity) + + // Determine side + side := "buy" + if req.Side == "SELL" { + side = "sell" + } + + body := map[string]interface{}{ + "symbol": symbol, + "productType": "USDT-FUTURES", + "marginMode": "crossed", + "marginCoin": "USDT", + "side": side, + "orderType": "limit", + "size": qtyStr, + "price": fmt.Sprintf("%.8f", req.Price), + "force": "GTC", // Good Till Cancel + "clientOid": genBitgetClientOid(), + } + + // Add reduce only if specified + if req.ReduceOnly { + body["reduceOnly"] = "YES" + } + + logger.Infof("[Bitget] PlaceLimitOrder: %s %s @ %.4f, qty=%s", symbol, side, req.Price, qtyStr) + + data, err := t.doRequest("POST", bitgetOrderPath, body) + if err != nil { + return nil, fmt.Errorf("failed to place limit order: %w", err) + } + + var order struct { + OrderId string `json:"orderId"` + ClientOid string `json:"clientOid"` + } + + if err := json.Unmarshal(data, &order); err != nil { + return nil, fmt.Errorf("failed to parse order response: %w", err) + } + + logger.Infof("✓ [Bitget] Limit order placed: %s %s @ %.4f, orderID=%s", + symbol, side, req.Price, order.OrderId) + + return &LimitOrderResult{ + OrderID: order.OrderId, + ClientID: order.ClientOid, + Symbol: req.Symbol, + Side: req.Side, + PositionSide: req.PositionSide, + Price: req.Price, + Quantity: req.Quantity, + Status: "NEW", + }, nil +} + +// CancelOrder cancels a specific order by ID +// Implements GridTrader interface +func (t *BitgetTrader) CancelOrder(symbol, orderID string) error { + symbol = t.convertSymbol(symbol) + + body := map[string]interface{}{ + "symbol": symbol, + "productType": "USDT-FUTURES", + "orderId": orderID, + } + + _, err := t.doRequest("POST", "/api/v2/mix/order/cancel-order", body) + if err != nil { + return fmt.Errorf("failed to cancel order: %w", err) + } + + logger.Infof("✓ [Bitget] Order cancelled: %s %s", symbol, orderID) + return nil +} + +// GetOrderBook gets the order book for a symbol +// Implements GridTrader interface +func (t *BitgetTrader) GetOrderBook(symbol string, depth int) (bids, asks [][]float64, err error) { + symbol = t.convertSymbol(symbol) + path := fmt.Sprintf("/api/v2/mix/market/depth?symbol=%s&productType=USDT-FUTURES&limit=%d", symbol, depth) + + data, err := t.doRequest("GET", path, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to get order book: %w", err) + } + + var result struct { + Bids [][]string `json:"bids"` + Asks [][]string `json:"asks"` + } + + if err := json.Unmarshal(data, &result); err != nil { + return nil, nil, fmt.Errorf("failed to parse order book: %w", err) + } + + // Parse bids + for _, b := range result.Bids { + if len(b) >= 2 { + price, _ := strconv.ParseFloat(b[0], 64) + qty, _ := strconv.ParseFloat(b[1], 64) + bids = append(bids, []float64{price, qty}) + } + } + + // Parse asks + for _, a := range result.Asks { + if len(a) >= 2 { + price, _ := strconv.ParseFloat(a[0], 64) + qty, _ := strconv.ParseFloat(a[1], 64) + asks = append(asks, []float64{price, qty}) + } + } + + return bids, asks, nil +} diff --git a/trader/bybit_trader.go b/trader/bybit_trader.go index d40de870..745a58e4 100644 --- a/trader/bybit_trader.go +++ b/trader/bybit_trader.go @@ -1105,3 +1105,159 @@ func (t *BybitTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { return result, nil } + +// PlaceLimitOrder places a limit order for grid trading +// Implements GridTrader interface +func (t *BybitTrader) PlaceLimitOrder(req *LimitOrderRequest) (*LimitOrderResult, error) { + // Format quantity + qtyStr, err := t.FormatQuantity(req.Symbol, req.Quantity) + if err != nil { + return nil, fmt.Errorf("failed to format quantity: %w", err) + } + + // Format price + priceStr := fmt.Sprintf("%.8f", req.Price) + + // Set leverage if specified + if req.Leverage > 0 { + if err := t.SetLeverage(req.Symbol, req.Leverage); err != nil { + logger.Warnf("[Bybit] Failed to set leverage: %v", err) + } + } + + // Determine side + side := "Buy" + if req.Side == "SELL" { + side = "Sell" + } + + params := map[string]interface{}{ + "category": "linear", + "symbol": req.Symbol, + "side": side, + "orderType": "Limit", + "qty": qtyStr, + "price": priceStr, + "timeInForce": "GTC", // Good Till Cancel + "positionIdx": 0, // One-way position mode + } + + // Add reduce only if specified + if req.ReduceOnly { + params["reduceOnly"] = true + } + + logger.Infof("[Bybit] PlaceLimitOrder: %s %s @ %s, qty=%s", req.Symbol, side, priceStr, qtyStr) + + result, err := t.client.NewUtaBybitServiceWithParams(params).PlaceOrder(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to place limit order: %w", err) + } + + // Parse result + orderID := "" + if result.RetCode == 0 { + if resultData, ok := result.Result.(map[string]interface{}); ok { + if id, ok := resultData["orderId"].(string); ok { + orderID = id + } + } + } else { + return nil, fmt.Errorf("Bybit order failed: %s", result.RetMsg) + } + + logger.Infof("✓ [Bybit] Limit order placed: %s %s @ %s, qty=%s, orderID=%s", + req.Symbol, side, priceStr, qtyStr, orderID) + + return &LimitOrderResult{ + OrderID: orderID, + ClientID: req.ClientID, + Symbol: req.Symbol, + Side: req.Side, + PositionSide: req.PositionSide, + Price: req.Price, + Quantity: req.Quantity, + Status: "NEW", + }, nil +} + +// CancelOrder cancels a specific order by ID +// Implements GridTrader interface +func (t *BybitTrader) CancelOrder(symbol, orderID string) error { + params := map[string]interface{}{ + "category": "linear", + "symbol": symbol, + "orderId": orderID, + } + + result, err := t.client.NewUtaBybitServiceWithParams(params).CancelOrder(context.Background()) + if err != nil { + return fmt.Errorf("failed to cancel order: %w", err) + } + + if result.RetCode != 0 { + return fmt.Errorf("Bybit cancel order failed: %s", result.RetMsg) + } + + logger.Infof("✓ [Bybit] Order cancelled: %s %s", symbol, orderID) + return nil +} + +// GetOrderBook gets the order book for a symbol +// Implements GridTrader interface +func (t *BybitTrader) GetOrderBook(symbol string, depth int) (bids, asks [][]float64, err error) { + if depth <= 0 { + depth = 25 + } + + // Use HTTP request directly since the SDK doesn't expose GetOrderbook + url := fmt.Sprintf("https://api.bybit.com/v5/market/orderbook?category=linear&symbol=%s&limit=%d", symbol, depth) + resp, err := http.Get(url) + if err != nil { + return nil, nil, fmt.Errorf("failed to get order book: %w", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) + } + + var result struct { + RetCode int `json:"retCode"` + RetMsg string `json:"retMsg"` + Result struct { + S string `json:"s"` // symbol + B [][]string `json:"b"` // bids [[price, size], ...] + A [][]string `json:"a"` // asks [[price, size], ...] + } `json:"result"` + } + + if err := json.Unmarshal(body, &result); err != nil { + return nil, nil, fmt.Errorf("failed to parse order book: %w", err) + } + + if result.RetCode != 0 { + return nil, nil, fmt.Errorf("Bybit get orderbook failed: %s", result.RetMsg) + } + + // Parse bids + for _, b := range result.Result.B { + if len(b) >= 2 { + price, _ := strconv.ParseFloat(b[0], 64) + qty, _ := strconv.ParseFloat(b[1], 64) + bids = append(bids, []float64{price, qty}) + } + } + + // Parse asks + for _, a := range result.Result.A { + if len(a) >= 2 { + price, _ := strconv.ParseFloat(a[0], 64) + qty, _ := strconv.ParseFloat(a[1], 64) + asks = append(asks, []float64{price, qty}) + } + } + + return bids, asks, nil +} diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index 354acd99..aa89f39e 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -2114,3 +2114,118 @@ func (t *HyperliquidTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { return result, nil } + +// PlaceLimitOrder places a limit order for grid trading +// Implements GridTrader interface +func (t *HyperliquidTrader) PlaceLimitOrder(req *LimitOrderRequest) (*LimitOrderResult, error) { + coin := convertSymbolToHyperliquid(req.Symbol) + + // Set leverage if specified and not xyz dex + isXyz := strings.HasPrefix(coin, "xyz:") + if req.Leverage > 0 && !isXyz { + if err := t.SetLeverage(req.Symbol, req.Leverage); err != nil { + logger.Warnf("[Hyperliquid] Failed to set leverage: %v", err) + } + } + + // Round quantity to allowed decimals + roundedQuantity := t.roundToSzDecimals(coin, req.Quantity) + + // Round price to 5 significant figures + roundedPrice := t.roundPriceToSigfigs(req.Price) + + // Determine if buy or sell + isBuy := req.Side == "BUY" + + logger.Infof("[Hyperliquid] PlaceLimitOrder: %s %s @ %.4f, qty=%.4f", coin, req.Side, roundedPrice, roundedQuantity) + + order := hyperliquid.CreateOrderRequest{ + Coin: coin, + IsBuy: isBuy, + Size: roundedQuantity, + Price: roundedPrice, + OrderType: hyperliquid.OrderType{ + Limit: &hyperliquid.LimitOrderType{ + Tif: hyperliquid.TifGtc, // Good Till Cancel for grid orders + }, + }, + ReduceOnly: req.ReduceOnly, + } + + _, err := t.exchange.Order(t.ctx, order, defaultBuilder) + if err != nil { + return nil, fmt.Errorf("failed to place limit order: %w", err) + } + + // Note: Hyperliquid's Order response doesn't return the order ID directly + // We would need to query open orders to get it, but for grid trading + // we can track orders by price level instead + orderID := fmt.Sprintf("%d", time.Now().UnixNano()) + + logger.Infof("✓ [Hyperliquid] Limit order placed: %s %s @ %.4f", + coin, req.Side, roundedPrice) + + return &LimitOrderResult{ + OrderID: orderID, + ClientID: req.ClientID, + Symbol: req.Symbol, + Side: req.Side, + PositionSide: req.PositionSide, + Price: roundedPrice, + Quantity: roundedQuantity, + Status: "NEW", + }, nil +} + +// CancelOrder cancels a specific order by ID +// Implements GridTrader interface +func (t *HyperliquidTrader) CancelOrder(symbol, orderID string) error { + coin := convertSymbolToHyperliquid(symbol) + + // Parse order ID + oid, err := strconv.ParseInt(orderID, 10, 64) + if err != nil { + return fmt.Errorf("invalid order ID: %w", err) + } + + _, err = t.exchange.Cancel(t.ctx, coin, oid) + if err != nil { + return fmt.Errorf("failed to cancel order: %w", err) + } + + logger.Infof("✓ [Hyperliquid] Order cancelled: %s %s", symbol, orderID) + return nil +} + +// GetOrderBook gets the order book for a symbol +// Implements GridTrader interface +func (t *HyperliquidTrader) GetOrderBook(symbol string, depth int) (bids, asks [][]float64, err error) { + coin := convertSymbolToHyperliquid(symbol) + + l2Book, err := t.exchange.Info().L2Snapshot(t.ctx, coin) + if err != nil { + return nil, nil, fmt.Errorf("failed to get order book: %w", err) + } + + if l2Book == nil || len(l2Book.Levels) < 2 { + return nil, nil, fmt.Errorf("invalid order book data") + } + + // Parse bids (first level array) + for i, level := range l2Book.Levels[0] { + if i >= depth { + break + } + bids = append(bids, []float64{level.Px, level.Sz}) + } + + // Parse asks (second level array) + for i, level := range l2Book.Levels[1] { + if i >= depth { + break + } + asks = append(asks, []float64{level.Px, level.Sz}) + } + + return bids, asks, nil +} diff --git a/trader/interface.go b/trader/interface.go index 35618633..74eb6498 100644 --- a/trader/interface.go +++ b/trader/interface.go @@ -112,3 +112,95 @@ type OpenOrder struct { Quantity float64 `json:"quantity"` Status string `json:"status"` // NEW } + +// LimitOrderRequest represents a limit order request for grid trading +type LimitOrderRequest struct { + Symbol string `json:"symbol"` + Side string `json:"side"` // BUY/SELL + PositionSide string `json:"position_side"` // LONG/SHORT (for hedge mode) + Price float64 `json:"price"` // Limit price + Quantity float64 `json:"quantity"` + Leverage int `json:"leverage"` + PostOnly bool `json:"post_only"` // Maker only order + ReduceOnly bool `json:"reduce_only"` // Reduce position only + ClientID string `json:"client_id"` // Client order ID for tracking +} + +// LimitOrderResult represents the result of placing a limit order +type LimitOrderResult struct { + OrderID string `json:"order_id"` + ClientID string `json:"client_id"` + Symbol string `json:"symbol"` + Side string `json:"side"` + PositionSide string `json:"position_side"` + Price float64 `json:"price"` + Quantity float64 `json:"quantity"` + Status string `json:"status"` // NEW, PARTIALLY_FILLED, FILLED, CANCELED +} + +// GridTrader extends Trader interface with limit order support for grid trading +// Exchanges that support grid trading should implement this interface +type GridTrader interface { + Trader + + // PlaceLimitOrder places a limit order at specified price + // Returns order ID and status + PlaceLimitOrder(req *LimitOrderRequest) (*LimitOrderResult, error) + + // CancelOrder cancels a specific order by ID + CancelOrder(symbol, orderID string) error + + // GetOrderBook gets current order book (for price validation) + // Returns best bid/ask prices + GetOrderBook(symbol string, depth int) (bids, asks [][]float64, err error) +} + +// GridTraderAdapter wraps a basic Trader to provide GridTrader interface +// Uses stop orders as a fallback when limit orders aren't directly available +type GridTraderAdapter struct { + Trader +} + +// NewGridTraderAdapter creates an adapter for basic Trader +func NewGridTraderAdapter(t Trader) *GridTraderAdapter { + return &GridTraderAdapter{Trader: t} +} + +// PlaceLimitOrder implements limit order using available methods +// For exchanges without native limit order support, this uses conditional orders +func (a *GridTraderAdapter) PlaceLimitOrder(req *LimitOrderRequest) (*LimitOrderResult, error) { + // Use SetStopLoss/SetTakeProfit as conditional limit orders + // For buy orders below current price, use stop-loss mechanism + // For sell orders above current price, use take-profit mechanism + var err error + if req.Side == "BUY" { + err = a.Trader.SetStopLoss(req.Symbol, "SHORT", req.Quantity, req.Price) + } else { + err = a.Trader.SetTakeProfit(req.Symbol, "LONG", req.Quantity, req.Price) + } + if err != nil { + return nil, err + } + return &LimitOrderResult{ + OrderID: req.ClientID, + ClientID: req.ClientID, + Symbol: req.Symbol, + Side: req.Side, + PositionSide: req.PositionSide, + Price: req.Price, + Quantity: req.Quantity, + Status: "NEW", + }, nil +} + +// CancelOrder cancels a specific order +func (a *GridTraderAdapter) CancelOrder(symbol, orderID string) error { + // Fallback: cancel all orders for the symbol + return a.Trader.CancelAllOrders(symbol) +} + +// GetOrderBook returns empty order book (not supported in basic Trader) +func (a *GridTraderAdapter) GetOrderBook(symbol string, depth int) (bids, asks [][]float64, err error) { + // Not supported, return empty + return nil, nil, nil +} diff --git a/trader/lighter_trader_v2_account.go b/trader/lighter_trader_v2_account.go index b9c84c18..65035192 100644 --- a/trader/lighter_trader_v2_account.go +++ b/trader/lighter_trader_v2_account.go @@ -328,12 +328,13 @@ func (t *LighterTraderV2) FormatQuantity(symbol string, quantity float64) (strin return fmt.Sprintf("%.4f", quantity), nil } -// GetOrderBook Get order book with best bid/ask prices -func (t *LighterTraderV2) GetOrderBook(symbol string) (bestBid, bestAsk float64, err error) { +// GetOrderBook Get order book (implements GridTrader interface) +// Returns bids and asks as [][]float64 where each element is [price, quantity] +func (t *LighterTraderV2) GetOrderBook(symbol string, depth int) (bids, asks [][]float64, err error) { // Get market_id first marketID, err := t.getMarketIndex(symbol) if err != nil { - return 0, 0, fmt.Errorf("failed to get market ID: %w", err) + return nil, nil, fmt.Errorf("failed to get market ID: %w", err) } // Get order book from Lighter API @@ -341,22 +342,22 @@ func (t *LighterTraderV2) GetOrderBook(symbol string) (bestBid, bestAsk float64, req, err := http.NewRequest("GET", endpoint, nil) if err != nil { - return 0, 0, err + return nil, nil, err } resp, err := t.client.Do(req) if err != nil { - return 0, 0, err + return nil, nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return 0, 0, err + return nil, nil, err } if resp.StatusCode != http.StatusOK { - return 0, 0, fmt.Errorf("failed to get order book (status %d): %s", resp.StatusCode, string(body)) + return nil, nil, fmt.Errorf("failed to get order book (status %d): %s", resp.StatusCode, string(body)) } // Parse response @@ -369,35 +370,61 @@ func (t *LighterTraderV2) GetOrderBook(symbol string) (bestBid, bestAsk float64, } if err := json.Unmarshal(body, &apiResp); err != nil { - return 0, 0, fmt.Errorf("failed to parse order book: %w", err) + return nil, nil, fmt.Errorf("failed to parse order book: %w", err) } if apiResp.Code != 200 { - return 0, 0, fmt.Errorf("API error code: %d", apiResp.Code) + return nil, nil, fmt.Errorf("API error code: %d", apiResp.Code) } - // Get best bid (highest buy price) - if len(apiResp.Data.Bids) > 0 && len(apiResp.Data.Bids[0]) >= 1 { - if price, ok := apiResp.Data.Bids[0][0].(float64); ok { - bestBid = price - } else if priceStr, ok := apiResp.Data.Bids[0][0].(string); ok { - bestBid, _ = strconv.ParseFloat(priceStr, 64) + // Helper to parse price/quantity from interface{} + parseFloat := func(v interface{}) float64 { + if f, ok := v.(float64); ok { + return f + } + if s, ok := v.(string); ok { + f, _ := strconv.ParseFloat(s, 64) + return f + } + return 0 + } + + // Convert bids to [][]float64 + maxBids := len(apiResp.Data.Bids) + if depth > 0 && depth < maxBids { + maxBids = depth + } + bids = make([][]float64, 0, maxBids) + for i := 0; i < maxBids; i++ { + if len(apiResp.Data.Bids[i]) >= 2 { + price := parseFloat(apiResp.Data.Bids[i][0]) + qty := parseFloat(apiResp.Data.Bids[i][1]) + if price > 0 && qty > 0 { + bids = append(bids, []float64{price, qty}) + } } } - // Get best ask (lowest sell price) - if len(apiResp.Data.Asks) > 0 && len(apiResp.Data.Asks[0]) >= 1 { - if price, ok := apiResp.Data.Asks[0][0].(float64); ok { - bestAsk = price - } else if priceStr, ok := apiResp.Data.Asks[0][0].(string); ok { - bestAsk, _ = strconv.ParseFloat(priceStr, 64) + // Convert asks to [][]float64 + maxAsks := len(apiResp.Data.Asks) + if depth > 0 && depth < maxAsks { + maxAsks = depth + } + asks = make([][]float64, 0, maxAsks) + for i := 0; i < maxAsks; i++ { + if len(apiResp.Data.Asks[i]) >= 2 { + price := parseFloat(apiResp.Data.Asks[i][0]) + qty := parseFloat(apiResp.Data.Asks[i][1]) + if price > 0 && qty > 0 { + asks = append(asks, []float64{price, qty}) + } } } - if bestBid <= 0 || bestAsk <= 0 { - return 0, 0, fmt.Errorf("invalid order book prices: bid=%.2f, ask=%.2f", bestBid, bestAsk) + if len(bids) > 0 && len(asks) > 0 { + logger.Infof("✓ Lighter order book: %s best_bid=%.2f, best_ask=%.2f, depth=%d/%d", + symbol, bids[0][0], asks[0][0], len(bids), len(asks)) } - logger.Infof("✓ Lighter order book: %s bid=%.2f, ask=%.2f", symbol, bestBid, bestAsk) - return bestBid, bestAsk, nil + return bids, asks, nil } diff --git a/trader/lighter_trader_v2_trading.go b/trader/lighter_trader_v2_trading.go index ff5a7341..f179082b 100644 --- a/trader/lighter_trader_v2_trading.go +++ b/trader/lighter_trader_v2_trading.go @@ -692,3 +692,45 @@ func (t *LighterTraderV2) GetOpenOrders(symbol string) ([]OpenOrder, error) { // TODO: Implement Lighter open orders return []OpenOrder{}, nil } + +// PlaceLimitOrder implements GridTrader interface for grid trading +// Places a limit order at the specified price +func (t *LighterTraderV2) PlaceLimitOrder(req *LimitOrderRequest) (*LimitOrderResult, error) { + if t.txClient == nil { + return nil, fmt.Errorf("TxClient not initialized") + } + + // Determine if this is a sell (ask) order + isAsk := req.Side == "SELL" + + logger.Infof("📝 LIGHTER placing limit order: %s %s @ %.4f, qty=%.4f", + req.Symbol, req.Side, req.Price, req.Quantity) + + // Create limit order using existing CreateOrder function + orderResult, err := t.CreateOrder(req.Symbol, isAsk, req.Quantity, req.Price, "limit", req.ReduceOnly) + if err != nil { + return nil, fmt.Errorf("failed to place limit order: %w", err) + } + + // Extract order ID from result + orderID := "" + if id, ok := orderResult["orderId"]; ok { + orderID = fmt.Sprintf("%v", id) + } else if txHash, ok := orderResult["tx_hash"]; ok { + orderID = fmt.Sprintf("%v", txHash) + } + + logger.Infof("✓ LIGHTER limit order placed: %s %s @ %.4f, OrderID: %s", + req.Symbol, req.Side, req.Price, orderID) + + return &LimitOrderResult{ + OrderID: orderID, + ClientID: req.ClientID, + Symbol: req.Symbol, + Side: req.Side, + PositionSide: req.PositionSide, + Price: req.Price, + Quantity: req.Quantity, + Status: "NEW", + }, nil +} diff --git a/trader/okx_trader.go b/trader/okx_trader.go index 2d4b8b89..73275a87 100644 --- a/trader/okx_trader.go +++ b/trader/okx_trader.go @@ -1393,3 +1393,155 @@ func (t *OKXTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { // TODO: Implement OKX open orders return []OpenOrder{}, nil } + +// PlaceLimitOrder places a limit order for grid trading +// Implements GridTrader interface +func (t *OKXTrader) PlaceLimitOrder(req *LimitOrderRequest) (*LimitOrderResult, error) { + instId := t.convertSymbol(req.Symbol) + + // Get instrument info + inst, err := t.getInstrument(req.Symbol) + if err != nil { + return nil, fmt.Errorf("failed to get instrument info: %w", err) + } + + // Set leverage if specified + if req.Leverage > 0 { + if err := t.SetLeverage(req.Symbol, req.Leverage); err != nil { + logger.Warnf("[OKX] Failed to set leverage: %v", err) + } + } + + // Convert quantity to contract size + sz := req.Quantity / inst.CtVal + szStr := t.formatSize(sz, inst) + + // Determine side and position side + side := "buy" + posSide := "long" + if req.Side == "SELL" { + side = "sell" + posSide = "short" + } + + body := map[string]interface{}{ + "instId": instId, + "tdMode": "cross", + "side": side, + "posSide": posSide, + "ordType": "limit", + "sz": szStr, + "px": fmt.Sprintf("%.8f", req.Price), + "clOrdId": genOkxClOrdID(), + "tag": okxTag, + } + + // Add reduce only if specified + if req.ReduceOnly { + body["reduceOnly"] = true + } + + logger.Infof("[OKX] PlaceLimitOrder: %s %s @ %.4f, sz=%s", instId, side, req.Price, szStr) + + data, err := t.doRequest("POST", okxOrderPath, body) + if err != nil { + return nil, fmt.Errorf("failed to place limit order: %w", err) + } + + var orders []struct { + OrdId string `json:"ordId"` + ClOrdId string `json:"clOrdId"` + SCode string `json:"sCode"` + SMsg string `json:"sMsg"` + } + + if err := json.Unmarshal(data, &orders); err != nil { + return nil, fmt.Errorf("failed to parse order response: %w", err) + } + + if len(orders) == 0 { + return nil, fmt.Errorf("empty order response") + } + + if orders[0].SCode != "0" { + return nil, fmt.Errorf("OKX order failed: %s", orders[0].SMsg) + } + + logger.Infof("✓ [OKX] Limit order placed: %s %s @ %.4f, orderID=%s", + instId, side, req.Price, orders[0].OrdId) + + return &LimitOrderResult{ + OrderID: orders[0].OrdId, + ClientID: orders[0].ClOrdId, + Symbol: req.Symbol, + Side: req.Side, + PositionSide: req.PositionSide, + Price: req.Price, + Quantity: req.Quantity, + Status: "NEW", + }, nil +} + +// CancelOrder cancels a specific order by ID +// Implements GridTrader interface +func (t *OKXTrader) CancelOrder(symbol, orderID string) error { + instId := t.convertSymbol(symbol) + + body := map[string]interface{}{ + "instId": instId, + "ordId": orderID, + } + + _, err := t.doRequest("POST", "/api/v5/trade/cancel-order", body) + if err != nil { + return fmt.Errorf("failed to cancel order: %w", err) + } + + logger.Infof("✓ [OKX] Order cancelled: %s %s", symbol, orderID) + return nil +} + +// GetOrderBook gets the order book for a symbol +// Implements GridTrader interface +func (t *OKXTrader) GetOrderBook(symbol string, depth int) (bids, asks [][]float64, err error) { + instId := t.convertSymbol(symbol) + path := fmt.Sprintf("/api/v5/market/books?instId=%s&sz=%d", instId, depth) + + data, err := t.doRequest("GET", path, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to get order book: %w", err) + } + + var result []struct { + Bids [][]string `json:"bids"` + Asks [][]string `json:"asks"` + } + + if err := json.Unmarshal(data, &result); err != nil { + return nil, nil, fmt.Errorf("failed to parse order book: %w", err) + } + + if len(result) == 0 { + return nil, nil, nil + } + + // Parse bids + for _, b := range result[0].Bids { + if len(b) >= 2 { + price, _ := strconv.ParseFloat(b[0], 64) + qty, _ := strconv.ParseFloat(b[1], 64) + bids = append(bids, []float64{price, qty}) + } + } + + // Parse asks + for _, a := range result[0].Asks { + if len(a) >= 2 { + price, _ := strconv.ParseFloat(a[0], 64) + qty, _ := strconv.ParseFloat(a[1], 64) + asks = append(asks, []float64{price, qty}) + } + } + + return bids, asks, nil +} diff --git a/web/src/components/strategy/GridConfigEditor.tsx b/web/src/components/strategy/GridConfigEditor.tsx new file mode 100644 index 00000000..7756f2ba --- /dev/null +++ b/web/src/components/strategy/GridConfigEditor.tsx @@ -0,0 +1,424 @@ +import { Grid, DollarSign, TrendingUp, Shield } from 'lucide-react' +import type { GridStrategyConfig } from '../../types' + +interface GridConfigEditorProps { + config: GridStrategyConfig + onChange: (config: GridStrategyConfig) => void + disabled?: boolean + language: string +} + +// Default grid config +export const defaultGridConfig: GridStrategyConfig = { + symbol: 'BTCUSDT', + grid_count: 10, + total_investment: 1000, + leverage: 5, + upper_price: 0, + lower_price: 0, + use_atr_bounds: true, + atr_multiplier: 2.0, + distribution: 'gaussian', + max_drawdown_pct: 15, + stop_loss_pct: 5, + daily_loss_limit_pct: 10, + use_maker_only: true, +} + +export function GridConfigEditor({ + config, + onChange, + disabled, + language, +}: GridConfigEditorProps) { + const t = (key: string) => { + const translations: Record> = { + // Section titles + tradingPair: { zh: '交易设置', en: 'Trading Setup' }, + gridParameters: { zh: '网格参数', en: 'Grid Parameters' }, + priceBounds: { zh: '价格边界', en: 'Price Bounds' }, + riskControl: { zh: '风险控制', en: 'Risk Control' }, + + // Trading pair + symbol: { zh: '交易对', en: 'Trading Pair' }, + symbolDesc: { zh: '选择要进行网格交易的交易对', en: 'Select trading pair for grid trading' }, + + // Investment + totalInvestment: { zh: '投资金额 (USDT)', en: 'Investment (USDT)' }, + totalInvestmentDesc: { zh: '网格策略的总投资金额', en: 'Total investment for grid strategy' }, + leverage: { zh: '杠杆倍数', en: 'Leverage' }, + leverageDesc: { zh: '交易使用的杠杆倍数 (1-20)', en: 'Leverage for trading (1-20)' }, + + // Grid parameters + gridCount: { zh: '网格数量', en: 'Grid Count' }, + gridCountDesc: { zh: '网格层级数量 (5-50)', en: 'Number of grid levels (5-50)' }, + distribution: { zh: '资金分配方式', en: 'Distribution' }, + distributionDesc: { zh: '网格层级的资金分配方式', en: 'Fund allocation across grid levels' }, + uniform: { zh: '均匀分配', en: 'Uniform' }, + gaussian: { zh: '高斯分配 (推荐)', en: 'Gaussian (Recommended)' }, + pyramid: { zh: '金字塔分配', en: 'Pyramid' }, + + // Price bounds + useAtrBounds: { zh: '自动计算边界 (ATR)', en: 'Auto-calculate Bounds (ATR)' }, + useAtrBoundsDesc: { zh: '基于 ATR 自动计算网格上下边界', en: 'Auto-calculate bounds based on ATR' }, + atrMultiplier: { zh: 'ATR 倍数', en: 'ATR Multiplier' }, + atrMultiplierDesc: { zh: '边界距离当前价格的 ATR 倍数', en: 'ATR multiplier for bounds distance' }, + upperPrice: { zh: '上边界价格', en: 'Upper Price' }, + upperPriceDesc: { zh: '网格上边界价格 (0=自动计算)', en: 'Grid upper bound (0=auto)' }, + lowerPrice: { zh: '下边界价格', en: 'Lower Price' }, + lowerPriceDesc: { zh: '网格下边界价格 (0=自动计算)', en: 'Grid lower bound (0=auto)' }, + + // Risk control + maxDrawdown: { zh: '最大回撤 (%)', en: 'Max Drawdown (%)' }, + maxDrawdownDesc: { zh: '触发紧急退出的最大回撤百分比', en: 'Max drawdown before emergency exit' }, + stopLoss: { zh: '止损 (%)', en: 'Stop Loss (%)' }, + stopLossDesc: { zh: '单仓位止损百分比', en: 'Stop loss per position' }, + dailyLossLimit: { zh: '日损失限制 (%)', en: 'Daily Loss Limit (%)' }, + dailyLossLimitDesc: { zh: '每日最大亏损百分比', en: 'Maximum daily loss percentage' }, + useMakerOnly: { zh: '仅使用 Maker 订单', en: 'Maker Only Orders' }, + useMakerOnlyDesc: { zh: '使用限价单以降低手续费', en: 'Use limit orders for lower fees' }, + } + return translations[key]?.[language] || key + } + + const updateField = ( + key: K, + value: GridStrategyConfig[K] + ) => { + if (!disabled) { + onChange({ ...config, [key]: value }) + } + } + + const inputStyle = { + background: '#1E2329', + border: '1px solid #2B3139', + color: '#EAECEF', + } + + const sectionStyle = { + background: '#0B0E11', + border: '1px solid #2B3139', + } + + return ( +
+ {/* Trading Setup */} +
+
+ +

+ {t('tradingPair')} +

+
+ +
+ {/* Symbol */} +
+ +

+ {t('symbolDesc')} +

+ +
+ + {/* Investment */} +
+ +

+ {t('totalInvestmentDesc')} +

+ updateField('total_investment', parseFloat(e.target.value) || 1000)} + disabled={disabled} + min={100} + step={100} + className="w-full px-3 py-2 rounded" + style={inputStyle} + /> +
+ + {/* Leverage */} +
+ +

+ {t('leverageDesc')} +

+ updateField('leverage', parseInt(e.target.value) || 5)} + disabled={disabled} + min={1} + max={20} + className="w-full px-3 py-2 rounded" + style={inputStyle} + /> +
+
+
+ + {/* Grid Parameters */} +
+
+ +

+ {t('gridParameters')} +

+
+ +
+ {/* Grid Count */} +
+ +

+ {t('gridCountDesc')} +

+ updateField('grid_count', parseInt(e.target.value) || 10)} + disabled={disabled} + min={5} + max={50} + className="w-full px-3 py-2 rounded" + style={inputStyle} + /> +
+ + {/* Distribution */} +
+ +

+ {t('distributionDesc')} +

+ +
+
+
+ + {/* Price Bounds */} +
+
+ +

+ {t('priceBounds')} +

+
+ + {/* ATR Toggle */} +
+
+
+ +

+ {t('useAtrBoundsDesc')} +

+
+ +
+
+ + {config.use_atr_bounds ? ( +
+ +

+ {t('atrMultiplierDesc')} +

+ updateField('atr_multiplier', parseFloat(e.target.value) || 2.0)} + disabled={disabled} + min={1} + max={5} + step={0.5} + className="w-32 px-3 py-2 rounded" + style={inputStyle} + /> +
+ ) : ( +
+
+ +

+ {t('upperPriceDesc')} +

+ updateField('upper_price', parseFloat(e.target.value) || 0)} + disabled={disabled} + min={0} + step={0.01} + className="w-full px-3 py-2 rounded" + style={inputStyle} + /> +
+
+ +

+ {t('lowerPriceDesc')} +

+ updateField('lower_price', parseFloat(e.target.value) || 0)} + disabled={disabled} + min={0} + step={0.01} + className="w-full px-3 py-2 rounded" + style={inputStyle} + /> +
+
+ )} +
+ + {/* Risk Control */} +
+
+ +

+ {t('riskControl')} +

+
+ +
+
+ +

+ {t('maxDrawdownDesc')} +

+ updateField('max_drawdown_pct', parseFloat(e.target.value) || 15)} + disabled={disabled} + min={5} + max={50} + className="w-full px-3 py-2 rounded" + style={inputStyle} + /> +
+ +
+ +

+ {t('stopLossDesc')} +

+ updateField('stop_loss_pct', parseFloat(e.target.value) || 5)} + disabled={disabled} + min={1} + max={20} + className="w-full px-3 py-2 rounded" + style={inputStyle} + /> +
+ +
+ +

+ {t('dailyLossLimitDesc')} +

+ updateField('daily_loss_limit_pct', parseFloat(e.target.value) || 10)} + disabled={disabled} + min={1} + max={30} + className="w-full px-3 py-2 rounded" + style={inputStyle} + /> +
+
+ + {/* Maker Only Toggle */} +
+
+
+ +

+ {t('useMakerOnlyDesc')} +

+
+ +
+
+
+
+ ) +} diff --git a/web/src/pages/StrategyStudioPage.tsx b/web/src/pages/StrategyStudioPage.tsx index fcb3fc17..8b21d502 100644 --- a/web/src/pages/StrategyStudioPage.tsx +++ b/web/src/pages/StrategyStudioPage.tsx @@ -37,6 +37,7 @@ import { IndicatorEditor } from '../components/strategy/IndicatorEditor' import { RiskControlEditor } from '../components/strategy/RiskControlEditor' import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor' import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor' +import { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor' import { DeepVoidBackground } from '../components/DeepVoidBackground' const API_BASE = import.meta.env.VITE_API_BASE || '' @@ -59,6 +60,7 @@ export function StrategyStudioPage() { // Accordion states for left panel const [expandedSections, setExpandedSections] = useState({ + gridConfig: true, coinSource: true, indicators: false, riskControl: false, @@ -486,6 +488,12 @@ export function StrategyStudioPage() { subtitle: { zh: '可视化配置和测试交易策略', en: 'Configure and test trading strategies' }, strategies: { zh: '策略', en: 'Strategies' }, newStrategy: { zh: '新建', en: 'New' }, + strategyType: { zh: '策略类型', en: 'Strategy Type' }, + aiTrading: { zh: 'AI 智能交易', en: 'AI Trading' }, + aiTradingDesc: { zh: 'AI 分析市场并自主决策买卖', en: 'AI analyzes market and makes trading decisions' }, + gridTrading: { zh: 'AI 网格交易', en: 'AI Grid Trading' }, + gridTradingDesc: { zh: 'AI 控制网格策略,在震荡市场获利', en: 'AI-controlled grid strategy for ranging markets' }, + gridConfig: { zh: '网格配置', en: 'Grid Configuration' }, coinSource: { zh: '币种来源', en: 'Coin Source' }, indicators: { zh: '技术指标', en: 'Indicators' }, riskControl: { zh: '风控参数', en: 'Risk Control' }, @@ -533,12 +541,33 @@ export function StrategyStudioPage() { ) } + // Get current strategy type (default to ai_trading if not set) + const currentStrategyType = editingConfig?.strategy_type || 'ai_trading' + const configSections = [ + // Grid Config - only for grid_trading + { + key: 'gridConfig' as const, + icon: Activity, + color: '#0ECB81', + title: t('gridConfig'), + forStrategyType: 'grid_trading' as const, + content: editingConfig?.grid_config && ( + updateConfig('grid_config', gridConfig)} + disabled={selectedStrategy?.is_default} + language={language} + /> + ), + }, + // AI Trading sections { key: 'coinSource' as const, icon: Target, color: '#F0B90B', title: t('coinSource'), + forStrategyType: 'ai_trading' as const, content: editingConfig && (

@@ -616,6 +649,7 @@ export function StrategyStudioPage() { icon: Globe, color: '#0ECB81', title: t('publishSettings'), + forStrategyType: 'both' as const, content: selectedStrategy && ( ), }, - ] + ].filter(section => + section.forStrategyType === 'both' || section.forStrategyType === currentStrategyType + ) return ( @@ -813,6 +849,62 @@ export function StrategyStudioPage() { + {/* Strategy Type Selector */} + {editingConfig && ( +

+
+ + {t('strategyType')} +
+
+ + +
+
+ )} + {/* Config Sections */}
{configSections.map(({ key, icon: Icon, color, title, content }) => ( diff --git a/web/src/pages/TraderDashboardPage.tsx b/web/src/pages/TraderDashboardPage.tsx index 6f60b76f..55a65004 100644 --- a/web/src/pages/TraderDashboardPage.tsx +++ b/web/src/pages/TraderDashboardPage.tsx @@ -151,6 +151,13 @@ export function TraderDashboardPage({ setPositionsCurrentPage(1) }, [selectedTraderId, positionsPageSize]) + // Auto-set chart symbol for grid trading + useEffect(() => { + if (status?.strategy_type === 'grid_trading' && status?.grid_symbol) { + setSelectedChartSymbol(status.grid_symbol) + } + }, [status?.strategy_type, status?.grid_symbol]) + // Get current exchange info for perp-dex wallet display const currentExchange = exchanges?.find( (e) => e.id === selectedTrader?.exchange_id diff --git a/web/src/types.ts b/web/src/types.ts index b4f39278..f4e6dee0 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -11,6 +11,8 @@ export interface SystemStatus { stop_until: string last_reset_time: string ai_provider: string + strategy_type?: 'ai_trading' | 'grid_trading' + grid_symbol?: string } export interface AccountInfo { @@ -462,6 +464,8 @@ export interface PromptSectionsConfig { } export interface StrategyConfig { + // Strategy type: "ai_trading" (default) or "grid_trading" + strategy_type?: 'ai_trading' | 'grid_trading'; // Language setting: "zh" for Chinese, "en" for English // Determines the language used for data formatting and prompt generation language?: 'zh' | 'en'; @@ -470,6 +474,38 @@ export interface StrategyConfig { custom_prompt?: string; risk_control: RiskControlConfig; prompt_sections?: PromptSectionsConfig; + // Grid trading configuration (only used when strategy_type is 'grid_trading') + grid_config?: GridStrategyConfig; +} + +// Grid trading specific configuration +export interface GridStrategyConfig { + // Trading pair (e.g., "BTCUSDT") + symbol: string; + // Number of grid levels (5-50) + grid_count: number; + // Total investment in USDT + total_investment: number; + // Leverage (1-20) + leverage: number; + // Upper price boundary (0 = auto-calculate from ATR) + upper_price: number; + // Lower price boundary (0 = auto-calculate from ATR) + lower_price: number; + // Use ATR to auto-calculate bounds + use_atr_bounds: boolean; + // ATR multiplier for bound calculation (default 2.0) + atr_multiplier: number; + // Position distribution: "uniform" | "gaussian" | "pyramid" + distribution: 'uniform' | 'gaussian' | 'pyramid'; + // Maximum drawdown percentage before emergency exit + max_drawdown_pct: number; + // Stop loss percentage per position + stop_loss_pct: number; + // Daily loss limit percentage + daily_loss_limit_pct: number; + // Use maker-only orders for lower fees + use_maker_only: boolean; } export interface CoinSourceConfig {