1
This commit is contained in:
parent
82b61bd808
commit
f6c95d4380
20
.env.example
20
.env.example
@ -76,12 +76,15 @@ ALPHAX_ONCHAIN_CANDIDATE_COOLDOWN_HOURS=6
|
||||
ALPHAX_ONCHAIN_WHALE_TX_USD=250000
|
||||
|
||||
# 策略交易挂单门控。wait_pullback 只是候选,必须通过这些条件才会创建挂单。
|
||||
ALPHAX_PAPER_TRADING_MODE=intraday_trading
|
||||
ALPHAX_PAPER_TARGET_TRADES_PER_DAY_MIN=3
|
||||
ALPHAX_PAPER_TARGET_TRADES_PER_DAY_MAX=5
|
||||
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_MIN_RR=1.5
|
||||
ALPHAX_PAPER_ENTRY_MIN_RR=1.5
|
||||
ALPHAX_PAPER_ENTRY_MIN_REC_SCORE=25
|
||||
ALPHAX_PAPER_MIN_RR=1.25
|
||||
ALPHAX_PAPER_ENTRY_MIN_RR=1.25
|
||||
ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT=20
|
||||
ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED=1
|
||||
ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN=3
|
||||
@ -91,22 +94,23 @@ ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS=6
|
||||
ALPHAX_PAPER_WEAK_ENTRY_MIN_MAX_PNL_PCT=1
|
||||
ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED=1
|
||||
ALPHAX_PAPER_GLOBAL_RISK_BLOCK_CRITICAL=0
|
||||
ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE=70
|
||||
ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_MIN_REC_SCORE=80
|
||||
ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE=25
|
||||
ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_MIN_REC_SCORE=25
|
||||
ALPHAX_PAPER_GLOBAL_RISK_SCORE_BLOCKS_INTRADAY=0
|
||||
ALPHAX_PAPER_GLOBAL_RISK_MIN_POSITION_MULTIPLIER=0.2
|
||||
ALPHAX_PAPER_GLOBAL_RISK_HIGH_DRAWDOWN_PCT=3
|
||||
ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_DRAWDOWN_PCT=6
|
||||
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.5
|
||||
ALPHAX_PAPER_ORDER_MIN_REC_SCORE=25
|
||||
ALPHAX_PAPER_ORDER_MIN_RR=1.25
|
||||
ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK=1
|
||||
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
|
||||
ALPHAX_PAPER_ORDER_EXPIRE_HOURS=24
|
||||
ALPHAX_PAPER_ORDER_EXPIRE_HOURS=8
|
||||
|
||||
# 策略交易移动止盈。volatility 会按持仓后实际高低价波动动态调整启动阈值和保护距离。
|
||||
ALPHAX_PAPER_TRAILING_STOP_ENABLED=1
|
||||
|
||||
@ -153,9 +153,12 @@ 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)
|
||||
paper_min_rr = _env_float("ALPHAX_PAPER_MIN_RR", 1.25)
|
||||
return {
|
||||
"enabled": _env_bool("ALPHAX_PAPER_TRADING_ENABLED", True),
|
||||
"trading_mode": _env_str("ALPHAX_PAPER_TRADING_MODE", "intraday_trading"),
|
||||
"target_trades_per_day_min": _env_int("ALPHAX_PAPER_TARGET_TRADES_PER_DAY_MIN", 3),
|
||||
"target_trades_per_day_max": _env_int("ALPHAX_PAPER_TARGET_TRADES_PER_DAY_MAX", 5),
|
||||
"account_equity_usdt": _env_float("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", 20000),
|
||||
"trade_notional_usdt": _env_float("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", 5000),
|
||||
"trade_leverage": _env_float("ALPHAX_PAPER_TRADE_LEVERAGE", 5),
|
||||
@ -164,10 +167,10 @@ def default_paper_trading_config():
|
||||
"slippage_pct": _env_float("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", 0.05),
|
||||
"trailing_stop_enabled": _env_bool("ALPHAX_PAPER_TRAILING_STOP_ENABLED", True),
|
||||
"trailing_mode": _env_str("ALPHAX_PAPER_TRAILING_MODE", "volatility"),
|
||||
"trailing_activate_pnl_pct": _env_float("ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT", 3.0),
|
||||
"trailing_activate_pnl_pct": _env_float("ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT", 2.0),
|
||||
"trailing_min_lock_profit_pct": _env_float("ALPHAX_PAPER_TRAILING_MIN_LOCK_PROFIT_PCT", 0.5),
|
||||
"trailing_distance_pct": _env_float("ALPHAX_PAPER_TRAILING_DISTANCE_PCT", 1.5),
|
||||
"trailing_volatility_min_activation_pct": _env_float("ALPHAX_PAPER_TRAILING_VOL_MIN_ACTIVATE_PCT", 2.5),
|
||||
"trailing_volatility_min_activation_pct": _env_float("ALPHAX_PAPER_TRAILING_VOL_MIN_ACTIVATE_PCT", 1.8),
|
||||
"trailing_volatility_max_activation_pct": _env_float("ALPHAX_PAPER_TRAILING_VOL_MAX_ACTIVATE_PCT", 8.0),
|
||||
"trailing_volatility_activation_mult": _env_float("ALPHAX_PAPER_TRAILING_VOL_ACTIVATE_MULT", 0.6),
|
||||
"trailing_volatility_min_distance_pct": _env_float("ALPHAX_PAPER_TRAILING_VOL_MIN_DISTANCE_PCT", 1.2),
|
||||
@ -177,7 +180,7 @@ def default_paper_trading_config():
|
||||
"trailing_move_push_min_step_pct": _env_float("ALPHAX_PAPER_TRAILING_MOVE_PUSH_MIN_STEP_PCT", 2.0),
|
||||
"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_rec_score": _env_float("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", 25.0),
|
||||
"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),
|
||||
@ -202,22 +205,23 @@ def default_paper_trading_config():
|
||||
"position_guard_critical_max_pnl_pct": _env_float("ALPHAX_PAPER_POSITION_GUARD_CRITICAL_MAX_PNL_PCT", 1.0),
|
||||
"global_risk_gate_enabled": _env_bool("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", True),
|
||||
"global_risk_block_critical": _env_bool("ALPHAX_PAPER_GLOBAL_RISK_BLOCK_CRITICAL", False),
|
||||
"global_risk_critical_min_rec_score": _env_float("ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_MIN_REC_SCORE", 80.0),
|
||||
"global_risk_critical_min_rec_score": _env_float("ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_MIN_REC_SCORE", 25.0),
|
||||
"global_risk_score_blocks_intraday": _env_bool("ALPHAX_PAPER_GLOBAL_RISK_SCORE_BLOCKS_INTRADAY", False),
|
||||
"global_risk_min_position_multiplier": _env_float("ALPHAX_PAPER_GLOBAL_RISK_MIN_POSITION_MULTIPLIER", 0.2),
|
||||
"global_risk_high_min_rec_score": _env_float("ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE", 70.0),
|
||||
"global_risk_high_min_rec_score": _env_float("ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE", 25.0),
|
||||
"global_risk_high_drawdown_pct": _env_float("ALPHAX_PAPER_GLOBAL_RISK_HIGH_DRAWDOWN_PCT", 3.0),
|
||||
"global_risk_critical_drawdown_pct": _env_float("ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_DRAWDOWN_PCT", 6.0),
|
||||
"global_risk_max_open_positions": _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS", 0),
|
||||
"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_rec_score": _env_float("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", 25.0),
|
||||
"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_min_distance_to_entry_pct": _env_float("ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT", 0.0),
|
||||
"order_max_distance_to_entry_pct": _env_float("ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT", 8.0),
|
||||
"order_require_current_trigger": _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER", False),
|
||||
"order_cancel_far_from_entry_pct": _env_float("ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT", 12.0),
|
||||
"order_expire_hours": _env_float("ALPHAX_PAPER_ORDER_EXPIRE_HOURS", 24.0),
|
||||
"order_expire_hours": _env_float("ALPHAX_PAPER_ORDER_EXPIRE_HOURS", 8.0),
|
||||
"trailing_tiers": [
|
||||
{"min_pnl_pct": 8.0, "distance_pct": 1.0, "label": "紧贴"},
|
||||
{"min_pnl_pct": 5.0, "distance_pct": 1.2, "label": "锁利"},
|
||||
@ -238,6 +242,9 @@ def _paper_trading_env_overrides():
|
||||
})
|
||||
checks = {
|
||||
"ALPHAX_PAPER_TRADING_ENABLED": ("enabled", lambda: _env_bool("ALPHAX_PAPER_TRADING_ENABLED", True)),
|
||||
"ALPHAX_PAPER_TRADING_MODE": ("trading_mode", lambda: _env_str("ALPHAX_PAPER_TRADING_MODE", "intraday_trading")),
|
||||
"ALPHAX_PAPER_TARGET_TRADES_PER_DAY_MIN": ("target_trades_per_day_min", lambda: _env_int("ALPHAX_PAPER_TARGET_TRADES_PER_DAY_MIN", 3)),
|
||||
"ALPHAX_PAPER_TARGET_TRADES_PER_DAY_MAX": ("target_trades_per_day_max", lambda: _env_int("ALPHAX_PAPER_TARGET_TRADES_PER_DAY_MAX", 5)),
|
||||
"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)),
|
||||
@ -246,8 +253,8 @@ def _paper_trading_env_overrides():
|
||||
"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_ENTRY_MIN_REC_SCORE": ("entry_min_rec_score", lambda: _env_float("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", 25.0)),
|
||||
"ALPHAX_PAPER_ENTRY_MIN_RR": ("entry_min_rr", lambda: _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", overrides.get("min_rr", 1.25))),
|
||||
"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_DYNAMIC_LEVERAGE_ENABLED": ("dynamic_leverage_enabled", lambda: _env_bool("ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED", True)),
|
||||
"ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN": ("dynamic_leverage_min", lambda: _env_float("ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN", 3.0)),
|
||||
@ -270,22 +277,23 @@ def _paper_trading_env_overrides():
|
||||
"ALPHAX_PAPER_POSITION_GUARD_CRITICAL_MAX_PNL_PCT": ("position_guard_critical_max_pnl_pct", lambda: _env_float("ALPHAX_PAPER_POSITION_GUARD_CRITICAL_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_CRITICAL_MIN_REC_SCORE": ("global_risk_critical_min_rec_score", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_MIN_REC_SCORE", 25.0)),
|
||||
"ALPHAX_PAPER_GLOBAL_RISK_SCORE_BLOCKS_INTRADAY": ("global_risk_score_blocks_intraday", lambda: _env_bool("ALPHAX_PAPER_GLOBAL_RISK_SCORE_BLOCKS_INTRADAY", False)),
|
||||
"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_MIN_REC_SCORE": ("global_risk_high_min_rec_score", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE", 25.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_MIN_REC_SCORE": ("order_min_rec_score", lambda: _env_float("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", 25.0)),
|
||||
"ALPHAX_PAPER_ORDER_MIN_RR": ("order_min_rr", lambda: _env_float("ALPHAX_PAPER_ORDER_MIN_RR", overrides.get("min_rr", 1.25))),
|
||||
"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)),
|
||||
"ALPHAX_PAPER_ORDER_EXPIRE_HOURS": ("order_expire_hours", lambda: _env_float("ALPHAX_PAPER_ORDER_EXPIRE_HOURS", 8.0)),
|
||||
}
|
||||
for env_name, (key, loader) in checks.items():
|
||||
if _env_present(env_name):
|
||||
@ -545,7 +553,46 @@ def paper_trading_config():
|
||||
_seed_one("paper_trading", default_paper_trading_config(), "Paper trading account and execution model")
|
||||
cfg = get_paper_trading_config(default=None)
|
||||
merged = deep_merge(default_paper_trading_config(), cfg or {})
|
||||
return deep_merge(merged, _paper_trading_env_overrides())
|
||||
merged = deep_merge(merged, _paper_trading_env_overrides())
|
||||
if str(merged.get("trading_mode") or "").strip() == "intraday_trading":
|
||||
intraday_defaults = {
|
||||
"min_rr": 1.25,
|
||||
"entry_min_rr": 1.25,
|
||||
"order_min_rr": 1.25,
|
||||
"entry_min_rec_score": 25.0,
|
||||
"order_min_rec_score": 25.0,
|
||||
"global_risk_high_min_rec_score": 25.0,
|
||||
"global_risk_critical_min_rec_score": 25.0,
|
||||
"global_risk_score_blocks_intraday": False,
|
||||
"order_min_distance_to_entry_pct": 0.0,
|
||||
"order_expire_hours": 8.0,
|
||||
"trailing_activate_pnl_pct": 2.0,
|
||||
"trailing_volatility_min_activation_pct": 1.8,
|
||||
}
|
||||
explicit = _paper_trading_env_overrides()
|
||||
for key, value in intraday_defaults.items():
|
||||
if key in explicit:
|
||||
continue
|
||||
current = merged.get(key)
|
||||
# Treat old seeded defaults as compatibility noise, while still
|
||||
# honoring deliberately more active manual settings.
|
||||
if key in {
|
||||
"min_rr",
|
||||
"entry_min_rr",
|
||||
"order_min_rr",
|
||||
"entry_min_rec_score",
|
||||
"order_min_rec_score",
|
||||
"global_risk_high_min_rec_score",
|
||||
"global_risk_critical_min_rec_score",
|
||||
"order_min_distance_to_entry_pct",
|
||||
"order_expire_hours",
|
||||
"trailing_activate_pnl_pct",
|
||||
"trailing_volatility_min_activation_pct",
|
||||
} and _env_float("__ALPHAX_UNUSED__", value) < _env_float("__ALPHAX_UNUSED__", current or value):
|
||||
merged[key] = value
|
||||
elif key == "global_risk_score_blocks_intraday" and current is None:
|
||||
merged[key] = value
|
||||
return merged
|
||||
|
||||
|
||||
def live_trading_config():
|
||||
|
||||
@ -191,6 +191,9 @@ def evaluate_global_risk(
|
||||
rec_score = _safe_float((rec or {}).get("rec_score") or (rec or {}).get("score"))
|
||||
min_score_high = max(0.0, _safe_float(cfg.get("global_risk_high_min_rec_score"), 70.0))
|
||||
min_score_critical = max(min_score_high, _safe_float(cfg.get("global_risk_critical_min_rec_score"), 80.0))
|
||||
trading_mode = str(cfg.get("trading_mode") or "").strip()
|
||||
score_blocks_intraday = bool(cfg.get("global_risk_score_blocks_intraday", False))
|
||||
intraday_soft_risk = trading_mode == "intraday_trading" and not score_blocks_intraday
|
||||
min_position_multiplier = max(0.0, _safe_float(cfg.get("global_risk_min_position_multiplier"), 0.2))
|
||||
max_drawdown_critical = max(0.0, _safe_float(cfg.get("global_risk_critical_drawdown_pct"), 6.0))
|
||||
max_drawdown_high = max(0.0, _safe_float(cfg.get("global_risk_high_drawdown_pct"), 3.0))
|
||||
@ -213,17 +216,23 @@ def evaluate_global_risk(
|
||||
risk_level = "high"
|
||||
reasons.append("账户浮亏偏高,只允许高质量机会")
|
||||
|
||||
if risk_level == "critical" and bool(cfg.get("global_risk_block_critical", False)):
|
||||
if risk_level == "critical" and bool(cfg.get("global_risk_block_critical", False)) and not intraday_soft_risk:
|
||||
allow = False
|
||||
decision = "block_critical"
|
||||
elif risk_level == "critical" and drawdown < max_drawdown_critical:
|
||||
if rec_score < min_score_critical:
|
||||
if intraday_soft_risk:
|
||||
decision = "allow_reduced_size"
|
||||
reasons.append(f"日内模式下 critical 市场风险只做仓位调节,不按推荐分 {rec_score:.1f} 拒单")
|
||||
elif rec_score < min_score_critical:
|
||||
allow = False
|
||||
decision = "block_critical_weak_score"
|
||||
reasons.append(f"critical 市场环境下推荐分 {rec_score:.1f} 低于 {min_score_critical:.1f}")
|
||||
else:
|
||||
decision = "allow_reduced_size"
|
||||
reasons.append(f"critical 市场环境不再一刀切,按 {position_multiplier:.0%} 仓位试运行")
|
||||
elif risk_level == "high" and intraday_soft_risk:
|
||||
decision = "allow_reduced_size" if position_multiplier < 1 else "allow"
|
||||
reasons.append(f"日内模式下 high 市场风险只做仓位调节,不按推荐分 {rec_score:.1f} 拒单")
|
||||
elif risk_level == "high" and rec_score < min_score_high:
|
||||
allow = False
|
||||
decision = "block_high_weak_score"
|
||||
@ -268,6 +277,8 @@ def evaluate_global_risk(
|
||||
"high_drawdown_pct": max_drawdown_high,
|
||||
"min_score_when_high_risk": min_score_high,
|
||||
"min_score_when_critical_risk": min_score_critical,
|
||||
"trading_mode": trading_mode,
|
||||
"intraday_soft_risk": intraday_soft_risk,
|
||||
"reasons": reasons,
|
||||
"market_regime": regime,
|
||||
"portfolio": portfolio,
|
||||
|
||||
@ -26,6 +26,7 @@ class StrategyDefinition:
|
||||
strategy_name: str
|
||||
description: str = ""
|
||||
direction: str = "long"
|
||||
frequency_profile: str = "intraday"
|
||||
mode: str = "paper_only"
|
||||
status: str = "active"
|
||||
entry_gate_config: dict = field(default_factory=dict)
|
||||
@ -38,19 +39,28 @@ STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
|
||||
strategy_name="多头动量启动",
|
||||
description="15m当前突破叠加1H成交量/波动增强,捕捉山寨币日内到1-3天启动段。",
|
||||
direction="long",
|
||||
frequency_profile="intraday",
|
||||
mode="paper_enabled",
|
||||
entry_gate_config={
|
||||
"min_entry_score_buy_now": 3,
|
||||
"min_entry_score_wait_pullback": 2,
|
||||
"min_rr_buy_now": 1.5,
|
||||
"min_rr_buy_now": 1.25,
|
||||
"breakout_distance_wait_pct": 8,
|
||||
"gain_24h_wait_pct": 10,
|
||||
},
|
||||
paper_config={
|
||||
"entry_min_rr": 1.5,
|
||||
"order_min_rr": 1.5,
|
||||
"frequency_profile": "intraday_trading",
|
||||
"target_trades_per_day_min": 3,
|
||||
"target_trades_per_day_max": 5,
|
||||
"entry_min_rec_score": 25,
|
||||
"order_min_rec_score": 25,
|
||||
"entry_min_rr": 1.25,
|
||||
"order_min_rr": 1.25,
|
||||
"order_min_distance_to_entry_pct": 0,
|
||||
"order_require_current_trigger": True,
|
||||
"order_expire_hours": 8,
|
||||
"trailing_activate_pnl_pct": 2.0,
|
||||
"trailing_volatility_min_activation_pct": 1.8,
|
||||
"dynamic_leverage_enabled": True,
|
||||
"dynamic_leverage_min": 3,
|
||||
},
|
||||
@ -60,20 +70,29 @@ STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
|
||||
strategy_name="多头二波回踩",
|
||||
description="强势榜或放量币第一波后回踩EMA、箱体上沿或前高转支撑,再次承接的短线策略。",
|
||||
direction="long",
|
||||
frequency_profile="intraday",
|
||||
mode="paper_enabled",
|
||||
entry_gate_config={
|
||||
"min_entry_score_buy_now": 2,
|
||||
"min_entry_score_wait_pullback": 0,
|
||||
"min_rr_buy_now": 1.5,
|
||||
"min_rr_buy_now": 1.25,
|
||||
"max_wait_pullback_deviation_pct": 10,
|
||||
"breakout_distance_wait_pct": 12,
|
||||
"gain_24h_wait_pct": 18,
|
||||
},
|
||||
paper_config={
|
||||
"entry_min_rr": 1.5,
|
||||
"order_min_rr": 1.5,
|
||||
"order_min_distance_to_entry_pct": 1.5,
|
||||
"frequency_profile": "intraday_trading",
|
||||
"target_trades_per_day_min": 3,
|
||||
"target_trades_per_day_max": 5,
|
||||
"entry_min_rec_score": 22,
|
||||
"order_min_rec_score": 22,
|
||||
"entry_min_rr": 1.25,
|
||||
"order_min_rr": 1.25,
|
||||
"order_min_distance_to_entry_pct": 0,
|
||||
"order_require_current_trigger": False,
|
||||
"order_expire_hours": 10,
|
||||
"trailing_activate_pnl_pct": 2.0,
|
||||
"trailing_volatility_min_activation_pct": 1.8,
|
||||
"dynamic_leverage_enabled": True,
|
||||
"dynamic_leverage_min": 3,
|
||||
},
|
||||
@ -83,20 +102,29 @@ STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
|
||||
strategy_name="空头破位反抽",
|
||||
description="1H支撑或箱体下沿破位后反抽失败,叠加15m弱确认和相对弱势的空头短线策略。",
|
||||
direction="short",
|
||||
frequency_profile="intraday",
|
||||
mode="paper_enabled",
|
||||
entry_gate_config={
|
||||
"direction": "short",
|
||||
"min_entry_score_buy_now": 2,
|
||||
"min_entry_score_wait_pullback": 1,
|
||||
"min_rr_buy_now": 1.5,
|
||||
"min_rr_buy_now": 1.3,
|
||||
"breakdown_distance_wait_pct": 10,
|
||||
"max_retest_deviation_pct": 8,
|
||||
},
|
||||
paper_config={
|
||||
"entry_min_rr": 1.8,
|
||||
"order_min_rr": 1.8,
|
||||
"frequency_profile": "intraday_trading",
|
||||
"target_trades_per_day_min": 3,
|
||||
"target_trades_per_day_max": 5,
|
||||
"entry_min_rec_score": 18,
|
||||
"order_min_rec_score": 18,
|
||||
"entry_min_rr": 1.3,
|
||||
"order_min_rr": 1.3,
|
||||
"order_min_distance_to_entry_pct": 0,
|
||||
"order_require_current_trigger": True,
|
||||
"order_require_current_trigger": False,
|
||||
"order_expire_hours": 8,
|
||||
"trailing_activate_pnl_pct": 2.0,
|
||||
"trailing_volatility_min_activation_pct": 1.8,
|
||||
"dynamic_leverage_enabled": True,
|
||||
"dynamic_leverage_min": 2,
|
||||
},
|
||||
|
||||
@ -11,6 +11,7 @@ from app.db.altcoin_db import (
|
||||
)
|
||||
from app.core.opportunity_funnel import screening_stage_meta, stage_label
|
||||
from app.core.strategy_registry import normalize_strategy_code, strategy_label
|
||||
from app.db.intraday_frequency import get_intraday_frequency_health
|
||||
from app.db.schema import get_conn
|
||||
|
||||
|
||||
@ -1411,6 +1412,7 @@ def get_pipeline_runs(limit=30, hours=24, offset=0):
|
||||
current_page = (offset // limit) + 1 if total_count else 0
|
||||
return {
|
||||
"kpi": kpi,
|
||||
"intraday_frequency": get_intraday_frequency_health(days=max(1, min((hours + 23) // 24, 7))),
|
||||
"runs": runs,
|
||||
"pagination": {
|
||||
"hours": hours,
|
||||
|
||||
247
app/db/intraday_frequency.py
Normal file
247
app/db/intraday_frequency.py
Normal file
@ -0,0 +1,247 @@
|
||||
"""Intraday trading frequency health read model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections import Counter
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.config.system_config import paper_trading_config
|
||||
from app.db.schema import get_conn
|
||||
|
||||
|
||||
def _safe_int(value, default=0):
|
||||
try:
|
||||
return int(value or 0)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _safe_float(value, default=0.0):
|
||||
try:
|
||||
return float(value or 0)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _loads(value, fallback=None):
|
||||
try:
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, str) and value.strip():
|
||||
return json.loads(value)
|
||||
except Exception:
|
||||
pass
|
||||
return fallback if fallback is not None else {}
|
||||
|
||||
|
||||
def _since(days):
|
||||
return (datetime.now() - timedelta(days=max(1, min(_safe_int(days, 1), 30)))).isoformat()
|
||||
|
||||
|
||||
def _date_expr(column):
|
||||
return f"substr({column},1,10)"
|
||||
|
||||
|
||||
def _counter_items(counter: Counter, limit=10):
|
||||
return [{"reason": key, "count": count} for key, count in counter.most_common(limit)]
|
||||
|
||||
|
||||
def _extract_gate_reasons(rows):
|
||||
counter = Counter()
|
||||
samples = []
|
||||
for row in rows:
|
||||
detail = _loads(row.get("detail_json"), {})
|
||||
reason = str(detail.get("reason") or "").strip()
|
||||
if reason:
|
||||
counter[reason] += 1
|
||||
gate_reasons = detail.get("gate_reasons")
|
||||
if isinstance(gate_reasons, list):
|
||||
for item in gate_reasons:
|
||||
if item:
|
||||
counter[str(item)] += 1
|
||||
risk_detail = detail.get("risk_detail") if isinstance(detail.get("risk_detail"), dict) else {}
|
||||
decision = str(risk_detail.get("decision") or "").strip()
|
||||
if decision:
|
||||
counter[decision] += 1
|
||||
samples.append({
|
||||
"time": row.get("event_time") or "",
|
||||
"symbol": row.get("symbol") or "",
|
||||
"strategy_code": row.get("strategy_code") or detail.get("strategy_code") or "",
|
||||
"reason": reason or (gate_reasons[0] if isinstance(gate_reasons, list) and gate_reasons else decision),
|
||||
"detail": detail,
|
||||
})
|
||||
return _counter_items(counter), samples[:12]
|
||||
|
||||
|
||||
def get_intraday_frequency_health(days: int = 1) -> dict:
|
||||
"""Summarize whether the intraday paper-trading funnel is active enough."""
|
||||
days = max(1, min(_safe_int(days, 1), 30))
|
||||
since = _since(days)
|
||||
cfg = paper_trading_config()
|
||||
target_min = max(0, _safe_int(cfg.get("target_trades_per_day_min"), 3))
|
||||
target_max = max(target_min, _safe_int(cfg.get("target_trades_per_day_max"), 5))
|
||||
conn = get_conn()
|
||||
try:
|
||||
screening = conn.execute(
|
||||
f"""
|
||||
SELECT {_date_expr('scan_time')} AS d, layer, state, COUNT(*) AS c
|
||||
FROM screening_log
|
||||
WHERE scan_time >= %s
|
||||
GROUP BY d, layer, state
|
||||
ORDER BY d DESC
|
||||
""",
|
||||
(since,),
|
||||
).fetchall()
|
||||
recommendations = conn.execute(
|
||||
f"""
|
||||
SELECT {_date_expr('rec_time')} AS d, execution_status, strategy_code, direction, COUNT(*) AS c
|
||||
FROM recommendation
|
||||
WHERE rec_time >= %s
|
||||
GROUP BY d, execution_status, strategy_code, direction
|
||||
ORDER BY d DESC
|
||||
""",
|
||||
(since,),
|
||||
).fetchall()
|
||||
orders = conn.execute(
|
||||
f"""
|
||||
SELECT {_date_expr('created_at')} AS d, status, strategy_code, side, COUNT(*) AS c
|
||||
FROM paper_orders
|
||||
WHERE created_at >= %s
|
||||
GROUP BY d, status, strategy_code, side
|
||||
ORDER BY d DESC
|
||||
""",
|
||||
(since,),
|
||||
).fetchall()
|
||||
trades = conn.execute(
|
||||
f"""
|
||||
SELECT {_date_expr('opened_at')} AS d, status, strategy_code, side, COUNT(*) AS c
|
||||
FROM paper_trades
|
||||
WHERE opened_at >= %s
|
||||
GROUP BY d, status, strategy_code, side
|
||||
ORDER BY d DESC
|
||||
""",
|
||||
(since,),
|
||||
).fetchall()
|
||||
gate_rows = [dict(r) for r in conn.execute(
|
||||
"""
|
||||
SELECT event_time, symbol, strategy_code, detail_json
|
||||
FROM paper_trade_events
|
||||
WHERE event_time >= %s AND event_type IN ('paper_gate_reject','paper_order_cancel')
|
||||
ORDER BY event_time DESC, id DESC
|
||||
LIMIT 200
|
||||
""",
|
||||
(since,),
|
||||
).fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
by_day = {}
|
||||
for row in screening:
|
||||
d = row["d"] or "unknown"
|
||||
item = by_day.setdefault(d, _empty_day(d))
|
||||
layer = str(row["layer"] or "")
|
||||
state = str(row["state"] or "")
|
||||
count = _safe_int(row["c"])
|
||||
if layer == "粗筛":
|
||||
item["rough_candidates"] += count
|
||||
elif layer == "细筛":
|
||||
item["quality_candidates"] += count
|
||||
elif layer == "确认":
|
||||
item["confirmed_candidates"] += count
|
||||
elif layer == "universe_gate":
|
||||
item["universe_filtered"] += count
|
||||
item["screening_breakdown"].append({"layer": layer, "state": state, "count": count})
|
||||
|
||||
for row in recommendations:
|
||||
d = row["d"] or "unknown"
|
||||
item = by_day.setdefault(d, _empty_day(d))
|
||||
status = str(row["execution_status"] or "")
|
||||
count = _safe_int(row["c"])
|
||||
item["recommendations"] += count
|
||||
if status in {"buy_now", "wait_pullback"}:
|
||||
item["actionable_opportunities"] += count
|
||||
elif status == "observe":
|
||||
item["observe_opportunities"] += count
|
||||
item["recommendation_breakdown"].append(dict(row))
|
||||
|
||||
for row in orders:
|
||||
d = row["d"] or "unknown"
|
||||
item = by_day.setdefault(d, _empty_day(d))
|
||||
status = str(row["status"] or "")
|
||||
count = _safe_int(row["c"])
|
||||
item["paper_orders"] += count
|
||||
if status == "pending":
|
||||
item["pending_orders"] += count
|
||||
elif status == "filled":
|
||||
item["filled_orders"] += count
|
||||
elif status in {"canceled", "expired"}:
|
||||
item["canceled_or_expired_orders"] += count
|
||||
item["order_breakdown"].append(dict(row))
|
||||
|
||||
for row in trades:
|
||||
d = row["d"] or "unknown"
|
||||
item = by_day.setdefault(d, _empty_day(d))
|
||||
count = _safe_int(row["c"])
|
||||
item["paper_trades"] += count
|
||||
item["trade_breakdown"].append(dict(row))
|
||||
|
||||
gate_reasons, gate_samples = _extract_gate_reasons(gate_rows)
|
||||
days_list = sorted(by_day.values(), key=lambda x: x["date"], reverse=True)
|
||||
for item in days_list:
|
||||
item["paper_actions"] = item["paper_orders"] + item["paper_trades"]
|
||||
converted = item["paper_trades"] + item["filled_orders"] + item["pending_orders"]
|
||||
item["paper_converted_count"] = converted
|
||||
item["trade_conversion_pct"] = round(min(converted, item["actionable_opportunities"]) / item["actionable_opportunities"] * 100, 1) if item["actionable_opportunities"] else 0
|
||||
if item["paper_actions"] < target_min:
|
||||
item["health_status"] = "low_frequency"
|
||||
item["health_label"] = f"低于日内目标 {target_min}-{target_max} 单"
|
||||
elif item["paper_actions"] > target_max:
|
||||
item["health_status"] = "high_frequency"
|
||||
item["health_label"] = f"高于日内目标 {target_min}-{target_max} 单"
|
||||
else:
|
||||
item["health_status"] = "healthy"
|
||||
item["health_label"] = f"达到日内目标 {target_min}-{target_max} 单"
|
||||
|
||||
latest = days_list[0] if days_list else _empty_day(datetime.now().date().isoformat())
|
||||
return {
|
||||
"mode": cfg.get("trading_mode") or "intraday_trading",
|
||||
"target_trades_per_day": {"min": target_min, "max": target_max},
|
||||
"days": days,
|
||||
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
||||
"latest": latest,
|
||||
"daily": days_list,
|
||||
"gate_reasons": gate_reasons,
|
||||
"gate_samples": gate_samples,
|
||||
"definition": "日内频率健康度只衡量机会到策略交易账本的转化,不代表收益质量。",
|
||||
}
|
||||
|
||||
|
||||
def _empty_day(day):
|
||||
return {
|
||||
"date": day,
|
||||
"universe_filtered": 0,
|
||||
"rough_candidates": 0,
|
||||
"quality_candidates": 0,
|
||||
"confirmed_candidates": 0,
|
||||
"recommendations": 0,
|
||||
"actionable_opportunities": 0,
|
||||
"observe_opportunities": 0,
|
||||
"paper_orders": 0,
|
||||
"pending_orders": 0,
|
||||
"filled_orders": 0,
|
||||
"canceled_or_expired_orders": 0,
|
||||
"paper_trades": 0,
|
||||
"paper_actions": 0,
|
||||
"paper_converted_count": 0,
|
||||
"trade_conversion_pct": 0,
|
||||
"health_status": "unknown",
|
||||
"health_label": "暂无数据",
|
||||
"screening_breakdown": [],
|
||||
"recommendation_breakdown": [],
|
||||
"order_breakdown": [],
|
||||
"trade_breakdown": [],
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["get_intraday_frequency_health"]
|
||||
@ -318,6 +318,24 @@ def _strategy_code_from_rec(rec: dict) -> str:
|
||||
def _paper_cfg_for_rec(rec: dict, config: dict | None = None) -> dict:
|
||||
cfg = dict(_paper_cfg(config) or {})
|
||||
cfg.update(strategy_paper_config(_strategy_code_from_rec(rec)))
|
||||
# Strategy configs tune the intraday defaults, but explicit environment
|
||||
# overrides remain the emergency/manual control surface for local and prod.
|
||||
env_float_keys = {
|
||||
"ALPHAX_PAPER_ENTRY_MIN_REC_SCORE": "entry_min_rec_score",
|
||||
"ALPHAX_PAPER_ENTRY_MIN_RR": "entry_min_rr",
|
||||
"ALPHAX_PAPER_ORDER_MIN_REC_SCORE": "order_min_rec_score",
|
||||
"ALPHAX_PAPER_ORDER_MIN_RR": "order_min_rr",
|
||||
"ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT": "order_min_distance_to_entry_pct",
|
||||
"ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT": "order_max_distance_to_entry_pct",
|
||||
"ALPHAX_PAPER_ORDER_EXPIRE_HOURS": "order_expire_hours",
|
||||
"ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT": "trailing_activate_pnl_pct",
|
||||
"ALPHAX_PAPER_TRAILING_VOL_MIN_ACTIVATE_PCT": "trailing_volatility_min_activation_pct",
|
||||
}
|
||||
for env_name, key in env_float_keys.items():
|
||||
if os.getenv(env_name) is not None:
|
||||
cfg[key] = _safe_float(os.getenv(env_name), cfg.get(key))
|
||||
if os.getenv("ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER") is not None:
|
||||
cfg["order_require_current_trigger"] = str(os.getenv("ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER") or "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
return cfg
|
||||
|
||||
|
||||
@ -541,6 +559,33 @@ def _record_event(conn, trade_id: int, rec_id: int, symbol: str, event_type: str
|
||||
)
|
||||
|
||||
|
||||
def _record_recommendation_event(conn, rec: dict, event_type: str, message: str, detail=None, event_time: str = "", price: float = 0.0):
|
||||
rec = rec or {}
|
||||
plan = _entry_plan(rec)
|
||||
lineage = _strategy_lineage_from_rec(rec)
|
||||
event_detail = {
|
||||
**(detail or {}),
|
||||
"strategy_code": lineage["strategy_code"],
|
||||
"strategy_name": lineage["strategy_name"],
|
||||
"strategy_signal_id": lineage["strategy_signal_id"],
|
||||
"execution_status": rec.get("execution_status") or "",
|
||||
"action_status": rec.get("action_status") or plan.get("entry_action") or "",
|
||||
"entry_plan": plan,
|
||||
}
|
||||
_record_event(
|
||||
conn,
|
||||
0,
|
||||
_safe_int(rec.get("id")),
|
||||
str(rec.get("symbol") or "").strip().upper(),
|
||||
event_type,
|
||||
_safe_float(price or rec.get("current_price") or rec.get("entry_price")),
|
||||
0.0,
|
||||
message,
|
||||
event_detail,
|
||||
event_time,
|
||||
)
|
||||
|
||||
|
||||
def _fmt_price(value) -> str:
|
||||
price = _safe_float(value)
|
||||
if price <= 0:
|
||||
@ -768,6 +813,31 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
|
||||
if max_sl_risk > 0 and sl_risk > max_sl_risk:
|
||||
entry_reasons.append("stop_loss_leverage_risk_exceeded")
|
||||
if entry_reasons:
|
||||
_record_recommendation_event(
|
||||
conn,
|
||||
rec,
|
||||
"paper_gate_reject",
|
||||
"日内策略交易入场门禁拒绝",
|
||||
{
|
||||
"reason": "entry_gate_rejected",
|
||||
"gate_reasons": entry_reasons,
|
||||
"gate_detail": {
|
||||
"rec_score": rec_score,
|
||||
"min_rec_score": min_score,
|
||||
"rr1": round(rr, 4) if rr > 0 else 0,
|
||||
"min_rr": min_rr,
|
||||
"stop_loss_leverage_risk_pct": sl_risk,
|
||||
"max_stop_loss_leverage_risk_pct": max_sl_risk,
|
||||
"entry_price": entry_price,
|
||||
"stop_loss": stop_loss,
|
||||
"tp1": tp1,
|
||||
"leverage": leverage,
|
||||
"leverage_risk": leverage_risk,
|
||||
},
|
||||
},
|
||||
event_time,
|
||||
entry_price,
|
||||
)
|
||||
return {
|
||||
"opened": False,
|
||||
"skipped": True,
|
||||
@ -791,6 +861,15 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
|
||||
if global_detail is None:
|
||||
global_ok, global_detail = _global_risk_entry_check(conn, rec, notional, cfg)
|
||||
if not global_ok:
|
||||
_record_recommendation_event(
|
||||
conn,
|
||||
rec,
|
||||
"paper_gate_reject",
|
||||
"日内策略交易全局风控拒绝",
|
||||
{"reason": "global_risk_rejected", "risk_detail": global_detail},
|
||||
event_time,
|
||||
entry_price,
|
||||
)
|
||||
return {
|
||||
"opened": False,
|
||||
"skipped": True,
|
||||
@ -811,9 +890,27 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
|
||||
plan["leverage_sizing"] = leverage_risk
|
||||
pause_ok, pause_reason, pause_detail = _portfolio_entry_pause_check(conn, notional, event_time, cfg)
|
||||
if not pause_ok:
|
||||
_record_recommendation_event(
|
||||
conn,
|
||||
rec,
|
||||
"paper_gate_reject",
|
||||
"日内策略交易账户暂停门禁拒绝",
|
||||
{"reason": pause_reason, "risk_detail": pause_detail},
|
||||
event_time,
|
||||
entry_price,
|
||||
)
|
||||
return {"opened": False, "skipped": True, "reason": pause_reason, "risk_detail": pause_detail}
|
||||
leverage_ok, leverage_detail = _cumulative_leverage_check(conn, notional, cfg, exclude_rec_id=rec_id)
|
||||
if not leverage_ok:
|
||||
_record_recommendation_event(
|
||||
conn,
|
||||
rec,
|
||||
"paper_gate_reject",
|
||||
"日内策略交易累计杠杆门禁拒绝",
|
||||
{"reason": "cumulative_leverage_exceeded", "risk_detail": leverage_detail},
|
||||
event_time,
|
||||
entry_price,
|
||||
)
|
||||
return {
|
||||
"opened": False,
|
||||
"skipped": True,
|
||||
@ -1046,6 +1143,28 @@ def _cancel_paper_order(conn, order: dict, reason: str, event_time: str) -> dict
|
||||
""",
|
||||
(reason, event_time, event_time, order["id"]),
|
||||
)
|
||||
_record_recommendation_event(
|
||||
conn,
|
||||
{
|
||||
"id": order.get("recommendation_id"),
|
||||
"symbol": order.get("symbol"),
|
||||
"strategy_code": order.get("strategy_code"),
|
||||
"strategy_signal_id": order.get("strategy_signal_id"),
|
||||
"entry_plan_json": order.get("entry_plan_snapshot_json"),
|
||||
"execution_status": order.get("source_status"),
|
||||
"action_status": order.get("source_action"),
|
||||
},
|
||||
"paper_order_cancel",
|
||||
"日内策略挂单取消",
|
||||
{
|
||||
"reason": reason,
|
||||
"paper_order_id": order.get("id"),
|
||||
"target_price": order.get("target_price"),
|
||||
"current_price_at_create": order.get("current_price_at_create"),
|
||||
},
|
||||
event_time,
|
||||
order.get("target_price") or 0,
|
||||
)
|
||||
return {"skipped": True, "reason": f"paper_order_{reason}", "paper_order_id": order["id"]}
|
||||
|
||||
|
||||
@ -1256,6 +1375,19 @@ def _sync_wait_pullback_order(conn, rec: dict, current_price: float, event_time:
|
||||
|
||||
gate_ok, gate_reasons, gate_detail = _paper_order_gate(rec, current_price, cfg, conn=conn)
|
||||
if not gate_ok:
|
||||
_record_recommendation_event(
|
||||
conn,
|
||||
rec,
|
||||
"paper_gate_reject",
|
||||
"日内策略挂单门禁拒绝",
|
||||
{
|
||||
"reason": "paper_order_gate_rejected",
|
||||
"gate_reasons": gate_reasons,
|
||||
"gate_detail": gate_detail,
|
||||
},
|
||||
event_time,
|
||||
current_price,
|
||||
)
|
||||
return {
|
||||
"skipped": True,
|
||||
"reason": "paper_order_gate_rejected",
|
||||
@ -1267,6 +1399,15 @@ def _sync_wait_pullback_order(conn, rec: dict, current_price: float, event_time:
|
||||
|
||||
payload = _order_payload_from_rec(rec, current_price, event_time, cfg)
|
||||
if payload["recommendation_id"] <= 0 or not payload["symbol"] or payload["target_price"] <= 0:
|
||||
_record_recommendation_event(
|
||||
conn,
|
||||
rec,
|
||||
"paper_gate_reject",
|
||||
"日内策略挂单参数无效",
|
||||
{"reason": "invalid_paper_order", "payload": payload},
|
||||
event_time,
|
||||
current_price,
|
||||
)
|
||||
return {"skipped": True, "reason": "invalid_paper_order"}
|
||||
row = conn.execute(
|
||||
"""
|
||||
|
||||
@ -12,6 +12,7 @@ import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.db.paper_trading import get_paper_trading_summary
|
||||
from app.db.intraday_frequency import get_intraday_frequency_health
|
||||
from app.db.schema import get_conn
|
||||
from app.db.strategy_insights import get_strategy_evaluation, get_strategy_insights
|
||||
|
||||
@ -389,6 +390,7 @@ def get_review_center_dashboard(days=30):
|
||||
evidence = _evidence_review(conn, since)
|
||||
iteration = _iteration_review(conn, since)
|
||||
strategy_evaluation = get_strategy_evaluation(days=days)
|
||||
intraday_frequency = get_intraday_frequency_health(days=min(days, 7))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@ -403,6 +405,7 @@ def get_review_center_dashboard(days=30):
|
||||
],
|
||||
"opportunity": opportunity,
|
||||
"strategy_evaluation": strategy_evaluation,
|
||||
"intraday_frequency": intraday_frequency,
|
||||
"paper_trading": paper,
|
||||
"evidence": evidence,
|
||||
"iteration": iteration,
|
||||
|
||||
@ -141,6 +141,16 @@ def _strategy_context_for_recommendation(symbol: str, result: dict, entry_plan:
|
||||
}
|
||||
|
||||
|
||||
def _is_tradeable_short_setup(short_1h: dict) -> bool:
|
||||
if not (short_1h or {}).get("detected"):
|
||||
return False
|
||||
return (
|
||||
float((short_1h or {}).get("quality_score") or (short_1h or {}).get("score") or 0) >= 5
|
||||
and bool((short_1h or {}).get("retest_rejected"))
|
||||
and str((short_1h or {}).get("quality") or "") in {"良好", "优质"}
|
||||
)
|
||||
|
||||
|
||||
def fetch_klines(symbol, timeframe, limit=200):
|
||||
try:
|
||||
exchange.timeout = _confirm_kline_timeout_ms()
|
||||
@ -1188,12 +1198,7 @@ def confirm_burst(symbol, cand):
|
||||
or (cand_detail.get("market_context") or {}).get("short_breakdown_retest_1h")
|
||||
or {}
|
||||
)
|
||||
if trade_side != "short" and (
|
||||
bool(cand_short_hint.get("detected"))
|
||||
or "破位反抽做空" in cand_signal_text
|
||||
or "等待反抽失败" in cand_signal_text
|
||||
or "breakdown_retest_1h_short" in cand_signal_text
|
||||
):
|
||||
if trade_side != "short" and _is_tradeable_short_setup(cand_short_hint):
|
||||
trade_side = "short"
|
||||
cand_is_top_gainer = bool(cand_detail.get("top_gainer_24h") or "24h强势榜" in cand_signal_text or cand_change_24h >= get_burst_threshold(symbol) * 1.5)
|
||||
|
||||
@ -1228,6 +1233,16 @@ def confirm_burst(symbol, cand):
|
||||
or (cand_detail.get("market_context") or {}).get("short_breakdown_retest_1h")
|
||||
or {"detected": False}
|
||||
)
|
||||
try:
|
||||
if h1_df is not None and len(h1_df) >= 60:
|
||||
fresh_short_1h = detect_breakdown_retest_short_1h(h1_df, change_24h=cand_change_24h)
|
||||
if fresh_short_1h.get("detected"):
|
||||
short_1h = fresh_short_1h
|
||||
if _is_tradeable_short_setup(short_1h):
|
||||
trade_side = "short"
|
||||
except Exception:
|
||||
if trade_side == "short":
|
||||
short_1h = {"detected": False}
|
||||
|
||||
upstream_sector_context = cand_detail.get("sector_context") or {}
|
||||
if upstream_sector_context.get("hot_sectors") or upstream_sector_context.get("leader_symbol"):
|
||||
@ -2278,17 +2293,29 @@ def _result_brief(item: dict) -> dict:
|
||||
signal_text = " ".join(str(x) for x in (item.get("signals") or []))
|
||||
inferred_strategy = ""
|
||||
side = normalize_trade_side(item.get("side") or ctx.get("side") or item.get("direction"))
|
||||
if side == "short" or any(key in signal_text for key in ("破位反抽做空", "等待反抽失败", "breakdown_retest_1h_short")):
|
||||
short_ctx = item.get("short_breakdown_retest_1h") or ctx.get("short_breakdown_retest_1h") or {}
|
||||
if side == "short" and _is_tradeable_short_setup(short_ctx):
|
||||
inferred_strategy = BREAKDOWN_RETEST_SHORT_1H_STRATEGY
|
||||
elif side == "short":
|
||||
inferred_strategy = ""
|
||||
elif any(key in signal_text for key in ("24h强势榜", "回踩", "箱体突破回踩")):
|
||||
inferred_strategy = LONG_SECOND_WAVE_PULLBACK_STRATEGY
|
||||
elif any(key in signal_text for key in ("15min", "短周期", "量价齐飞", "放量")):
|
||||
inferred_strategy = LONG_MOMENTUM_BREAKOUT_STRATEGY
|
||||
action = item.get("entry_action") or (item.get("entry_plan") or {}).get("entry_action") or ""
|
||||
if side == "short":
|
||||
if _is_tradeable_short_setup(short_ctx):
|
||||
if action in ("可即刻买入", "即刻买入"):
|
||||
action = "可开空"
|
||||
elif action == "等回踩":
|
||||
action = "等反抽"
|
||||
else:
|
||||
action = "观察"
|
||||
return {
|
||||
"symbol": item.get("symbol"),
|
||||
"confirmed": bool(item.get("confirmed")),
|
||||
"score": item.get("score"),
|
||||
"action": item.get("entry_action") or (item.get("entry_plan") or {}).get("entry_action") or "",
|
||||
"action": action,
|
||||
"strategy_code": item.get("strategy_code") or (item.get("strategy_snapshot") or {}).get("strategy_code") or inferred_strategy,
|
||||
"rec_id": item.get("rec_id") or 0,
|
||||
"published_watch": bool(item.get("published_watch")),
|
||||
|
||||
@ -28,6 +28,29 @@ def _signals_text(result: dict) -> str:
|
||||
return " ".join(str(x or "") for x in (result or {}).get("signals") or [])
|
||||
|
||||
|
||||
def _has_short_context(result: dict) -> bool:
|
||||
text = _signals_text(result)
|
||||
market_context = (result or {}).get("market_context") or {}
|
||||
short_ctx = (result or {}).get("short_breakdown_retest_1h") or market_context.get("short_breakdown_retest_1h") or {}
|
||||
return bool(short_ctx.get("detected")) or any(
|
||||
key in text
|
||||
for key in ("破位反抽做空", "等待反抽失败", "反抽失败确认", "breakdown_retest_1h_short")
|
||||
)
|
||||
|
||||
|
||||
def _has_second_wave_context(result: dict) -> bool:
|
||||
text = _signals_text(result)
|
||||
market_context = (result or {}).get("market_context") or {}
|
||||
has_first_wave = (
|
||||
bool((result or {}).get("top_gainer_24h") or market_context.get("top_gainer_24h"))
|
||||
or "24h强势榜" in text
|
||||
or "量价齐飞" in text
|
||||
or "放量" in text
|
||||
)
|
||||
has_pullback_context = any(key in text for key in ("回踩", "箱体", "EMA", "前高", "底部抬高", "静K"))
|
||||
return bool(has_first_wave and has_pullback_context)
|
||||
|
||||
|
||||
def _trigger_context(result: dict) -> dict:
|
||||
return (result or {}).get("trigger_context") or ((result or {}).get("market_context") or {}).get("trigger_context") or {}
|
||||
|
||||
@ -53,6 +76,8 @@ def _status_for_entry(result: dict, entry_plan: dict | None = None, *, require_c
|
||||
|
||||
|
||||
def build_long_momentum_breakout_signal(*, symbol: str, result: dict, entry_plan: dict | None = None) -> StrategySignal | None:
|
||||
if _has_short_context(result) or _has_second_wave_context(result):
|
||||
return None
|
||||
text = _signals_text(result)
|
||||
has_15m = any(key in text for key in ("15min", "15m", "短周期", "5m/15m"))
|
||||
has_1h_participation = "量价齐飞" in text or ("连续" in text and "放量" in text) or ("1H" in text and ("突破" in text or "起爆" in text))
|
||||
@ -97,11 +122,10 @@ def build_long_momentum_breakout_signal(*, symbol: str, result: dict, entry_plan
|
||||
|
||||
|
||||
def build_long_second_wave_pullback_signal(*, symbol: str, result: dict, entry_plan: dict | None = None) -> StrategySignal | None:
|
||||
if _has_short_context(result):
|
||||
return None
|
||||
text = _signals_text(result)
|
||||
market_context = (result or {}).get("market_context") or {}
|
||||
has_first_wave = bool((result or {}).get("top_gainer_24h") or market_context.get("top_gainer_24h")) or "24h强势榜" in text or "量价齐飞" in text or "放量" in text
|
||||
has_pullback_context = any(key in text for key in ("回踩", "箱体", "EMA", "前高", "底部抬高", "静K"))
|
||||
if not (has_first_wave and has_pullback_context):
|
||||
if not _has_second_wave_context(result):
|
||||
return None
|
||||
status, reasons = _status_for_entry(result, entry_plan, require_current_trigger=False, allow_wait=True)
|
||||
score = _safe_float(result.get("score"))
|
||||
|
||||
@ -11,6 +11,7 @@ from app.db.paper_trading import (
|
||||
reset_paper_trading_data,
|
||||
send_paper_trading_report,
|
||||
)
|
||||
from app.db.intraday_frequency import get_intraday_frequency_health
|
||||
from app.db.strategy_insights import get_strategy_evaluation
|
||||
from app.web.shared import require_admin
|
||||
|
||||
@ -24,6 +25,12 @@ async def api_paper_trading_summary(days: int = 30, altcoin_session: str = Cooki
|
||||
return get_paper_trading_summary(days=days)
|
||||
|
||||
|
||||
@router.get("/api/paper-trading/frequency-health")
|
||||
async def api_paper_trading_frequency_health(days: int = 7, altcoin_session: str = Cookie(default="")):
|
||||
require_admin(altcoin_session)
|
||||
return get_intraday_frequency_health(days=days)
|
||||
|
||||
|
||||
@router.get("/api/paper-trading/performance")
|
||||
async def api_paper_trading_performance(days: int = 30, altcoin_session: str = Cookie(default="")):
|
||||
require_admin(altcoin_session)
|
||||
|
||||
@ -109,6 +109,39 @@ def test_global_risk_off_is_favorable_for_short_entries(pg_conn, monkeypatch):
|
||||
assert "risk_off 对空头属于顺风环境" in " ".join(result["reasons"])
|
||||
|
||||
|
||||
def test_intraday_global_risk_softens_risk_off_for_low_score_long(pg_conn, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.core.global_risk.get_crypto_market_overview",
|
||||
lambda allow_live_fallback=False: _overview(
|
||||
benchmarks={"BTC/USDT": {"change_24h": -3.4}, "ETH/USDT": {"change_24h": -4.2}},
|
||||
advance_decline_ratio=0.4,
|
||||
crash_count_5pct=35,
|
||||
),
|
||||
)
|
||||
|
||||
result = evaluate_global_risk(
|
||||
conn=pg_conn,
|
||||
config={
|
||||
"trading_mode": "intraday_trading",
|
||||
"account_equity_usdt": 20000,
|
||||
"global_risk_gate_enabled": True,
|
||||
"global_risk_block_critical": False,
|
||||
"global_risk_score_blocks_intraday": False,
|
||||
"global_risk_critical_min_rec_score": 80,
|
||||
"global_risk_high_min_rec_score": 70,
|
||||
},
|
||||
rec={"symbol": "LONG/USDT", "side": "long", "rec_score": 28},
|
||||
additional_notional=1000,
|
||||
)
|
||||
|
||||
assert result["allow_new_entries"] is True
|
||||
assert result["decision"] == "allow_reduced_size"
|
||||
assert result["risk_level"] == "critical"
|
||||
assert result["intraday_soft_risk"] is True
|
||||
assert result["position_multiplier"] == 0.25
|
||||
assert "日内模式下 critical 市场风险只做仓位调节" in " ".join(result["reasons"])
|
||||
|
||||
|
||||
def test_altcoin_rotation_is_adverse_for_short_entries(pg_conn, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.core.global_risk.get_crypto_market_overview",
|
||||
|
||||
@ -11,6 +11,7 @@ from app.core.strategy_registry import (
|
||||
registered_strategy_codes,
|
||||
strategy_direction,
|
||||
strategy_label,
|
||||
strategy_paper_config,
|
||||
)
|
||||
from app.db.recommendation_commands import create_recommendation
|
||||
from app.db.paper_trading import _open_trade, _order_payload_from_rec
|
||||
@ -64,6 +65,21 @@ def test_active_strategy_pool_only_contains_short_term_three_strategies():
|
||||
assert is_strategy_allowed_for_side(BREAKDOWN_RETEST_SHORT_1H_STRATEGY, "short") is True
|
||||
|
||||
|
||||
def test_active_strategy_pool_uses_intraday_paper_gates():
|
||||
long_momentum = strategy_paper_config(LONG_MOMENTUM_BREAKOUT_STRATEGY)
|
||||
second_wave = strategy_paper_config(LONG_SECOND_WAVE_PULLBACK_STRATEGY)
|
||||
short_retest = strategy_paper_config(SHORT_BREAKDOWN_RETEST_STRATEGY)
|
||||
|
||||
assert long_momentum["frequency_profile"] == "intraday_trading"
|
||||
assert long_momentum["entry_min_rr"] == 1.25
|
||||
assert long_momentum["entry_min_rec_score"] == 25
|
||||
assert long_momentum["order_require_current_trigger"] is True
|
||||
assert second_wave["order_min_distance_to_entry_pct"] == 0
|
||||
assert second_wave["order_require_current_trigger"] is False
|
||||
assert short_retest["entry_min_rr"] == 1.3
|
||||
assert short_retest["order_require_current_trigger"] is False
|
||||
|
||||
|
||||
def test_long_momentum_breakout_strategy_builds_independent_signal():
|
||||
signal = build_long_momentum_breakout_signal(
|
||||
symbol="VOL/USDT",
|
||||
@ -83,6 +99,48 @@ def test_long_momentum_breakout_strategy_builds_independent_signal():
|
||||
assert payload["factor_roles"]["momentum_breakout_15m_1h"] == "trigger"
|
||||
|
||||
|
||||
def test_long_builders_reject_short_breakdown_context():
|
||||
result = {
|
||||
"score": 8,
|
||||
"signals": ["1H破位反抽做空(破位1.0)", "等待反抽失败确认", "15min即刻入场"],
|
||||
"trigger_context": {"current_triggers": ["15m"], "trigger_status": "current"},
|
||||
"market_context": {"short_breakdown_retest_1h": {"detected": True}},
|
||||
}
|
||||
|
||||
assert build_long_momentum_breakout_signal(
|
||||
symbol="SHORTY/USDT",
|
||||
result=result,
|
||||
entry_plan={"entry_action": "可即刻买入"},
|
||||
) is None
|
||||
assert build_long_second_wave_pullback_signal(
|
||||
symbol="SHORTY/USDT",
|
||||
result={**result, "signals": [*result["signals"], "1H箱体突破回踩", "量价齐飞"]},
|
||||
entry_plan={"entry_action": "等回踩"},
|
||||
) is None
|
||||
|
||||
|
||||
def test_momentum_and_second_wave_are_mutually_exclusive():
|
||||
result = {
|
||||
"score": 8,
|
||||
"signals": ["1H量价齐飞 · 连续放量", "15min即刻入场", "1H箱体突破回踩"],
|
||||
"trigger_context": {"current_triggers": ["15m突破"], "trigger_status": "current"},
|
||||
"entry_plan": {"entry_action": "可即刻买入"},
|
||||
}
|
||||
|
||||
assert build_long_momentum_breakout_signal(
|
||||
symbol="WAVE/USDT",
|
||||
result=result,
|
||||
entry_plan={"entry_action": "可即刻买入", "entry_price": 1.0},
|
||||
) is None
|
||||
signal = build_long_second_wave_pullback_signal(
|
||||
symbol="WAVE/USDT",
|
||||
result=result,
|
||||
entry_plan={"entry_action": "可即刻买入", "entry_price": 1.0},
|
||||
)
|
||||
assert signal is not None
|
||||
assert signal.to_json_dict()["strategy_code"] == LONG_SECOND_WAVE_PULLBACK_STRATEGY
|
||||
|
||||
|
||||
def test_legacy_strategy_builders_no_longer_emit_signals():
|
||||
assert build_volume_ignition_1h_signal(
|
||||
symbol="OLD/USDT",
|
||||
|
||||
@ -48,11 +48,34 @@ def test_buy_now_with_bad_rr_sets_real_pullback_price():
|
||||
|
||||
assert action == '等回踩'
|
||||
assert plan['entry_price'] < 0.11455
|
||||
assert round(plan['entry_price'], 6) == 0.11251
|
||||
assert round(plan['entry_price'], 6) == 0.113071
|
||||
assert plan['rr_target_entry'] == plan['entry_price']
|
||||
assert any('现价不买' in r for r in reasons)
|
||||
|
||||
|
||||
def test_buy_now_with_bad_rr_can_use_stricter_rr_override():
|
||||
action, plan, reasons = apply_entry_quality_gate(
|
||||
action_status='可即刻买入',
|
||||
entry_plan={
|
||||
'entry_action': '即刻买入',
|
||||
'entry_price': 0.11455,
|
||||
'current_price': 0.11455,
|
||||
'stop_loss': 0.107457,
|
||||
'tp1': 0.120089,
|
||||
'risk_reward_ok': False,
|
||||
'rr1': 0.83,
|
||||
},
|
||||
signals=['🟢 15min即刻入场信号', '日线 站稳突破位+19.2%'],
|
||||
current_price=0.11455,
|
||||
market_context={'change_24h': 3.1},
|
||||
cfg={'min_rr_buy_now': 1.5},
|
||||
)
|
||||
|
||||
assert action == '等回踩'
|
||||
assert round(plan['entry_price'], 6) == 0.11251
|
||||
assert any('现价不买' in r for r in reasons)
|
||||
|
||||
|
||||
def test_low_entry_score_blocks_buy_now_and_weak_pullback():
|
||||
action, plan, reasons = apply_entry_quality_gate(
|
||||
action_status='可即刻买入',
|
||||
|
||||
@ -300,6 +300,11 @@ def test_wait_pullback_without_tradeable_plan_does_not_create_order(monkeypatch)
|
||||
assert "missing_stop_loss" in result["gate_reasons"]
|
||||
assert "missing_tp1" in result["gate_reasons"]
|
||||
assert list_paper_orders()["total"] == 0
|
||||
events = list_paper_trade_events(event_type="paper_gate_reject")["items"]
|
||||
assert events
|
||||
assert events[0]["symbol"] == "BADPLAN/USDT"
|
||||
assert events[0]["detail"]["reason"] == "paper_order_gate_rejected"
|
||||
assert "missing_stop_loss" in events[0]["detail"]["gate_reasons"]
|
||||
|
||||
|
||||
def test_wait_pullback_requires_confirmed_risk_reward(monkeypatch):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user