This commit is contained in:
aaron 2026-05-28 01:05:11 +08:00
parent 7a7f7261a9
commit 476cfef193
7 changed files with 146 additions and 19 deletions

View File

@ -80,7 +80,8 @@ ALPHAX_PAPER_ORDER_GATE_ENABLED=1
ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE=5
ALPHAX_PAPER_ENTRY_GATE_ENABLED=1
ALPHAX_PAPER_ENTRY_MIN_REC_SCORE=50
ALPHAX_PAPER_ENTRY_MIN_RR=1.8
ALPHAX_PAPER_MIN_RR=1.5
ALPHAX_PAPER_ENTRY_MIN_RR=1.5
ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT=20
ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT=3
ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES=3
@ -97,9 +98,9 @@ ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS=0
ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS=3
ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS=6
ALPHAX_PAPER_ORDER_MIN_REC_SCORE=50
ALPHAX_PAPER_ORDER_MIN_RR=1.8
ALPHAX_PAPER_ORDER_MIN_RR=1.5
ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK=1
ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT=1.5
ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT=0
ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT=8
ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER=0
ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT=12

View File

@ -150,6 +150,10 @@ def _onchain_env_overrides(default_chains=("ethereum", "bsc")):
def default_paper_trading_config():
# One shared default keeps buy-now entries and wait-pullback orders from
# drifting into two unrelated RR standards. The explicit entry/order envs
# remain supported for advanced overrides.
paper_min_rr = _env_float("ALPHAX_PAPER_MIN_RR", 1.5)
return {
"enabled": _env_bool("ALPHAX_PAPER_TRADING_ENABLED", True),
"account_equity_usdt": _env_float("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", 20000),
@ -174,7 +178,8 @@ def default_paper_trading_config():
"order_gate_enabled": _env_bool("ALPHAX_PAPER_ORDER_GATE_ENABLED", True),
"entry_gate_enabled": _env_bool("ALPHAX_PAPER_ENTRY_GATE_ENABLED", True),
"entry_min_rec_score": _env_float("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", 50.0),
"entry_min_rr": _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", 1.8),
"min_rr": paper_min_rr,
"entry_min_rr": _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", paper_min_rr),
"max_stop_loss_leverage_risk_pct": _env_float("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", 20.0),
"max_account_drawdown_pause_pct": _env_float("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", 3.0),
"pause_after_weak_entries": _env_int("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", 3),
@ -191,7 +196,7 @@ def default_paper_trading_config():
"global_risk_max_same_sector_positions": _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS", 3),
"global_risk_max_same_direction_positions": _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS", 6),
"order_min_rec_score": _env_float("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", 50.0),
"order_min_rr": _env_float("ALPHAX_PAPER_ORDER_MIN_RR", 1.8),
"order_min_rr": _env_float("ALPHAX_PAPER_ORDER_MIN_RR", paper_min_rr),
"order_require_risk_reward_ok": _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK", True),
"order_min_distance_to_entry_pct": _env_float("ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT", 1.5),
"order_max_distance_to_entry_pct": _env_float("ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT", 8.0),
@ -206,6 +211,58 @@ def default_paper_trading_config():
}
def _paper_trading_env_overrides():
"""Honor explicit paper-trading env vars even when DB runtime config exists."""
overrides = {}
if _env_present("ALPHAX_PAPER_MIN_RR"):
shared_min_rr = _env_float("ALPHAX_PAPER_MIN_RR", 1.5)
overrides.update({
"min_rr": shared_min_rr,
"entry_min_rr": shared_min_rr,
"order_min_rr": shared_min_rr,
})
checks = {
"ALPHAX_PAPER_TRADING_ENABLED": ("enabled", lambda: _env_bool("ALPHAX_PAPER_TRADING_ENABLED", True)),
"ALPHAX_PAPER_ACCOUNT_EQUITY_USDT": ("account_equity_usdt", lambda: _env_float("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", 20000)),
"ALPHAX_PAPER_TRADE_NOTIONAL_USDT": ("trade_notional_usdt", lambda: _env_float("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", 5000)),
"ALPHAX_PAPER_TRADE_LEVERAGE": ("trade_leverage", lambda: _env_float("ALPHAX_PAPER_TRADE_LEVERAGE", 5)),
"ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE": ("max_cumulative_leverage", lambda: _env_float("ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE", 5.0)),
"ALPHAX_PAPER_TRADE_FEE_RATE": ("fee_rate", lambda: _env_float("ALPHAX_PAPER_TRADE_FEE_RATE", 0.001)),
"ALPHAX_PAPER_TRADE_SLIPPAGE_PCT": ("slippage_pct", lambda: _env_float("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", 0.05)),
"ALPHAX_PAPER_ORDER_GATE_ENABLED": ("order_gate_enabled", lambda: _env_bool("ALPHAX_PAPER_ORDER_GATE_ENABLED", True)),
"ALPHAX_PAPER_ENTRY_GATE_ENABLED": ("entry_gate_enabled", lambda: _env_bool("ALPHAX_PAPER_ENTRY_GATE_ENABLED", True)),
"ALPHAX_PAPER_ENTRY_MIN_REC_SCORE": ("entry_min_rec_score", lambda: _env_float("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", 50.0)),
"ALPHAX_PAPER_ENTRY_MIN_RR": ("entry_min_rr", lambda: _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", overrides.get("min_rr", 1.5))),
"ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT": ("max_stop_loss_leverage_risk_pct", lambda: _env_float("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", 20.0)),
"ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT": ("max_account_drawdown_pause_pct", lambda: _env_float("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", 3.0)),
"ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES": ("pause_after_weak_entries", lambda: _env_int("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", 3)),
"ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS": ("weak_entry_window_hours", lambda: _env_float("ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS", 6.0)),
"ALPHAX_PAPER_WEAK_ENTRY_MIN_MAX_PNL_PCT": ("weak_entry_min_max_pnl_pct", lambda: _env_float("ALPHAX_PAPER_WEAK_ENTRY_MIN_MAX_PNL_PCT", 1.0)),
"ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED": ("global_risk_gate_enabled", lambda: _env_bool("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", True)),
"ALPHAX_PAPER_GLOBAL_RISK_BLOCK_CRITICAL": ("global_risk_block_critical", lambda: _env_bool("ALPHAX_PAPER_GLOBAL_RISK_BLOCK_CRITICAL", False)),
"ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_MIN_REC_SCORE": ("global_risk_critical_min_rec_score", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_MIN_REC_SCORE", 80.0)),
"ALPHAX_PAPER_GLOBAL_RISK_MIN_POSITION_MULTIPLIER": ("global_risk_min_position_multiplier", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_MIN_POSITION_MULTIPLIER", 0.2)),
"ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE": ("global_risk_high_min_rec_score", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE", 70.0)),
"ALPHAX_PAPER_GLOBAL_RISK_HIGH_DRAWDOWN_PCT": ("global_risk_high_drawdown_pct", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_HIGH_DRAWDOWN_PCT", 3.0)),
"ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_DRAWDOWN_PCT": ("global_risk_critical_drawdown_pct", lambda: _env_float("ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_DRAWDOWN_PCT", 6.0)),
"ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS": ("global_risk_max_open_positions", lambda: _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS", 0)),
"ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS": ("global_risk_max_same_sector_positions", lambda: _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS", 3)),
"ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS": ("global_risk_max_same_direction_positions", lambda: _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS", 6)),
"ALPHAX_PAPER_ORDER_MIN_REC_SCORE": ("order_min_rec_score", lambda: _env_float("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", 50.0)),
"ALPHAX_PAPER_ORDER_MIN_RR": ("order_min_rr", lambda: _env_float("ALPHAX_PAPER_ORDER_MIN_RR", overrides.get("min_rr", 1.5))),
"ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK": ("order_require_risk_reward_ok", lambda: _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK", True)),
"ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT": ("order_min_distance_to_entry_pct", lambda: _env_float("ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT", 0.0)),
"ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT": ("order_max_distance_to_entry_pct", lambda: _env_float("ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT", 8.0)),
"ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER": ("order_require_current_trigger", lambda: _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER", False)),
"ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT": ("order_cancel_far_from_entry_pct", lambda: _env_float("ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT", 12.0)),
"ALPHAX_PAPER_ORDER_EXPIRE_HOURS": ("order_expire_hours", lambda: _env_float("ALPHAX_PAPER_ORDER_EXPIRE_HOURS", 24.0)),
}
for env_name, (key, loader) in checks.items():
if _env_present(env_name):
overrides[key] = loader()
return overrides
def default_live_trading_config():
return {
"enabled": _env_bool("ALPHAX_LIVE_TRADING_ENABLED", False),
@ -457,7 +514,8 @@ def paper_trading_config():
if cfg is None:
_seed_one("paper_trading", default_paper_trading_config(), "Paper trading account and execution model")
cfg = get_paper_trading_config(default=None)
return deep_merge(default_paper_trading_config(), cfg or {})
merged = deep_merge(default_paper_trading_config(), cfg or {})
return deep_merge(merged, _paper_trading_env_overrides())
def live_trading_config():

View File

@ -664,14 +664,20 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
notional = default_notional_usdt(cfg)
side = str(plan.get("side") or rec.get("side") or "long").strip().lower() or "long"
leverage = default_leverage(cfg)
stop_loss = _safe_float(rec.get("stop_loss") or plan.get("stop_loss"))
tp1 = _safe_float(rec.get("tp1") or plan.get("tp1") or plan.get("take_profit_1"))
stop_loss = _safe_float(plan.get("stop_loss") or rec.get("stop_loss"))
tp1 = _safe_float(plan.get("tp1") or plan.get("take_profit_1") or rec.get("tp1"))
rec_score = _safe_float(rec.get("rec_score") or rec.get("score"))
if rec_score <= 0 and rec_id > 0:
row = conn.execute("SELECT rec_score FROM recommendation WHERE id=%s", (rec_id,)).fetchone()
rec_score = _safe_float(row["rec_score"] if row else 0)
if bool(cfg.get("entry_gate_enabled", True)):
rr = _safe_float(plan.get("rr1") or plan.get("rr1_live")) or _trade_rr(side, entry_price, stop_loss, tp1)
calc_rr = _trade_rr(side, entry_price, stop_loss, tp1)
rr_candidates = [
_safe_float(plan.get("rr1")),
_safe_float(plan.get("rr1_live")),
calc_rr,
]
rr = max([x for x in rr_candidates if x > 0], default=0.0)
min_rr = max(0.0, _safe_float(cfg.get("entry_min_rr"), 0))
min_score = max(0.0, _safe_float(cfg.get("entry_min_rec_score"), 0))
sl_risk = _stop_loss_leverage_risk_pct(side, entry_price, stop_loss, leverage)
@ -904,8 +910,8 @@ def _paper_order_gate(rec: dict, current_price: float, config: dict | None = Non
plan = _entry_plan(rec)
side = str(plan.get("side") or rec.get("side") or "long").strip().lower() or "long"
target = _paper_order_target_price(rec)
stop_loss = _safe_float(rec.get("stop_loss") or plan.get("stop_loss"))
tp1 = _safe_float(rec.get("tp1") or plan.get("tp1") or plan.get("take_profit_1"))
stop_loss = _safe_float(plan.get("stop_loss") or rec.get("stop_loss"))
tp1 = _safe_float(plan.get("tp1") or plan.get("take_profit_1") or rec.get("tp1"))
rr = _safe_float(plan.get("rr1") or plan.get("rr1_live"))
calc_rr = _paper_order_rr(side, target, stop_loss, tp1)
# Wait-pullback orders must be judged at the intended limit price, not at
@ -1043,9 +1049,9 @@ def _order_payload_from_rec(rec: dict, current_price: float, event_time: str, co
"target_price": _paper_order_target_price(rec),
"current_price_at_create": current_price,
"notional_usdt": default_notional_usdt(cfg),
"stop_loss": _safe_float(rec.get("stop_loss") or plan.get("stop_loss")),
"tp1": _safe_float(rec.get("tp1") or plan.get("tp1") or plan.get("take_profit_1")),
"tp2": _safe_float(rec.get("tp2") or plan.get("tp2") or plan.get("take_profit_2")),
"stop_loss": _safe_float(plan.get("stop_loss") or rec.get("stop_loss")),
"tp1": _safe_float(plan.get("tp1") or plan.get("take_profit_1") or rec.get("tp1")),
"tp2": _safe_float(plan.get("tp2") or plan.get("take_profit_2") or rec.get("tp2")),
"strategy_version": str(rec.get("strategy_version") or ""),
"strategy_code": lineage["strategy_code"],
"strategy_name": lineage["strategy_name"],

View File

@ -340,7 +340,7 @@ def derive_execution_fields(item):
if item.get("latest_cache_updated_at"):
item["current_price_updated_at"] = item.get("latest_cache_updated_at")
entry_window = entry_window_policy(
item.get("entry_price") or entry_plan.get("entry_price") or 0,
entry_plan.get("entry_price") or item.get("entry_price") or 0,
current_price_for_window,
item.get("rec_time") or "",
) if action_status == "可即刻买入" else {}
@ -399,9 +399,9 @@ def derive_execution_fields(item):
if entry_window and entry_window.get("status") != "active":
item["entry_window_alert"] = entry_window
item["risk_suggestion"] = risk_suggestion(
item.get("entry_price") or entry_plan.get("entry_price") or 0,
item.get("stop_loss") or entry_plan.get("stop_loss") or 0,
item.get("tp1") or entry_plan.get("tp1") or entry_plan.get("take_profit_1") or 0,
entry_plan.get("entry_price") or item.get("entry_price") or 0,
entry_plan.get("stop_loss") or item.get("stop_loss") or 0,
entry_plan.get("tp1") or entry_plan.get("take_profit_1") or item.get("tp1") or 0,
)
item["market_context"] = market_context
item["derivatives_context"] = derivatives_context

File diff suppressed because one or more lines are too long

View File

@ -426,6 +426,42 @@ def test_buy_now_rejects_large_leveraged_stop_loss(monkeypatch):
assert list_paper_trades()["total"] == 0
def test_buy_now_entry_gate_uses_latest_entry_plan_rr(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", "50")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_RR", "1.5")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="PLANRR/USDT",
rec_state="爆发",
rec_score=67,
entry_price=0.0913,
stop_loss=0.085064,
tp1=0.0993,
signals=["当前15min即刻入场信号"],
entry_plan={
"entry_action": "可即刻买入",
"entry_price": 0.0899,
"current_price": 0.0899,
"stop_loss": 0.085064,
"tp1": 0.098243,
"risk_reward_ok": True,
"rr1": 1.73,
"entry_trigger_confirmed": True,
},
)
rec = next(r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False) if r["id"] == rec_id)
result = sync_recommendation(rec, 0.0899, event_time="2026-05-16T10:00:00")
assert result["opened"] is True
trade = list_paper_trades()["items"][0]
assert trade["stop_loss"] == pytest.approx(0.085064)
assert trade["tp1"] == pytest.approx(0.098243)
def test_buy_now_pauses_when_portfolio_drawdown_exceeded(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", "1000")

View File

@ -190,6 +190,32 @@ class RecommendationSignalTrustTests(unittest.TestCase):
self.assertAlmostEqual(target['entry_window']['current_price'], 10.12, places=4)
self.assertAlmostEqual(target['entry_window']['deviation_pct'], 1.2, places=2)
def test_entry_window_uses_latest_entry_plan_price_before_stale_top_level_price(self):
self._insert_rec(
rec_time=datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
entry_price=10.0,
current_price=9.75,
stop_loss=9.4,
tp1=10.8,
entry_plan_json=json.dumps({
'entry_price': 9.8,
'entry_action': '可即刻买入',
'risk_reward_ok': True,
'entry_trigger_confirmed': True,
'rr1': 2.0,
'stop_loss': 9.4,
'tp1': 10.8,
}, ensure_ascii=False),
)
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False, version='v-test')
target = rows[0]
self.assertEqual(target['entry_window']['status'], 'active')
self.assertEqual(target['execution_status'], 'buy_now')
self.assertAlmostEqual(target['entry_window']['entry_price'], 9.8, places=4)
self.assertAlmostEqual(target['entry_window']['deviation_pct'], -0.51, places=2)
if __name__ == '__main__':
unittest.main()