diff --git a/decision/engine.go b/decision/engine.go index 12c7305f..eeb16551 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -973,6 +973,67 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string { sb.WriteString("\n") } + // Historical trading statistics (helps AI understand past performance) + if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 { + // Detect language from strategy config + lang := detectLanguage(e.config.PromptSections.RoleDefinition) + + // Win/Loss ratio + var winLossRatio float64 + if ctx.TradingStats.AvgLoss > 0 { + winLossRatio = ctx.TradingStats.AvgWin / ctx.TradingStats.AvgLoss + } + + if lang == LangChinese { + sb.WriteString("## 历史交易统计\n") + sb.WriteString(fmt.Sprintf("总交易: %d 笔 | 盈利因子: %.2f | 夏普比率: %.2f | 盈亏比: %.2f\n", + ctx.TradingStats.TotalTrades, + ctx.TradingStats.ProfitFactor, + ctx.TradingStats.SharpeRatio, + winLossRatio)) + sb.WriteString(fmt.Sprintf("总盈亏: %+.2f USDT | 平均盈利: +%.2f | 平均亏损: -%.2f | 最大回撤: %.1f%%\n", + ctx.TradingStats.TotalPnL, + ctx.TradingStats.AvgWin, + ctx.TradingStats.AvgLoss, + ctx.TradingStats.MaxDrawdownPct)) + + // Performance hints based on profit factor, sharpe, and drawdown + if ctx.TradingStats.ProfitFactor >= 1.5 && ctx.TradingStats.SharpeRatio >= 1 { + sb.WriteString("表现: 良好 - 保持当前策略\n") + } else if ctx.TradingStats.ProfitFactor < 1 { + sb.WriteString("表现: 需改进 - 提高盈亏比,优化止盈止损\n") + } else if ctx.TradingStats.MaxDrawdownPct > 30 { + sb.WriteString("表现: 风险偏高 - 减少仓位,控制回撤\n") + } else { + sb.WriteString("表现: 正常 - 有优化空间\n") + } + } else { + sb.WriteString("## Historical Trading Statistics\n") + sb.WriteString(fmt.Sprintf("Total Trades: %d | Profit Factor: %.2f | Sharpe: %.2f | Win/Loss Ratio: %.2f\n", + ctx.TradingStats.TotalTrades, + ctx.TradingStats.ProfitFactor, + ctx.TradingStats.SharpeRatio, + winLossRatio)) + sb.WriteString(fmt.Sprintf("Total PnL: %+.2f USDT | Avg Win: +%.2f | Avg Loss: -%.2f | Max Drawdown: %.1f%%\n", + ctx.TradingStats.TotalPnL, + ctx.TradingStats.AvgWin, + ctx.TradingStats.AvgLoss, + ctx.TradingStats.MaxDrawdownPct)) + + // Performance hints based on profit factor, sharpe, and drawdown + if ctx.TradingStats.ProfitFactor >= 1.5 && ctx.TradingStats.SharpeRatio >= 1 { + sb.WriteString("Performance: GOOD - maintain current strategy\n") + } else if ctx.TradingStats.ProfitFactor < 1 { + sb.WriteString("Performance: NEEDS IMPROVEMENT - improve win/loss ratio, optimize TP/SL\n") + } else if ctx.TradingStats.MaxDrawdownPct > 30 { + sb.WriteString("Performance: HIGH RISK - reduce position size, control drawdown\n") + } else { + sb.WriteString("Performance: NORMAL - room for optimization\n") + } + } + sb.WriteString("\n") + } + // Position information if len(ctx.Positions) > 0 { sb.WriteString("## Current Positions\n") diff --git a/decision/formatter.go b/decision/formatter.go index 0b88b78b..9b2174bd 100644 --- a/decision/formatter.go +++ b/decision/formatter.go @@ -51,7 +51,16 @@ func formatContextData(ctx *Context, lang Language) string { sb.WriteString(formatAccountEN(ctx)) } - // 4. 最近交易记录 + // 4. 历史交易统计 + if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 { + if lang == LangChinese { + sb.WriteString(formatTradingStatsZH(ctx.TradingStats)) + } else { + sb.WriteString(formatTradingStatsEN(ctx.TradingStats)) + } + } + + // 5. 最近交易记录 if len(ctx.RecentOrders) > 0 { if lang == LangChinese { sb.WriteString(formatRecentTradesZH(ctx.RecentOrders)) @@ -120,6 +129,67 @@ func formatAccountZH(ctx *Context) string { return sb.String() } +// formatTradingStatsZH 格式化历史交易统计(中文) +func formatTradingStatsZH(stats *TradingStats) string { + var sb strings.Builder + sb.WriteString("## 历史交易统计\n\n") + + // 盈亏比计算 + var winLossRatio float64 + if stats.AvgLoss > 0 { + winLossRatio = stats.AvgWin / stats.AvgLoss + } + + // 指标定义说明(去掉胜率,聚焦核心指标) + sb.WriteString("**指标说明**:\n") + sb.WriteString("- 盈利因子: 总盈利 ÷ 总亏损(>1表示盈利,>1.5为良好,>2为优秀)\n") + sb.WriteString("- 夏普比率: (平均收益 - 无风险收益) ÷ 收益标准差(>1良好,>2优秀)\n") + sb.WriteString("- 盈亏比: 平均盈利 ÷ 平均亏损(>1.5为良好,>2为优秀)\n") + sb.WriteString("- 最大回撤: 资金曲线从峰值到谷底的最大跌幅(<20%为低风险)\n\n") + + // 数据值 + sb.WriteString("**当前数据**:\n") + sb.WriteString(fmt.Sprintf("- 总交易: %d 笔\n", stats.TotalTrades)) + sb.WriteString(fmt.Sprintf("- 盈利因子: %.2f\n", stats.ProfitFactor)) + sb.WriteString(fmt.Sprintf("- 夏普比率: %.2f\n", stats.SharpeRatio)) + sb.WriteString(fmt.Sprintf("- 盈亏比: %.2f\n", winLossRatio)) + sb.WriteString(fmt.Sprintf("- 总盈亏: %+.2f USDT\n", stats.TotalPnL)) + sb.WriteString(fmt.Sprintf("- 平均盈利: +%.2f USDT\n", stats.AvgWin)) + sb.WriteString(fmt.Sprintf("- 平均亏损: -%.2f USDT\n", stats.AvgLoss)) + sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n\n", stats.MaxDrawdownPct)) + + // 综合分析和决策建议 + sb.WriteString("**决策参考**:\n") + + // 根据统计数据给出具体建议 + if stats.TotalTrades < 10 { + sb.WriteString("- 样本量较小(<10笔),统计结果参考意义有限\n") + } + + if stats.ProfitFactor >= 1.5 && stats.SharpeRatio >= 1 { + sb.WriteString("- 📈 表现良好: 可以维持当前策略风格\n") + } else if stats.ProfitFactor >= 1.0 { + sb.WriteString("- 📊 表现正常: 策略可行但有优化空间\n") + } + + if stats.ProfitFactor < 1.0 { + sb.WriteString("- ⚠️ 盈利因子<1: 亏损大于盈利,需要提高盈亏比,优化止盈止损\n") + } + + if winLossRatio > 0 && winLossRatio < 1.5 { + sb.WriteString("- ⚠️ 盈亏比偏低: 建议让利润奔跑,提高止盈目标\n") + } + + if stats.MaxDrawdownPct > 30 { + sb.WriteString("- ⚠️ 最大回撤过高: 建议降低仓位大小控制风险\n") + } else if stats.MaxDrawdownPct < 10 { + sb.WriteString("- ✅ 回撤控制良好: 风险管理有效\n") + } + + sb.WriteString("\n") + return sb.String() +} + // formatRecentTradesZH 格式化最近交易(中文) func formatRecentTradesZH(orders []RecentOrder) string { var sb strings.Builder @@ -333,6 +403,67 @@ func formatAccountEN(ctx *Context) string { return sb.String() } +// formatTradingStatsEN 格式化历史交易统计(英文) +func formatTradingStatsEN(stats *TradingStats) string { + var sb strings.Builder + sb.WriteString("## Historical Trading Statistics\n\n") + + // Win/Loss ratio calculation + var winLossRatio float64 + if stats.AvgLoss > 0 { + winLossRatio = stats.AvgWin / stats.AvgLoss + } + + // Metric definitions (focus on core metrics, remove win rate) + sb.WriteString("**Metric Definitions**:\n") + sb.WriteString("- Profit Factor: Total profits ÷ Total losses (>1 = profitable, >1.5 = good, >2 = excellent)\n") + sb.WriteString("- Sharpe Ratio: (Avg return - Risk-free rate) ÷ Std dev of returns (>1 = good, >2 = excellent)\n") + sb.WriteString("- Win/Loss Ratio: Avg win ÷ Avg loss (>1.5 = good, >2 = excellent)\n") + sb.WriteString("- Max Drawdown: Largest peak-to-trough decline in equity curve (<20% = low risk)\n\n") + + // Data values + sb.WriteString("**Current Data**:\n") + sb.WriteString(fmt.Sprintf("- Total Trades: %d\n", stats.TotalTrades)) + sb.WriteString(fmt.Sprintf("- Profit Factor: %.2f\n", stats.ProfitFactor)) + sb.WriteString(fmt.Sprintf("- Sharpe Ratio: %.2f\n", stats.SharpeRatio)) + sb.WriteString(fmt.Sprintf("- Win/Loss Ratio: %.2f\n", winLossRatio)) + sb.WriteString(fmt.Sprintf("- Total PnL: %+.2f USDT\n", stats.TotalPnL)) + sb.WriteString(fmt.Sprintf("- Avg Win: +%.2f USDT\n", stats.AvgWin)) + sb.WriteString(fmt.Sprintf("- Avg Loss: -%.2f USDT\n", stats.AvgLoss)) + sb.WriteString(fmt.Sprintf("- Max Drawdown: %.1f%%\n\n", stats.MaxDrawdownPct)) + + // Analysis and decision guidance + sb.WriteString("**Decision Guidance**:\n") + + // Specific recommendations based on stats + if stats.TotalTrades < 10 { + sb.WriteString("- Small sample size (<10 trades), statistics have limited significance\n") + } + + if stats.ProfitFactor >= 1.5 && stats.SharpeRatio >= 1 { + sb.WriteString("- 📈 Good performance: Maintain current strategy approach\n") + } else if stats.ProfitFactor >= 1.0 { + sb.WriteString("- 📊 Normal performance: Strategy viable but has room for optimization\n") + } + + if stats.ProfitFactor < 1.0 { + sb.WriteString("- ⚠️ Profit factor <1: Losses exceed profits, improve win/loss ratio, optimize TP/SL\n") + } + + if winLossRatio > 0 && winLossRatio < 1.5 { + sb.WriteString("- ⚠️ Low win/loss ratio: Let profits run, increase take-profit targets\n") + } + + if stats.MaxDrawdownPct > 30 { + sb.WriteString("- ⚠️ High max drawdown: Consider reducing position sizes to control risk\n") + } else if stats.MaxDrawdownPct < 10 { + sb.WriteString("- ✅ Good drawdown control: Risk management is effective\n") + } + + sb.WriteString("\n") + return sb.String() +} + // formatRecentTradesEN 格式化最近交易(英文) func formatRecentTradesEN(orders []RecentOrder) string { var sb strings.Builder diff --git a/docker/Dockerfile.backend b/docker/Dockerfile.backend index 7bd02348..b8c1c14e 100644 --- a/docker/Dockerfile.backend +++ b/docker/Dockerfile.backend @@ -56,7 +56,7 @@ RUN CGO_ENABLED=1 GOOS=linux \ # ────────────────────────────────────────────────────────────── FROM alpine:${ALPINE_VERSION} -RUN apk update && apk add --no-cache ca-certificates tzdata +RUN apk update && apk add --no-cache ca-certificates tzdata sqlite COPY --from=ta-lib-builder /usr/local /usr/local WORKDIR /app diff --git a/install.sh b/install.sh index b28b13cb..0dac74c4 100644 --- a/install.sh +++ b/install.sh @@ -128,6 +128,38 @@ pull_images() { echo -e "${GREEN}✓ Images pulled${NC}" } +# Ask user if they want to clear trading data +ask_clear_trading_data() { + local db_file="data/data.db" + + # Only ask if database file exists + if [ ! -f "$db_file" ]; then + CLEAR_TRADING_DATA="no" + return 0 + fi + + echo "" + echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${YELLOW}Do you want to clear trading data? (orders, fills, positions)${NC}" + echo -e "${BLUE} • trader_orders (Order records)${NC}" + echo -e "${BLUE} • trader_fills (Fill/execution records)${NC}" + echo -e "${BLUE} • trader_positions (Position records)${NC}" + echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo -e "${BLUE}Type 'yes' to clear tables, press Enter or any other input to skip${NC}" + echo -n "Input: " + read -r confirm + + if [ "$confirm" == "yes" ]; then + CLEAR_TRADING_DATA="yes" + echo -e "${YELLOW}Trading data will be cleared after services start...${NC}" + else + CLEAR_TRADING_DATA="no" + echo -e "${BLUE}Skipping data clear${NC}" + fi + echo "" +} + # Start services start_services() { echo -e "${YELLOW}Starting NOFX services...${NC}" @@ -135,6 +167,28 @@ start_services() { echo -e "${GREEN}✓ Services started${NC}" } +# Clear trading data via container (called after services are ready) +clear_trading_data() { + if [ "$CLEAR_TRADING_DATA" != "yes" ]; then + return 0 + fi + + echo -e "${YELLOW}Clearing trading data tables via container...${NC}" + + # Wait a moment for database to be ready + sleep 2 + + # Execute SQL to clear tables via docker exec + $COMPOSE_CMD exec -T backend sh -c "sqlite3 /app/data/data.db 'DELETE FROM trader_fills; DELETE FROM trader_orders; DELETE FROM trader_positions;'" 2>/dev/null + + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ Trading data tables cleared${NC}" + else + echo -e "${RED}Failed to clear trading data. You can manually run:${NC}" + echo -e "${BLUE} $COMPOSE_CMD exec backend sqlite3 /app/data/data.db 'DELETE FROM trader_fills; DELETE FROM trader_orders; DELETE FROM trader_positions;'${NC}" + fi +} + # Wait for services wait_for_services() { echo -e "${YELLOW}Waiting for services to be ready...${NC}" @@ -224,8 +278,10 @@ main() { download_files generate_env pull_images + ask_clear_trading_data start_services wait_for_services + clear_trading_data print_success } diff --git a/store/position.go b/store/position.go index ba9ebff2..cf86c623 100644 --- a/store/position.go +++ b/store/position.go @@ -469,6 +469,15 @@ func (s *PositionStore) GetPositionStats(traderID string) (map[string]interface{ func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) { stats := &TraderStats{} + // First check how many rows exist + var count int + if err := s.db.QueryRow(`SELECT COUNT(*) FROM trader_positions WHERE trader_id = ? AND status = 'CLOSED'`, traderID).Scan(&count); err == nil { + if count == 0 { + // No closed positions, return empty stats + return stats, nil + } + } + // Query all closed positions rows, err := s.db.Query(` SELECT realized_pnl, fee, exit_time diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 7003a50a..e489fc3e 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -866,6 +866,28 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { }) } } + // Get trading statistics for AI context + stats, err := at.store.Position().GetFullStats(at.id) + if err != nil { + logger.Infof("⚠️ [%s] Failed to get trading stats: %v", at.name, err) + } else if stats == nil { + logger.Infof("⚠️ [%s] GetFullStats returned nil", at.name) + } else if stats.TotalTrades == 0 { + logger.Infof("⚠️ [%s] GetFullStats returned 0 trades (traderID=%s)", at.name, at.id) + } else { + ctx.TradingStats = &decision.TradingStats{ + TotalTrades: stats.TotalTrades, + WinRate: stats.WinRate, + ProfitFactor: stats.ProfitFactor, + SharpeRatio: stats.SharpeRatio, + TotalPnL: stats.TotalPnL, + AvgWin: stats.AvgWin, + AvgLoss: stats.AvgLoss, + MaxDrawdownPct: stats.MaxDrawdownPct, + } + logger.Infof("📈 [%s] Trading stats: %d trades, %.1f%% win rate, PF=%.2f, Sharpe=%.2f, DD=%.1f%%", + at.name, stats.TotalTrades, stats.WinRate, stats.ProfitFactor, stats.SharpeRatio, stats.MaxDrawdownPct) + } } else { logger.Infof("⚠️ [%s] Store is nil, cannot get recent trades", at.name) }