当前结论
'+esc(statusLabel(r))+'
原因
'+esc(clean(r.execution_reason||r.state_reason||dl.decision||'等待进一步确认'))+'
入场模型
'+esc(clean(ep.entry_model||ep.entry_method||'--'))+'
失效条件
'+esc(clean(ep.invalid_if||ep.stop_basis||'跌破风险边界或信号衰减'))+'
diff --git a/.env.example b/.env.example index a2293ed..af164e7 100644 --- a/.env.example +++ b/.env.example @@ -80,7 +80,8 @@ ALPHAX_PAPER_ORDER_GATE_ENABLED=1 ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE=5 ALPHAX_PAPER_ENTRY_GATE_ENABLED=1 ALPHAX_PAPER_ENTRY_MIN_REC_SCORE=50 -ALPHAX_PAPER_ENTRY_MIN_RR=1.8 +ALPHAX_PAPER_MIN_RR=1.5 +ALPHAX_PAPER_ENTRY_MIN_RR=1.5 ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT=20 ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT=3 ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES=3 @@ -97,9 +98,9 @@ ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS=0 ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS=3 ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS=6 ALPHAX_PAPER_ORDER_MIN_REC_SCORE=50 -ALPHAX_PAPER_ORDER_MIN_RR=1.8 +ALPHAX_PAPER_ORDER_MIN_RR=1.5 ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK=1 -ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT=1.5 +ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT=0 ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT=8 ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER=0 ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT=12 diff --git a/app/config/system_config.py b/app/config/system_config.py index 8daf3b8..f5a9377 100644 --- a/app/config/system_config.py +++ b/app/config/system_config.py @@ -150,6 +150,10 @@ def _onchain_env_overrides(default_chains=("ethereum", "bsc")): def default_paper_trading_config(): + # One shared default keeps buy-now entries and wait-pullback orders from + # drifting into two unrelated RR standards. The explicit entry/order envs + # remain supported for advanced overrides. + paper_min_rr = _env_float("ALPHAX_PAPER_MIN_RR", 1.5) return { "enabled": _env_bool("ALPHAX_PAPER_TRADING_ENABLED", True), "account_equity_usdt": _env_float("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", 20000), @@ -174,7 +178,8 @@ def default_paper_trading_config(): "order_gate_enabled": _env_bool("ALPHAX_PAPER_ORDER_GATE_ENABLED", True), "entry_gate_enabled": _env_bool("ALPHAX_PAPER_ENTRY_GATE_ENABLED", True), "entry_min_rec_score": _env_float("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", 50.0), - "entry_min_rr": _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", 1.8), + "min_rr": paper_min_rr, + "entry_min_rr": _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", paper_min_rr), "max_stop_loss_leverage_risk_pct": _env_float("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", 20.0), "max_account_drawdown_pause_pct": _env_float("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", 3.0), "pause_after_weak_entries": _env_int("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", 3), @@ -191,7 +196,7 @@ def default_paper_trading_config(): "global_risk_max_same_sector_positions": _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS", 3), "global_risk_max_same_direction_positions": _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS", 6), "order_min_rec_score": _env_float("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", 50.0), - "order_min_rr": _env_float("ALPHAX_PAPER_ORDER_MIN_RR", 1.8), + "order_min_rr": _env_float("ALPHAX_PAPER_ORDER_MIN_RR", paper_min_rr), "order_require_risk_reward_ok": _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK", True), "order_min_distance_to_entry_pct": _env_float("ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT", 1.5), "order_max_distance_to_entry_pct": _env_float("ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT", 8.0), @@ -206,6 +211,58 @@ def default_paper_trading_config(): } +def _paper_trading_env_overrides(): + """Honor explicit paper-trading env vars even when DB runtime config exists.""" + overrides = {} + if _env_present("ALPHAX_PAPER_MIN_RR"): + shared_min_rr = _env_float("ALPHAX_PAPER_MIN_RR", 1.5) + overrides.update({ + "min_rr": shared_min_rr, + "entry_min_rr": shared_min_rr, + "order_min_rr": shared_min_rr, + }) + checks = { + "ALPHAX_PAPER_TRADING_ENABLED": ("enabled", lambda: _env_bool("ALPHAX_PAPER_TRADING_ENABLED", True)), + "ALPHAX_PAPER_ACCOUNT_EQUITY_USDT": ("account_equity_usdt", lambda: _env_float("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", 20000)), + "ALPHAX_PAPER_TRADE_NOTIONAL_USDT": ("trade_notional_usdt", lambda: _env_float("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", 5000)), + "ALPHAX_PAPER_TRADE_LEVERAGE": ("trade_leverage", lambda: _env_float("ALPHAX_PAPER_TRADE_LEVERAGE", 5)), + "ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE": ("max_cumulative_leverage", lambda: _env_float("ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE", 5.0)), + "ALPHAX_PAPER_TRADE_FEE_RATE": ("fee_rate", lambda: _env_float("ALPHAX_PAPER_TRADE_FEE_RATE", 0.001)), + "ALPHAX_PAPER_TRADE_SLIPPAGE_PCT": ("slippage_pct", lambda: _env_float("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", 0.05)), + "ALPHAX_PAPER_ORDER_GATE_ENABLED": ("order_gate_enabled", lambda: _env_bool("ALPHAX_PAPER_ORDER_GATE_ENABLED", True)), + "ALPHAX_PAPER_ENTRY_GATE_ENABLED": ("entry_gate_enabled", lambda: _env_bool("ALPHAX_PAPER_ENTRY_GATE_ENABLED", True)), + "ALPHAX_PAPER_ENTRY_MIN_REC_SCORE": ("entry_min_rec_score", lambda: _env_float("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", 50.0)), + "ALPHAX_PAPER_ENTRY_MIN_RR": ("entry_min_rr", lambda: _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", overrides.get("min_rr", 1.5))), + "ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT": ("max_stop_loss_leverage_risk_pct", lambda: _env_float("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", 20.0)), + "ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT": ("max_account_drawdown_pause_pct", lambda: _env_float("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", 3.0)), + "ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES": ("pause_after_weak_entries", lambda: _env_int("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", 3)), + "ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS": ("weak_entry_window_hours", lambda: _env_float("ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS", 6.0)), + "ALPHAX_PAPER_WEAK_ENTRY_MIN_MAX_PNL_PCT": ("weak_entry_min_max_pnl_pct", lambda: _env_float("ALPHAX_PAPER_WEAK_ENTRY_MIN_MAX_PNL_PCT", 1.0)), + "ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED": ("global_risk_gate_enabled", lambda: _env_bool("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", True)), + "ALPHAX_PAPER_GLOBAL_RISK_BLOCK_CRITICAL": ("global_risk_block_critical", lambda: _env_bool("ALPHAX_PAPER_GLOBAL_RISK_BLOCK_CRITICAL", False)), + "ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_MIN_REC_SCORE": ("global_risk_critical_min_rec_score", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_MIN_REC_SCORE", 80.0)), + "ALPHAX_PAPER_GLOBAL_RISK_MIN_POSITION_MULTIPLIER": ("global_risk_min_position_multiplier", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_MIN_POSITION_MULTIPLIER", 0.2)), + "ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE": ("global_risk_high_min_rec_score", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE", 70.0)), + "ALPHAX_PAPER_GLOBAL_RISK_HIGH_DRAWDOWN_PCT": ("global_risk_high_drawdown_pct", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_HIGH_DRAWDOWN_PCT", 3.0)), + "ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_DRAWDOWN_PCT": ("global_risk_critical_drawdown_pct", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_DRAWDOWN_PCT", 6.0)), + "ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS": ("global_risk_max_open_positions", lambda: _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS", 0)), + "ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS": ("global_risk_max_same_sector_positions", lambda: _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS", 3)), + "ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS": ("global_risk_max_same_direction_positions", lambda: _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS", 6)), + "ALPHAX_PAPER_ORDER_MIN_REC_SCORE": ("order_min_rec_score", lambda: _env_float("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", 50.0)), + "ALPHAX_PAPER_ORDER_MIN_RR": ("order_min_rr", lambda: _env_float("ALPHAX_PAPER_ORDER_MIN_RR", overrides.get("min_rr", 1.5))), + "ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK": ("order_require_risk_reward_ok", lambda: _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK", True)), + "ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT": ("order_min_distance_to_entry_pct", lambda: _env_float("ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT", 0.0)), + "ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT": ("order_max_distance_to_entry_pct", lambda: _env_float("ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT", 8.0)), + "ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER": ("order_require_current_trigger", lambda: _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER", False)), + "ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT": ("order_cancel_far_from_entry_pct", lambda: _env_float("ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT", 12.0)), + "ALPHAX_PAPER_ORDER_EXPIRE_HOURS": ("order_expire_hours", lambda: _env_float("ALPHAX_PAPER_ORDER_EXPIRE_HOURS", 24.0)), + } + for env_name, (key, loader) in checks.items(): + if _env_present(env_name): + overrides[key] = loader() + return overrides + + def default_live_trading_config(): return { "enabled": _env_bool("ALPHAX_LIVE_TRADING_ENABLED", False), @@ -457,7 +514,8 @@ def paper_trading_config(): if cfg is None: _seed_one("paper_trading", default_paper_trading_config(), "Paper trading account and execution model") cfg = get_paper_trading_config(default=None) - return deep_merge(default_paper_trading_config(), cfg or {}) + merged = deep_merge(default_paper_trading_config(), cfg or {}) + return deep_merge(merged, _paper_trading_env_overrides()) def live_trading_config(): diff --git a/app/db/paper_trading.py b/app/db/paper_trading.py index 48830a4..4b68af8 100644 --- a/app/db/paper_trading.py +++ b/app/db/paper_trading.py @@ -664,14 +664,20 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config: notional = default_notional_usdt(cfg) side = str(plan.get("side") or rec.get("side") or "long").strip().lower() or "long" leverage = default_leverage(cfg) - stop_loss = _safe_float(rec.get("stop_loss") or plan.get("stop_loss")) - tp1 = _safe_float(rec.get("tp1") or plan.get("tp1") or plan.get("take_profit_1")) + stop_loss = _safe_float(plan.get("stop_loss") or rec.get("stop_loss")) + tp1 = _safe_float(plan.get("tp1") or plan.get("take_profit_1") or rec.get("tp1")) rec_score = _safe_float(rec.get("rec_score") or rec.get("score")) if rec_score <= 0 and rec_id > 0: row = conn.execute("SELECT rec_score FROM recommendation WHERE id=%s", (rec_id,)).fetchone() rec_score = _safe_float(row["rec_score"] if row else 0) if bool(cfg.get("entry_gate_enabled", True)): - rr = _safe_float(plan.get("rr1") or plan.get("rr1_live")) or _trade_rr(side, entry_price, stop_loss, tp1) + calc_rr = _trade_rr(side, entry_price, stop_loss, tp1) + rr_candidates = [ + _safe_float(plan.get("rr1")), + _safe_float(plan.get("rr1_live")), + calc_rr, + ] + rr = max([x for x in rr_candidates if x > 0], default=0.0) min_rr = max(0.0, _safe_float(cfg.get("entry_min_rr"), 0)) min_score = max(0.0, _safe_float(cfg.get("entry_min_rec_score"), 0)) sl_risk = _stop_loss_leverage_risk_pct(side, entry_price, stop_loss, leverage) @@ -904,8 +910,8 @@ def _paper_order_gate(rec: dict, current_price: float, config: dict | None = Non plan = _entry_plan(rec) side = str(plan.get("side") or rec.get("side") or "long").strip().lower() or "long" target = _paper_order_target_price(rec) - stop_loss = _safe_float(rec.get("stop_loss") or plan.get("stop_loss")) - tp1 = _safe_float(rec.get("tp1") or plan.get("tp1") or plan.get("take_profit_1")) + stop_loss = _safe_float(plan.get("stop_loss") or rec.get("stop_loss")) + tp1 = _safe_float(plan.get("tp1") or plan.get("take_profit_1") or rec.get("tp1")) rr = _safe_float(plan.get("rr1") or plan.get("rr1_live")) calc_rr = _paper_order_rr(side, target, stop_loss, tp1) # Wait-pullback orders must be judged at the intended limit price, not at @@ -1043,9 +1049,9 @@ def _order_payload_from_rec(rec: dict, current_price: float, event_time: str, co "target_price": _paper_order_target_price(rec), "current_price_at_create": current_price, "notional_usdt": default_notional_usdt(cfg), - "stop_loss": _safe_float(rec.get("stop_loss") or plan.get("stop_loss")), - "tp1": _safe_float(rec.get("tp1") or plan.get("tp1") or plan.get("take_profit_1")), - "tp2": _safe_float(rec.get("tp2") or plan.get("tp2") or plan.get("take_profit_2")), + "stop_loss": _safe_float(plan.get("stop_loss") or rec.get("stop_loss")), + "tp1": _safe_float(plan.get("tp1") or plan.get("take_profit_1") or rec.get("tp1")), + "tp2": _safe_float(plan.get("tp2") or plan.get("take_profit_2") or rec.get("tp2")), "strategy_version": str(rec.get("strategy_version") or ""), "strategy_code": lineage["strategy_code"], "strategy_name": lineage["strategy_name"], diff --git a/app/db/recommendation_state.py b/app/db/recommendation_state.py index 91f0b60..22e992d 100644 --- a/app/db/recommendation_state.py +++ b/app/db/recommendation_state.py @@ -340,7 +340,7 @@ def derive_execution_fields(item): if item.get("latest_cache_updated_at"): item["current_price_updated_at"] = item.get("latest_cache_updated_at") entry_window = entry_window_policy( - item.get("entry_price") or entry_plan.get("entry_price") or 0, + entry_plan.get("entry_price") or item.get("entry_price") or 0, current_price_for_window, item.get("rec_time") or "", ) if action_status == "可即刻买入" else {} @@ -399,9 +399,9 @@ def derive_execution_fields(item): if entry_window and entry_window.get("status") != "active": item["entry_window_alert"] = entry_window item["risk_suggestion"] = risk_suggestion( - item.get("entry_price") or entry_plan.get("entry_price") or 0, - item.get("stop_loss") or entry_plan.get("stop_loss") or 0, - item.get("tp1") or entry_plan.get("tp1") or entry_plan.get("take_profit_1") or 0, + entry_plan.get("entry_price") or item.get("entry_price") or 0, + entry_plan.get("stop_loss") or item.get("stop_loss") or 0, + entry_plan.get("tp1") or entry_plan.get("take_profit_1") or item.get("tp1") or 0, ) item["market_context"] = market_context item["derivatives_context"] = derivatives_context diff --git a/static/opportunity_detail.html b/static/opportunity_detail.html index d1866bf..b662861 100644 --- a/static/opportunity_detail.html +++ b/static/opportunity_detail.html @@ -34,7 +34,7 @@ function decisionLog(r){var ep=r.entry_plan||{},mc=r.market_context||{};return e function signals(r){return Array.isArray(r.signal_labels)&&r.signal_labels.length?r.signal_labels:(Array.isArray(r.signals)?r.signals:[])} function aiInsight(r){return r.llm_insight&&r.llm_insight.content?r.llm_insight.content:null} function renderRows(rows,opts){opts=opts||{};if(!rows||!rows.length)return'
'+esc(statusLabel(r))+'
'+esc(clean(r.execution_reason||r.state_reason||dl.decision||'等待进一步确认'))+'
'+esc(clean(ep.entry_model||ep.entry_method||'--'))+'
'+esc(clean(ep.invalid_if||ep.stop_basis||'跌破风险边界或信号衰减'))+'
'+esc(statusLabel(r))+'
'+esc(clean(r.execution_reason||r.state_reason||dl.decision||'等待进一步确认'))+'
'+esc(clean(ep.entry_model||ep.entry_method||'--'))+'
'+esc(clean(ep.invalid_if||ep.stop_basis||'跌破风险边界或信号衰减'))+'