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
|
ALPHAX_ONCHAIN_WHALE_TX_USD=250000
|
||||||
|
|
||||||
# 策略交易挂单门控。wait_pullback 只是候选,必须通过这些条件才会创建挂单。
|
# 策略交易挂单门控。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_ORDER_GATE_ENABLED=1
|
||||||
ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE=5
|
ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE=5
|
||||||
ALPHAX_PAPER_ENTRY_GATE_ENABLED=1
|
ALPHAX_PAPER_ENTRY_GATE_ENABLED=1
|
||||||
ALPHAX_PAPER_ENTRY_MIN_REC_SCORE=50
|
ALPHAX_PAPER_ENTRY_MIN_REC_SCORE=25
|
||||||
ALPHAX_PAPER_MIN_RR=1.5
|
ALPHAX_PAPER_MIN_RR=1.25
|
||||||
ALPHAX_PAPER_ENTRY_MIN_RR=1.5
|
ALPHAX_PAPER_ENTRY_MIN_RR=1.25
|
||||||
ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT=20
|
ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT=20
|
||||||
ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED=1
|
ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED=1
|
||||||
ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN=3
|
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_WEAK_ENTRY_MIN_MAX_PNL_PCT=1
|
||||||
ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED=1
|
ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED=1
|
||||||
ALPHAX_PAPER_GLOBAL_RISK_BLOCK_CRITICAL=0
|
ALPHAX_PAPER_GLOBAL_RISK_BLOCK_CRITICAL=0
|
||||||
ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE=70
|
ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE=25
|
||||||
ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_MIN_REC_SCORE=80
|
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_MIN_POSITION_MULTIPLIER=0.2
|
||||||
ALPHAX_PAPER_GLOBAL_RISK_HIGH_DRAWDOWN_PCT=3
|
ALPHAX_PAPER_GLOBAL_RISK_HIGH_DRAWDOWN_PCT=3
|
||||||
ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_DRAWDOWN_PCT=6
|
ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_DRAWDOWN_PCT=6
|
||||||
ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS=0
|
ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS=0
|
||||||
ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS=3
|
ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS=3
|
||||||
ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS=6
|
ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS=6
|
||||||
ALPHAX_PAPER_ORDER_MIN_REC_SCORE=50
|
ALPHAX_PAPER_ORDER_MIN_REC_SCORE=25
|
||||||
ALPHAX_PAPER_ORDER_MIN_RR=1.5
|
ALPHAX_PAPER_ORDER_MIN_RR=1.25
|
||||||
ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK=1
|
ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK=1
|
||||||
ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT=0
|
ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT=0
|
||||||
ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT=8
|
ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT=8
|
||||||
ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER=0
|
ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER=0
|
||||||
ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT=12
|
ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT=12
|
||||||
ALPHAX_PAPER_ORDER_EXPIRE_HOURS=24
|
ALPHAX_PAPER_ORDER_EXPIRE_HOURS=8
|
||||||
|
|
||||||
# 策略交易移动止盈。volatility 会按持仓后实际高低价波动动态调整启动阈值和保护距离。
|
# 策略交易移动止盈。volatility 会按持仓后实际高低价波动动态调整启动阈值和保护距离。
|
||||||
ALPHAX_PAPER_TRAILING_STOP_ENABLED=1
|
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
|
# One shared default keeps buy-now entries and wait-pullback orders from
|
||||||
# drifting into two unrelated RR standards. The explicit entry/order envs
|
# drifting into two unrelated RR standards. The explicit entry/order envs
|
||||||
# remain supported for advanced overrides.
|
# 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 {
|
return {
|
||||||
"enabled": _env_bool("ALPHAX_PAPER_TRADING_ENABLED", True),
|
"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),
|
"account_equity_usdt": _env_float("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", 20000),
|
||||||
"trade_notional_usdt": _env_float("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", 5000),
|
"trade_notional_usdt": _env_float("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", 5000),
|
||||||
"trade_leverage": _env_float("ALPHAX_PAPER_TRADE_LEVERAGE", 5),
|
"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),
|
"slippage_pct": _env_float("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", 0.05),
|
||||||
"trailing_stop_enabled": _env_bool("ALPHAX_PAPER_TRAILING_STOP_ENABLED", True),
|
"trailing_stop_enabled": _env_bool("ALPHAX_PAPER_TRAILING_STOP_ENABLED", True),
|
||||||
"trailing_mode": _env_str("ALPHAX_PAPER_TRAILING_MODE", "volatility"),
|
"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_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_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_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_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),
|
"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),
|
"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),
|
"order_gate_enabled": _env_bool("ALPHAX_PAPER_ORDER_GATE_ENABLED", True),
|
||||||
"entry_gate_enabled": _env_bool("ALPHAX_PAPER_ENTRY_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,
|
"min_rr": paper_min_rr,
|
||||||
"entry_min_rr": _env_float("ALPHAX_PAPER_ENTRY_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_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),
|
"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_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_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_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_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_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_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_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),
|
"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_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_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_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_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_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": [
|
"trailing_tiers": [
|
||||||
{"min_pnl_pct": 8.0, "distance_pct": 1.0, "label": "紧贴"},
|
{"min_pnl_pct": 8.0, "distance_pct": 1.0, "label": "紧贴"},
|
||||||
{"min_pnl_pct": 5.0, "distance_pct": 1.2, "label": "锁利"},
|
{"min_pnl_pct": 5.0, "distance_pct": 1.2, "label": "锁利"},
|
||||||
@ -238,6 +242,9 @@ def _paper_trading_env_overrides():
|
|||||||
})
|
})
|
||||||
checks = {
|
checks = {
|
||||||
"ALPHAX_PAPER_TRADING_ENABLED": ("enabled", lambda: _env_bool("ALPHAX_PAPER_TRADING_ENABLED", True)),
|
"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_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_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_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_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_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_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_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.5))),
|
"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_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_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)),
|
"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_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_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_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_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_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_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_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_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_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_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.5))),
|
"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_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_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_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_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_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():
|
for env_name, (key, loader) in checks.items():
|
||||||
if _env_present(env_name):
|
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")
|
_seed_one("paper_trading", default_paper_trading_config(), "Paper trading account and execution model")
|
||||||
cfg = get_paper_trading_config(default=None)
|
cfg = get_paper_trading_config(default=None)
|
||||||
merged = 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())
|
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():
|
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"))
|
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_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))
|
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))
|
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_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))
|
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"
|
risk_level = "high"
|
||||||
reasons.append("账户浮亏偏高,只允许高质量机会")
|
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
|
allow = False
|
||||||
decision = "block_critical"
|
decision = "block_critical"
|
||||||
elif risk_level == "critical" and drawdown < max_drawdown_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
|
allow = False
|
||||||
decision = "block_critical_weak_score"
|
decision = "block_critical_weak_score"
|
||||||
reasons.append(f"critical 市场环境下推荐分 {rec_score:.1f} 低于 {min_score_critical:.1f}")
|
reasons.append(f"critical 市场环境下推荐分 {rec_score:.1f} 低于 {min_score_critical:.1f}")
|
||||||
else:
|
else:
|
||||||
decision = "allow_reduced_size"
|
decision = "allow_reduced_size"
|
||||||
reasons.append(f"critical 市场环境不再一刀切,按 {position_multiplier:.0%} 仓位试运行")
|
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:
|
elif risk_level == "high" and rec_score < min_score_high:
|
||||||
allow = False
|
allow = False
|
||||||
decision = "block_high_weak_score"
|
decision = "block_high_weak_score"
|
||||||
@ -268,6 +277,8 @@ def evaluate_global_risk(
|
|||||||
"high_drawdown_pct": max_drawdown_high,
|
"high_drawdown_pct": max_drawdown_high,
|
||||||
"min_score_when_high_risk": min_score_high,
|
"min_score_when_high_risk": min_score_high,
|
||||||
"min_score_when_critical_risk": min_score_critical,
|
"min_score_when_critical_risk": min_score_critical,
|
||||||
|
"trading_mode": trading_mode,
|
||||||
|
"intraday_soft_risk": intraday_soft_risk,
|
||||||
"reasons": reasons,
|
"reasons": reasons,
|
||||||
"market_regime": regime,
|
"market_regime": regime,
|
||||||
"portfolio": portfolio,
|
"portfolio": portfolio,
|
||||||
|
|||||||
@ -26,6 +26,7 @@ class StrategyDefinition:
|
|||||||
strategy_name: str
|
strategy_name: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
direction: str = "long"
|
direction: str = "long"
|
||||||
|
frequency_profile: str = "intraday"
|
||||||
mode: str = "paper_only"
|
mode: str = "paper_only"
|
||||||
status: str = "active"
|
status: str = "active"
|
||||||
entry_gate_config: dict = field(default_factory=dict)
|
entry_gate_config: dict = field(default_factory=dict)
|
||||||
@ -38,19 +39,28 @@ STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
|
|||||||
strategy_name="多头动量启动",
|
strategy_name="多头动量启动",
|
||||||
description="15m当前突破叠加1H成交量/波动增强,捕捉山寨币日内到1-3天启动段。",
|
description="15m当前突破叠加1H成交量/波动增强,捕捉山寨币日内到1-3天启动段。",
|
||||||
direction="long",
|
direction="long",
|
||||||
|
frequency_profile="intraday",
|
||||||
mode="paper_enabled",
|
mode="paper_enabled",
|
||||||
entry_gate_config={
|
entry_gate_config={
|
||||||
"min_entry_score_buy_now": 3,
|
"min_entry_score_buy_now": 3,
|
||||||
"min_entry_score_wait_pullback": 2,
|
"min_entry_score_wait_pullback": 2,
|
||||||
"min_rr_buy_now": 1.5,
|
"min_rr_buy_now": 1.25,
|
||||||
"breakout_distance_wait_pct": 8,
|
"breakout_distance_wait_pct": 8,
|
||||||
"gain_24h_wait_pct": 10,
|
"gain_24h_wait_pct": 10,
|
||||||
},
|
},
|
||||||
paper_config={
|
paper_config={
|
||||||
"entry_min_rr": 1.5,
|
"frequency_profile": "intraday_trading",
|
||||||
"order_min_rr": 1.5,
|
"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_min_distance_to_entry_pct": 0,
|
||||||
"order_require_current_trigger": True,
|
"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_enabled": True,
|
||||||
"dynamic_leverage_min": 3,
|
"dynamic_leverage_min": 3,
|
||||||
},
|
},
|
||||||
@ -60,20 +70,29 @@ STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
|
|||||||
strategy_name="多头二波回踩",
|
strategy_name="多头二波回踩",
|
||||||
description="强势榜或放量币第一波后回踩EMA、箱体上沿或前高转支撑,再次承接的短线策略。",
|
description="强势榜或放量币第一波后回踩EMA、箱体上沿或前高转支撑,再次承接的短线策略。",
|
||||||
direction="long",
|
direction="long",
|
||||||
|
frequency_profile="intraday",
|
||||||
mode="paper_enabled",
|
mode="paper_enabled",
|
||||||
entry_gate_config={
|
entry_gate_config={
|
||||||
"min_entry_score_buy_now": 2,
|
"min_entry_score_buy_now": 2,
|
||||||
"min_entry_score_wait_pullback": 0,
|
"min_entry_score_wait_pullback": 0,
|
||||||
"min_rr_buy_now": 1.5,
|
"min_rr_buy_now": 1.25,
|
||||||
"max_wait_pullback_deviation_pct": 10,
|
"max_wait_pullback_deviation_pct": 10,
|
||||||
"breakout_distance_wait_pct": 12,
|
"breakout_distance_wait_pct": 12,
|
||||||
"gain_24h_wait_pct": 18,
|
"gain_24h_wait_pct": 18,
|
||||||
},
|
},
|
||||||
paper_config={
|
paper_config={
|
||||||
"entry_min_rr": 1.5,
|
"frequency_profile": "intraday_trading",
|
||||||
"order_min_rr": 1.5,
|
"target_trades_per_day_min": 3,
|
||||||
"order_min_distance_to_entry_pct": 1.5,
|
"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_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_enabled": True,
|
||||||
"dynamic_leverage_min": 3,
|
"dynamic_leverage_min": 3,
|
||||||
},
|
},
|
||||||
@ -83,20 +102,29 @@ STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
|
|||||||
strategy_name="空头破位反抽",
|
strategy_name="空头破位反抽",
|
||||||
description="1H支撑或箱体下沿破位后反抽失败,叠加15m弱确认和相对弱势的空头短线策略。",
|
description="1H支撑或箱体下沿破位后反抽失败,叠加15m弱确认和相对弱势的空头短线策略。",
|
||||||
direction="short",
|
direction="short",
|
||||||
|
frequency_profile="intraday",
|
||||||
mode="paper_enabled",
|
mode="paper_enabled",
|
||||||
entry_gate_config={
|
entry_gate_config={
|
||||||
"direction": "short",
|
"direction": "short",
|
||||||
"min_entry_score_buy_now": 2,
|
"min_entry_score_buy_now": 2,
|
||||||
"min_entry_score_wait_pullback": 1,
|
"min_entry_score_wait_pullback": 1,
|
||||||
"min_rr_buy_now": 1.5,
|
"min_rr_buy_now": 1.3,
|
||||||
"breakdown_distance_wait_pct": 10,
|
"breakdown_distance_wait_pct": 10,
|
||||||
"max_retest_deviation_pct": 8,
|
"max_retest_deviation_pct": 8,
|
||||||
},
|
},
|
||||||
paper_config={
|
paper_config={
|
||||||
"entry_min_rr": 1.8,
|
"frequency_profile": "intraday_trading",
|
||||||
"order_min_rr": 1.8,
|
"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_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_enabled": True,
|
||||||
"dynamic_leverage_min": 2,
|
"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.opportunity_funnel import screening_stage_meta, stage_label
|
||||||
from app.core.strategy_registry import normalize_strategy_code, strategy_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
|
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
|
current_page = (offset // limit) + 1 if total_count else 0
|
||||||
return {
|
return {
|
||||||
"kpi": kpi,
|
"kpi": kpi,
|
||||||
|
"intraday_frequency": get_intraday_frequency_health(days=max(1, min((hours + 23) // 24, 7))),
|
||||||
"runs": runs,
|
"runs": runs,
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"hours": hours,
|
"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:
|
def _paper_cfg_for_rec(rec: dict, config: dict | None = None) -> dict:
|
||||||
cfg = dict(_paper_cfg(config) or {})
|
cfg = dict(_paper_cfg(config) or {})
|
||||||
cfg.update(strategy_paper_config(_strategy_code_from_rec(rec)))
|
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
|
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:
|
def _fmt_price(value) -> str:
|
||||||
price = _safe_float(value)
|
price = _safe_float(value)
|
||||||
if price <= 0:
|
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:
|
if max_sl_risk > 0 and sl_risk > max_sl_risk:
|
||||||
entry_reasons.append("stop_loss_leverage_risk_exceeded")
|
entry_reasons.append("stop_loss_leverage_risk_exceeded")
|
||||||
if entry_reasons:
|
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 {
|
return {
|
||||||
"opened": False,
|
"opened": False,
|
||||||
"skipped": True,
|
"skipped": True,
|
||||||
@ -791,6 +861,15 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
|
|||||||
if global_detail is None:
|
if global_detail is None:
|
||||||
global_ok, global_detail = _global_risk_entry_check(conn, rec, notional, cfg)
|
global_ok, global_detail = _global_risk_entry_check(conn, rec, notional, cfg)
|
||||||
if not global_ok:
|
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 {
|
return {
|
||||||
"opened": False,
|
"opened": False,
|
||||||
"skipped": True,
|
"skipped": True,
|
||||||
@ -811,9 +890,27 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
|
|||||||
plan["leverage_sizing"] = leverage_risk
|
plan["leverage_sizing"] = leverage_risk
|
||||||
pause_ok, pause_reason, pause_detail = _portfolio_entry_pause_check(conn, notional, event_time, cfg)
|
pause_ok, pause_reason, pause_detail = _portfolio_entry_pause_check(conn, notional, event_time, cfg)
|
||||||
if not pause_ok:
|
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}
|
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)
|
leverage_ok, leverage_detail = _cumulative_leverage_check(conn, notional, cfg, exclude_rec_id=rec_id)
|
||||||
if not leverage_ok:
|
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 {
|
return {
|
||||||
"opened": False,
|
"opened": False,
|
||||||
"skipped": True,
|
"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"]),
|
(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"]}
|
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)
|
gate_ok, gate_reasons, gate_detail = _paper_order_gate(rec, current_price, cfg, conn=conn)
|
||||||
if not gate_ok:
|
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 {
|
return {
|
||||||
"skipped": True,
|
"skipped": True,
|
||||||
"reason": "paper_order_gate_rejected",
|
"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)
|
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:
|
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"}
|
return {"skipped": True, "reason": "invalid_paper_order"}
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import json
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from app.db.paper_trading import get_paper_trading_summary
|
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.schema import get_conn
|
||||||
from app.db.strategy_insights import get_strategy_evaluation, get_strategy_insights
|
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)
|
evidence = _evidence_review(conn, since)
|
||||||
iteration = _iteration_review(conn, since)
|
iteration = _iteration_review(conn, since)
|
||||||
strategy_evaluation = get_strategy_evaluation(days=days)
|
strategy_evaluation = get_strategy_evaluation(days=days)
|
||||||
|
intraday_frequency = get_intraday_frequency_health(days=min(days, 7))
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@ -403,6 +405,7 @@ def get_review_center_dashboard(days=30):
|
|||||||
],
|
],
|
||||||
"opportunity": opportunity,
|
"opportunity": opportunity,
|
||||||
"strategy_evaluation": strategy_evaluation,
|
"strategy_evaluation": strategy_evaluation,
|
||||||
|
"intraday_frequency": intraday_frequency,
|
||||||
"paper_trading": paper,
|
"paper_trading": paper,
|
||||||
"evidence": evidence,
|
"evidence": evidence,
|
||||||
"iteration": iteration,
|
"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):
|
def fetch_klines(symbol, timeframe, limit=200):
|
||||||
try:
|
try:
|
||||||
exchange.timeout = _confirm_kline_timeout_ms()
|
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 (cand_detail.get("market_context") or {}).get("short_breakdown_retest_1h")
|
||||||
or {}
|
or {}
|
||||||
)
|
)
|
||||||
if trade_side != "short" and (
|
if trade_side != "short" and _is_tradeable_short_setup(cand_short_hint):
|
||||||
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
|
|
||||||
):
|
|
||||||
trade_side = "short"
|
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)
|
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 (cand_detail.get("market_context") or {}).get("short_breakdown_retest_1h")
|
||||||
or {"detected": False}
|
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 {}
|
upstream_sector_context = cand_detail.get("sector_context") or {}
|
||||||
if upstream_sector_context.get("hot_sectors") or upstream_sector_context.get("leader_symbol"):
|
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 []))
|
signal_text = " ".join(str(x) for x in (item.get("signals") or []))
|
||||||
inferred_strategy = ""
|
inferred_strategy = ""
|
||||||
side = normalize_trade_side(item.get("side") or ctx.get("side") or item.get("direction"))
|
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
|
inferred_strategy = BREAKDOWN_RETEST_SHORT_1H_STRATEGY
|
||||||
|
elif side == "short":
|
||||||
|
inferred_strategy = ""
|
||||||
elif any(key in signal_text for key in ("24h强势榜", "回踩", "箱体突破回踩")):
|
elif any(key in signal_text for key in ("24h强势榜", "回踩", "箱体突破回踩")):
|
||||||
inferred_strategy = LONG_SECOND_WAVE_PULLBACK_STRATEGY
|
inferred_strategy = LONG_SECOND_WAVE_PULLBACK_STRATEGY
|
||||||
elif any(key in signal_text for key in ("15min", "短周期", "量价齐飞", "放量")):
|
elif any(key in signal_text for key in ("15min", "短周期", "量价齐飞", "放量")):
|
||||||
inferred_strategy = LONG_MOMENTUM_BREAKOUT_STRATEGY
|
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 {
|
return {
|
||||||
"symbol": item.get("symbol"),
|
"symbol": item.get("symbol"),
|
||||||
"confirmed": bool(item.get("confirmed")),
|
"confirmed": bool(item.get("confirmed")),
|
||||||
"score": item.get("score"),
|
"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,
|
"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,
|
"rec_id": item.get("rec_id") or 0,
|
||||||
"published_watch": bool(item.get("published_watch")),
|
"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 [])
|
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:
|
def _trigger_context(result: dict) -> dict:
|
||||||
return (result or {}).get("trigger_context") or ((result or {}).get("market_context") or {}).get("trigger_context") or {}
|
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:
|
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)
|
text = _signals_text(result)
|
||||||
has_15m = any(key in text for key in ("15min", "15m", "短周期", "5m/15m"))
|
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))
|
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:
|
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)
|
text = _signals_text(result)
|
||||||
market_context = (result or {}).get("market_context") or {}
|
if not _has_second_wave_context(result):
|
||||||
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):
|
|
||||||
return None
|
return None
|
||||||
status, reasons = _status_for_entry(result, entry_plan, require_current_trigger=False, allow_wait=True)
|
status, reasons = _status_for_entry(result, entry_plan, require_current_trigger=False, allow_wait=True)
|
||||||
score = _safe_float(result.get("score"))
|
score = _safe_float(result.get("score"))
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from app.db.paper_trading import (
|
|||||||
reset_paper_trading_data,
|
reset_paper_trading_data,
|
||||||
send_paper_trading_report,
|
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.db.strategy_insights import get_strategy_evaluation
|
||||||
from app.web.shared import require_admin
|
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)
|
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")
|
@router.get("/api/paper-trading/performance")
|
||||||
async def api_paper_trading_performance(days: int = 30, altcoin_session: str = Cookie(default="")):
|
async def api_paper_trading_performance(days: int = 30, altcoin_session: str = Cookie(default="")):
|
||||||
require_admin(altcoin_session)
|
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"])
|
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):
|
def test_altcoin_rotation_is_adverse_for_short_entries(pg_conn, monkeypatch):
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"app.core.global_risk.get_crypto_market_overview",
|
"app.core.global_risk.get_crypto_market_overview",
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from app.core.strategy_registry import (
|
|||||||
registered_strategy_codes,
|
registered_strategy_codes,
|
||||||
strategy_direction,
|
strategy_direction,
|
||||||
strategy_label,
|
strategy_label,
|
||||||
|
strategy_paper_config,
|
||||||
)
|
)
|
||||||
from app.db.recommendation_commands import create_recommendation
|
from app.db.recommendation_commands import create_recommendation
|
||||||
from app.db.paper_trading import _open_trade, _order_payload_from_rec
|
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
|
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():
|
def test_long_momentum_breakout_strategy_builds_independent_signal():
|
||||||
signal = build_long_momentum_breakout_signal(
|
signal = build_long_momentum_breakout_signal(
|
||||||
symbol="VOL/USDT",
|
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"
|
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():
|
def test_legacy_strategy_builders_no_longer_emit_signals():
|
||||||
assert build_volume_ignition_1h_signal(
|
assert build_volume_ignition_1h_signal(
|
||||||
symbol="OLD/USDT",
|
symbol="OLD/USDT",
|
||||||
|
|||||||
@ -48,11 +48,34 @@ def test_buy_now_with_bad_rr_sets_real_pullback_price():
|
|||||||
|
|
||||||
assert action == '等回踩'
|
assert action == '等回踩'
|
||||||
assert plan['entry_price'] < 0.11455
|
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 plan['rr_target_entry'] == plan['entry_price']
|
||||||
assert any('现价不买' in r for r in reasons)
|
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():
|
def test_low_entry_score_blocks_buy_now_and_weak_pullback():
|
||||||
action, plan, reasons = apply_entry_quality_gate(
|
action, plan, reasons = apply_entry_quality_gate(
|
||||||
action_status='可即刻买入',
|
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_stop_loss" in result["gate_reasons"]
|
||||||
assert "missing_tp1" in result["gate_reasons"]
|
assert "missing_tp1" in result["gate_reasons"]
|
||||||
assert list_paper_orders()["total"] == 0
|
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):
|
def test_wait_pullback_requires_confirmed_risk_reward(monkeypatch):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user