This commit is contained in:
aaron 2026-06-04 10:11:35 +08:00
parent cb7eb2f202
commit 15900b9f53
3 changed files with 74 additions and 11 deletions

View File

@ -264,6 +264,8 @@ def evaluate_global_risk(
"directional_market_bias": directional_gate, "directional_market_bias": directional_gate,
"position_multiplier": position_multiplier, "position_multiplier": position_multiplier,
"max_open_positions": max_open_positions, "max_open_positions": max_open_positions,
"critical_drawdown_pct": max_drawdown_critical,
"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,
"reasons": reasons, "reasons": reasons,

View File

@ -1017,20 +1017,27 @@ def _cancel_paper_order(conn, order: dict, reason: str, event_time: str) -> dict
def _touch_global_risk_cancel_reason(global_detail: dict | None) -> str: def _touch_global_risk_cancel_reason(global_detail: dict | None) -> str:
"""Only hard account/portfolio gates should cancel an already-touched order.""" """Only hard account/portfolio gates should cancel an already-touched order.
A touched pullback/retest order already waited for a planned price. Broad
market risk is noisy for altcoins and should mostly resize or annotate the
fill, not cancel it. Cancel only when account/portfolio constraints say the
system cannot add exposure.
"""
detail = global_detail if isinstance(global_detail, dict) else {} detail = global_detail if isinstance(global_detail, dict) else {}
decision = str(detail.get("decision") or "").strip() decision = str(detail.get("decision") or "").strip()
position_multiplier = _safe_float(detail.get("position_multiplier"), 1.0) portfolio = detail.get("portfolio") if isinstance(detail.get("portfolio"), dict) else {}
if position_multiplier <= 0: reasons_text = " ".join(str(x) for x in (detail.get("reasons") or []))
return "touch_position_multiplier_zero" drawdown = _safe_float(portfolio.get("unrealized_drawdown_pct"))
if decision == "block_critical": critical_drawdown = _safe_float(detail.get("critical_drawdown_pct") or detail.get("max_drawdown_critical"))
return "touch_critical_risk"
if decision == "block_max_open_positions": if decision == "block_max_open_positions":
return "touch_max_open_positions" return "touch_max_open_positions"
if decision == "block_same_direction_concentration": if decision == "block_same_direction_concentration":
return "touch_same_direction_concentration" return "touch_same_direction_concentration"
if decision == "block_same_sector_concentration": if decision == "block_same_sector_concentration":
return "touch_same_sector_concentration" return "touch_same_sector_concentration"
if "账户浮亏" in reasons_text or (critical_drawdown > 0 and drawdown >= critical_drawdown):
return "touch_position_multiplier_zero"
return "" return ""

View File

@ -816,7 +816,7 @@ def test_pending_paper_order_reconciles_from_latest_price_cache(monkeypatch):
assert order["fill_price"] == pytest.approx(95) assert order["fill_price"] == pytest.approx(95)
def test_touched_wait_pullback_order_cancels_when_global_risk_pauses(monkeypatch): def test_touched_wait_pullback_order_ignores_market_only_critical_pause(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1") monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", "1") monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", "1")
monkeypatch.setattr( monkeypatch.setattr(
@ -827,6 +827,8 @@ def test_touched_wait_pullback_order_cancels_when_global_risk_pauses(monkeypatch
"risk_level": "critical", "risk_level": "critical",
"position_multiplier": 0, "position_multiplier": 0,
"reasons": ["critical 市场环境下推荐分不足"], "reasons": ["critical 市场环境下推荐分不足"],
"portfolio": {"unrealized_drawdown_pct": 0.0},
"critical_drawdown_pct": 6.0,
}, },
) )
altcoin_db.init_db() altcoin_db.init_db()
@ -854,15 +856,67 @@ def test_touched_wait_pullback_order_cancels_when_global_risk_pauses(monkeypatch
} }
created = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00") created = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
paused = sync_recommendation(rec, 94.9, event_time="2026-05-16T10:05:00") filled = sync_recommendation(rec, 94.9, event_time="2026-05-16T10:05:00")
assert created["reason"] == "paper_order_created" assert created["reason"] == "paper_order_created"
assert paused["reason"] == "paper_order_touch_position_multiplier_zero" assert filled.get("opened") is True
assert filled["global_risk"]["touch_soft_gate_overridden"] is True
assert filled["global_risk"]["touch_soft_gate_reason"] == "block_critical"
assert list_paper_trades()["total"] == 1
assert list_paper_orders(status="filled")["total"] == 1
assert list_paper_orders(status="canceled")["total"] == 0
def test_touched_wait_pullback_order_still_cancels_on_account_hard_limit(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", "1")
monkeypatch.setattr(
"app.db.paper_trading.evaluate_global_risk",
lambda **kwargs: {
"allow_new_entries": False,
"decision": "block_max_open_positions",
"risk_level": "high",
"position_multiplier": 1,
"reasons": ["持仓数量已达到上限"],
"portfolio": {"unrealized_drawdown_pct": 0.0},
"critical_drawdown_pct": 6.0,
},
)
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="HARDLIMIT/USDT",
rec_state="蓄力",
rec_score=80,
entry_price=95,
stop_loss=90,
tp1=105,
tp2=112,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
rec = {
"id": rec_id,
"symbol": "HARDLIMIT/USDT",
"rec_score": 80,
"execution_status": "wait_pullback",
"action_status": "等回踩",
"entry_price": 95,
"stop_loss": 90,
"tp1": 105,
"tp2": 112,
"entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
}
created = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
canceled = sync_recommendation(rec, 94.9, event_time="2026-05-16T10:05:00")
assert created["reason"] == "paper_order_created"
assert canceled["reason"] == "paper_order_touch_max_open_positions"
assert list_paper_trades()["total"] == 0 assert list_paper_trades()["total"] == 0
assert list_paper_orders(status="pending")["total"] == 0 assert list_paper_orders(status="pending")["total"] == 0
canceled = list_paper_orders(status="canceled")["items"][0] canceled = list_paper_orders(status="canceled")["items"][0]
assert canceled["symbol"] == "RISKPAUSE/USDT" assert canceled["symbol"] == "HARDLIMIT/USDT"
assert canceled["cancel_reason"] == "touch_position_multiplier_zero" assert canceled["cancel_reason"] == "touch_max_open_positions"
def test_touched_order_soft_global_risk_gate_fills_instead_of_canceling(monkeypatch): def test_touched_order_soft_global_risk_gate_fills_instead_of_canceling(monkeypatch):