·
This commit is contained in:
parent
cb7eb2f202
commit
15900b9f53
@ -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,
|
||||||
|
|||||||
@ -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 ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user