alphax/tests/test_paper_trading.py
2026-06-07 21:39:11 +08:00

1793 lines
75 KiB
Python

import json
from datetime import datetime
import pytest
from app.db import altcoin_db
from app.db.paper_trading import (
_gate_reject_lock_id,
_paper_order_gate,
delete_paper_order,
delete_paper_trade,
get_paper_trading_performance,
get_paper_trading_summary,
list_paper_orders,
list_paper_trade_events,
list_paper_trades,
reset_paper_trading_data,
send_paper_trading_report,
sync_pending_paper_orders,
sync_recommendation,
)
from app.db.recommendation_queries import get_opportunity_detail
from app.core.strategy_registry import (
LONG_MOMENTUM_BREAKOUT_STRATEGY,
SHORT_BREAKDOWN_RETEST_STRATEGY,
)
def _visible_card_text(card: dict) -> str:
texts = []
def walk(value):
if isinstance(value, dict):
content = value.get("content")
if isinstance(content, str):
texts.append(content)
for child in value.values():
walk(child)
elif isinstance(value, list):
for child in value:
walk(child)
walk(card.get("header"))
walk(card.get("elements"))
return "\n".join(texts)
def _assert_no_paper_trading_copy(card: dict) -> None:
text = _visible_card_text(card).lower()
assert "模拟" not in text
assert "paper trading" not in text
assert "paper-trading" not in text
assert "paper trade" not in text
assert "paper_trades" not in text
@pytest.fixture(autouse=True)
def legacy_paper_trade_thresholds(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "0")
monkeypatch.setenv("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", "20")
monkeypatch.setenv("ALPHAX_PAPER_ORDER_MIN_RR", "1.2")
monkeypatch.setenv("ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT", "0")
monkeypatch.setenv("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", "0")
monkeypatch.setenv("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", "0")
monkeypatch.setenv("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", "0")
monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", "0")
@pytest.fixture
def buy_now_rec(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="PAPER/USDT",
rec_state="爆发",
rec_score=28,
entry_price=100,
stop_loss=96,
tp1=106,
tp2=112,
signals=["当前15min即刻入场信号"],
entry_plan={
"entry_action": "可即刻买入",
"entry_price": 100,
"stop_loss": 96,
"tp1": 106,
"tp2": 112,
"risk_reward_ok": True,
"rr1": 1.5,
"entry_trigger_confirmed": True,
},
)
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False)
return next(r for r in rows if r["id"] == rec_id)
def test_buy_now_opens_paper_trade_once(buy_now_rec):
first = sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
second = sync_recommendation(buy_now_rec, 101, event_time="2026-05-16T10:01:00")
assert first["opened"] is True
assert second["updated"] is True
trades = list_paper_trades()["items"]
assert len(trades) == 1
assert trades[0]["symbol"] == "PAPER/USDT"
assert trades[0]["status"] == "open"
assert trades[0]["pnl_pct"] == pytest.approx(1.0)
def test_opportunity_detail_exposes_strategy_trade_markers(buy_now_rec):
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
sync_recommendation(buy_now_rec, 106, event_time="2026-05-16T10:05:00")
detail = get_opportunity_detail(symbol="PAPER/USDT", rec_id=buy_now_rec["id"])
markers = detail["trade_markers"]
labels = [item["label"] for item in markers]
assert "开仓" in labels
assert "平仓" in labels
assert all(item["time"] for item in markers)
assert all(item["source"] in {"paper_event", "paper_trade", "paper_order"} for item in markers)
def test_default_paper_trade_uses_5000u_notional_5x_and_1000u_margin(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.delenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", raising=False)
monkeypatch.delenv("ALPHAX_PAPER_TRADE_MARGIN_USDT", raising=False)
monkeypatch.delenv("ALPHAX_PAPER_TRADE_LEVERAGE", raising=False)
monkeypatch.delenv("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", raising=False)
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="DEFAULT/USDT",
rec_state="爆发",
rec_score=28,
entry_price=100,
stop_loss=95,
tp1=106,
tp2=112,
signals=["当前15min即刻入场信号"],
entry_plan={"entry_action": "可即刻买入", "entry_trigger_confirmed": True, "risk_reward_ok": True},
)
rec = next(r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False) if r["id"] == rec_id)
rec = {**rec, "action_status": "可即刻买入", "execution_status": "buy_now"}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
trade = list_paper_trades()["items"][0]
summary = get_paper_trading_summary(days=30)
assert trade["notional_usdt"] == pytest.approx(5000.0)
assert trade["leverage"] == pytest.approx(5.0)
assert trade["margin_usdt"] == pytest.approx(1000.0)
assert summary["account_equity_usdt"] == pytest.approx(20000.0)
assert summary["initial_equity_usdt"] == pytest.approx(20000.0)
assert summary["current_balance_usdt"] == pytest.approx(20000.0)
assert summary["open_position_value_usdt"] == pytest.approx(5000.0)
assert summary["cumulative_leverage"] == pytest.approx(0.25)
assert summary["notional_usdt"] == pytest.approx(5000.0)
assert summary["margin_usdt"] == pytest.approx(1000.0)
def test_paper_margin_is_derived_from_notional_and_leverage(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "5000")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_LEVERAGE", "5")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_MARGIN_USDT", "5000")
summary = get_paper_trading_summary(days=30)
assert summary["notional_usdt"] == pytest.approx(5000.0)
assert summary["leverage"] == pytest.approx(5.0)
assert summary["margin_usdt"] == pytest.approx(1000.0)
def test_paper_performance_returns_daily_equity_curve(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
now = datetime.now().replace(microsecond=0).isoformat()
sync_recommendation(buy_now_rec, 100, event_time=now)
sync_recommendation(buy_now_rec, 106, event_time=now)
curve = get_paper_trading_performance(days=7)
assert curve["points"]
assert curve["current_equity_usdt"] == pytest.approx(20006.0)
assert curve["total_pnl_usdt"] == pytest.approx(6.0)
assert curve["total_return_pct"] == pytest.approx(0.03)
assert curve["max_drawdown_pct"] >= 0
assert curve["points"][-1]["daily_pnl_usdt"] == pytest.approx(6.0)
assert curve["points"][-1]["equity_usdt"] == pytest.approx(20006.0)
def test_observation_does_not_open_paper_trade(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="OBS/USDT",
rec_state="加速",
rec_score=18,
entry_price=100,
stop_loss=95,
tp1=106,
signals=["历史放量"],
entry_plan={"entry_action": "观察", "entry_price": 100, "stop_loss": 95, "tp1": 106},
)
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False)
rec = next(r for r in rows if r["id"] == rec_id)
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["skipped"] is True
assert result["reason"] == "not_buy_now"
assert list_paper_trades()["total"] == 0
def test_wait_pullback_creates_pending_paper_order(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="WAIT/USDT",
rec_state="蓄力",
rec_score=22,
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},
)
with altcoin_db.get_conn() as conn:
conn.execute(
"UPDATE recommendation SET execution_status='wait_pullback', action_status='等回踩', display_bucket='watch_pool' WHERE id=%s",
(rec_id,),
)
conn.commit()
rec = {"id": rec_id, "symbol": "WAIT/USDT", "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}}
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["skipped"] is True
assert result["reason"] == "paper_order_created"
assert result["target_price"] == pytest.approx(95)
assert list_paper_trades()["total"] == 0
orders = list_paper_orders()["items"]
assert len(orders) == 1
assert orders[0]["status"] == "pending"
assert orders[0]["symbol"] == "WAIT/USDT"
def test_wait_pullback_paper_order_pushes_created_card(monkeypatch):
pushed = []
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setattr("app.db.paper_trading.push_card", lambda card: pushed.append(card) or (True, {"StatusCode": 0}))
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="PUSHORD/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
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": "PUSHORD/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert pushed
card = pushed[0]
assert card["metadata"]["event_type"] == "paper_order_create"
assert "挂单创建" in card["header"]["title"]["content"]
_assert_no_paper_trading_copy(card)
assert card["elements"][0]["tag"] == "column_set"
def test_wait_pullback_without_tradeable_plan_does_not_create_order(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="BADPLAN/USDT",
rec_state="蓄力",
rec_score=20,
entry_price=95,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95},
)
rec = {"id": rec_id, "symbol": "BADPLAN/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "entry_plan": {"entry_action": "等回踩", "entry_price": 95}}
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["reason"] == "paper_order_gate_rejected"
assert "missing_stop_loss" in result["gate_reasons"]
assert "missing_tp1" in result["gate_reasons"]
assert list_paper_orders()["total"] == 0
events = list_paper_trade_events(event_type="paper_gate_reject")["items"]
assert events
assert events[0]["symbol"] == "BADPLAN/USDT"
assert events[0]["detail"]["reason"] == "paper_order_gate_rejected"
assert "missing_stop_loss" in events[0]["detail"]["gate_reasons"]
def test_repeated_paper_gate_reject_is_deduped(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_GATE_REJECT_DEDUP_MINUTES", "30")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="DEDUP/USDT",
rec_state="蓄力",
rec_score=20,
entry_price=95,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95},
)
rec = {
"id": rec_id,
"symbol": "DEDUP/USDT",
"execution_status": "wait_pullback",
"action_status": "等回踩",
"entry_price": 95,
"entry_plan": {"entry_action": "等回踩", "entry_price": 95},
}
first = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
second = sync_recommendation(rec, 100, event_time="2026-05-16T10:05:00")
assert first["reason"] == "paper_order_gate_rejected"
assert second["reason"] == "paper_order_gate_rejected"
events = list_paper_trade_events(event_type="paper_gate_reject")["items"]
assert len(events) == 1
assert events[0]["symbol"] == "DEDUP/USDT"
def test_strategy_order_gate_threshold_wins_over_global_env(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", "25")
rec = {
"id": 0,
"symbol": "SHORTOK/USDT",
"strategy_code": SHORT_BREAKDOWN_RETEST_STRATEGY,
"rec_score": 18,
"execution_status": "wait_pullback",
"entry_plan": {
"strategy_code": SHORT_BREAKDOWN_RETEST_STRATEGY,
"entry_action": "等回踩",
"side": "short",
"entry_price": 100,
"stop_loss": 110,
"tp1": 85,
"risk_reward_ok": True,
"rr1": 1.5,
},
}
ok, reasons, detail = _paper_order_gate(rec, 100)
assert ok is True
assert "rec_score_below_min" not in reasons
assert detail["min_rec_score"] == 18
def test_gate_reject_lock_id_uses_stable_reject_identity():
base = {
"reason": "paper_order_gate_rejected",
"gate_reasons": ["rec_score_below_min"],
"action_status": "等回踩",
"execution_status": "wait_pullback",
}
changed_runtime = {**base, "action_status": "等待回踩", "execution_status": "observe"}
assert _gate_reject_lock_id(1, "paper_gate_reject", SHORT_BREAKDOWN_RETEST_STRATEGY, base) == _gate_reject_lock_id(
1,
"paper_gate_reject",
SHORT_BREAKDOWN_RETEST_STRATEGY,
changed_runtime,
)
def test_wait_pullback_requires_confirmed_risk_reward(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="NORR/USDT",
rec_state="蓄力",
rec_score=24,
entry_price=95,
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "rr1": 2.0},
)
rec = {"id": rec_id, "symbol": "NORR/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "rr1": 2.0}}
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["reason"] == "paper_order_gate_rejected"
assert "risk_reward_not_confirmed" in result["gate_reasons"]
assert list_paper_orders()["total"] == 0
def test_wait_pullback_recalculates_rr_at_target_price(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="TARGETRR/USDT",
rec_state="蓄力",
rec_score=24,
entry_price=95,
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={
"entry_action": "等回踩",
"entry_price": 95,
"stop_loss": 90,
"tp1": 105,
"risk_reward_ok": False,
"rr1": 0.6,
},
)
rec = {
"id": rec_id,
"symbol": "TARGETRR/USDT",
"execution_status": "wait_pullback",
"action_status": "等回踩",
"entry_price": 95,
"stop_loss": 90,
"tp1": 105,
"entry_plan": {
"entry_action": "等回踩",
"entry_price": 95,
"stop_loss": 90,
"tp1": 105,
"risk_reward_ok": False,
"rr1": 0.6,
},
}
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["reason"] == "paper_order_created"
assert result["gate_detail"]["rr1"] == pytest.approx(2.0)
assert result["gate_detail"]["calc_rr1"] == pytest.approx(2.0)
assert list_paper_orders()["total"] == 1
def test_wait_pullback_too_far_from_entry_does_not_create_order(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="FARGATE/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
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": "FARGATE/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
result = sync_recommendation(rec, 110, event_time="2026-05-16T10:00:00")
assert result["reason"] == "paper_order_gate_rejected"
assert "too_far_from_entry" in result["gate_reasons"]
assert result["gate_detail"]["distance_to_entry_pct"] > 8
assert list_paper_orders()["total"] == 0
def test_wait_pullback_too_close_to_entry_does_not_create_order(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT", "1.5")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="CLOSEWAIT/USDT",
rec_state="蓄力",
rec_score=60,
entry_price=99,
stop_loss=95,
tp1=107,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 99, "stop_loss": 95, "tp1": 107, "risk_reward_ok": True, "rr1": 2.0},
)
rec = {"id": rec_id, "symbol": "CLOSEWAIT/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 99, "stop_loss": 95, "tp1": 107, "entry_plan": {"entry_action": "等回踩", "entry_price": 99, "stop_loss": 95, "tp1": 107, "risk_reward_ok": True, "rr1": 2.0}}
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["reason"] == "paper_order_gate_rejected"
assert "too_close_to_entry" in result["gate_reasons"]
assert list_paper_orders()["total"] == 0
def test_buy_now_lowers_leverage_for_large_stop_loss(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_LEVERAGE", "5")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", "50")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_RR", "1.8")
monkeypatch.setenv("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", "20")
monkeypatch.setenv("ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="WIDESTOP/USDT",
rec_state="爆发",
rec_score=60,
entry_price=100,
stop_loss=90,
tp1=120,
signals=["当前15min即刻入场信号"],
entry_plan={"entry_action": "可即刻买入", "entry_price": 100, "stop_loss": 90, "tp1": 120, "risk_reward_ok": True, "rr1": 2.0, "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, 100, event_time="2026-05-16T10:00:00")
assert result["opened"] is True, result
trade = list_paper_trades()["items"][0]
assert trade["leverage"] < 5
assert trade["leverage"] >= 1
assert trade["leverage"] * 10.05 <= 20.01
def test_buy_now_rejects_large_leveraged_stop_loss_when_dynamic_leverage_disabled(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_LEVERAGE", "5")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", "50")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_RR", "1.8")
monkeypatch.setenv("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", "20")
monkeypatch.setenv("ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED", "0")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="WIDESTOP2/USDT",
rec_state="爆发",
rec_score=60,
entry_price=100,
stop_loss=90,
tp1=120,
signals=["当前15min即刻入场信号"],
entry_plan={"entry_action": "可即刻买入", "entry_price": 100, "stop_loss": 90, "tp1": 120, "risk_reward_ok": True, "rr1": 2.0, "entry_trigger_confirmed": True},
)
rec = next(r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False) if r["id"] == rec_id)
rec = {**rec, "action_status": "可即刻买入", "execution_status": "buy_now"}
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["reason"] == "entry_gate_rejected"
assert "stop_loss_leverage_risk_exceeded" in result["gate_reasons"]
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_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", "20")
monkeypatch.setenv("ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN", "3")
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)
rec = {**rec, "action_status": "可即刻买入", "execution_status": "buy_now"}
result = sync_recommendation(rec, 0.0899, event_time="2026-05-16T10:00:00")
assert result["opened"] is True, result
trade = list_paper_trades()["items"][0]
assert trade["stop_loss"] == pytest.approx(0.085064)
assert trade["tp1"] == pytest.approx(0.098243)
assert trade["leverage"] < 5
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")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
monkeypatch.setenv("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", "3")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "0")
altcoin_db.init_db()
with altcoin_db.get_conn() as conn:
conn.execute(
"""
INSERT INTO paper_trades (
recommendation_id, symbol, side, status, opened_at, entry_price,
qty, notional_usdt, stop_loss, tp1, tp2, max_price, min_price,
current_price, pnl_pct, source_status, source_action,
strategy_version, created_at, updated_at, margin_usdt, leverage
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
""",
(
9001,
"DRAWDOWN/USDT",
"long",
"open",
"2026-05-16T09:00:00",
100,
1,
100,
1,
120,
130,
100,
60,
60,
-40,
"test",
"test",
"",
"2026-05-16T09:00:00",
"2026-05-16T09:01:00",
20,
5,
),
)
conn.commit()
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="PAUSE/USDT",
rec_state="爆发",
rec_score=80,
entry_price=100,
stop_loss=95,
tp1=110,
signals=["当前15min即刻入场信号"],
entry_plan={"entry_action": "可即刻买入", "entry_trigger_confirmed": True, "risk_reward_ok": True, "rr1": 2.0},
)
rec = next(r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False) if r["id"] == rec_id)
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:02:00")
assert result["reason"] == "portfolio_drawdown_pause"
assert list_paper_trades(status="open")["total"] == 1
def test_buy_now_rejects_when_cumulative_leverage_exceeded(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", "100")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "500")
monkeypatch.setenv("ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE", "5")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db()
first_id = altcoin_db.create_recommendation(
symbol="LEV1/USDT",
rec_state="爆发",
rec_score=28,
entry_price=100,
stop_loss=95,
tp1=106,
signals=["当前15min即刻入场信号"],
entry_plan={"entry_action": "可即刻买入", "entry_trigger_confirmed": True, "risk_reward_ok": True},
)
second_id = altcoin_db.create_recommendation(
symbol="LEV2/USDT",
rec_state="爆发",
rec_score=28,
entry_price=100,
stop_loss=95,
tp1=106,
signals=["当前15min即刻入场信号"],
entry_plan={"entry_action": "可即刻买入", "entry_trigger_confirmed": True, "risk_reward_ok": True},
)
rows = {r["id"]: r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False)}
first = sync_recommendation(rows[first_id], 100, event_time="2026-05-16T10:00:00")
second = sync_recommendation(rows[second_id], 100, event_time="2026-05-16T10:01:00")
assert first["opened"] is True
assert second["reason"] == "cumulative_leverage_exceeded"
assert second["risk_detail"]["projected_cumulative_leverage"] > 5
assert list_paper_trades(status="open")["total"] == 1
def test_buy_now_uses_reduced_size_when_global_market_risk_is_critical(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_BLOCK_CRITICAL", "0")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "0")
monkeypatch.setenv("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", "0")
monkeypatch.setattr(
"app.core.global_risk.get_crypto_market_overview",
lambda allow_live_fallback=False: {
"sample_count": 200,
"benchmarks": {"BTC/USDT": {"change_24h": -3.5}, "ETH/USDT": {"change_24h": -4.1}},
"advance_decline_ratio": 0.45,
"avg_change_24h": -2.2,
"hot_count_5pct": 2,
"crash_count_5pct": 36,
"funding": {"avg_funding_rate": -0.0001, "extreme_positive_count": 0},
},
)
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="RISKOFF/USDT",
rec_state="爆发",
rec_score=95,
entry_price=100,
stop_loss=95,
tp1=112,
signals=["当前15min即刻入场信号"],
entry_plan={"entry_action": "可即刻买入", "entry_trigger_confirmed": True, "risk_reward_ok": True, "rr1": 2.4},
)
rec = next(r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False) if r["id"] == rec_id)
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["opened"] is True
assert result["global_risk"]["risk_level"] == "critical"
assert result["global_risk"]["decision"] == "allow_reduced_size"
assert result["notional_usdt"] == pytest.approx(1250.0)
assert list_paper_trades()["total"] == 1
def test_short_buy_now_not_reduced_by_risk_off_market(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_BLOCK_CRITICAL", "0")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
monkeypatch.setattr(
"app.core.global_risk.get_crypto_market_overview",
lambda allow_live_fallback=False: {
"sample_count": 200,
"benchmarks": {"BTC/USDT": {"change_24h": -3.5}, "ETH/USDT": {"change_24h": -4.1}},
"advance_decline_ratio": 0.45,
"avg_change_24h": -2.2,
"hot_count_5pct": 2,
"crash_count_5pct": 36,
"funding": {"avg_funding_rate": -0.0001, "extreme_positive_count": 0},
},
)
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="SHORTOFF/USDT",
rec_state="爆发",
rec_score=60,
entry_price=100,
stop_loss=105,
tp1=90,
signals=["1H破位反抽做空"],
direction="空头启动",
entry_plan={"side": "short", "entry_action": "可即刻买入", "entry_trigger_confirmed": True, "risk_reward_ok": True, "rr1": 2.0},
)
rec = next(r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False) if r["id"] == rec_id)
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["opened"] is True
assert result["side"] == "short"
assert result["global_risk"]["directional_market_bias"]["market_bias"] == "favorable"
assert result["notional_usdt"] == pytest.approx(100.0)
def test_open_event_records_market_regime_and_score_components(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "0")
monkeypatch.setattr(
"app.core.global_risk.get_crypto_market_overview",
lambda allow_live_fallback=False: {
"sample_count": 200,
"benchmarks": {"BTC/USDT": {"change_24h": 0.8}, "ETH/USDT": {"change_24h": 1.2}},
"advance_decline_ratio": 1.5,
"avg_change_24h": 1.4,
"hot_count_5pct": 25,
"crash_count_5pct": 2,
"funding": {"avg_funding_rate": 0.0001, "extreme_positive_count": 3},
},
)
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="REGIME/USDT",
rec_state="爆发",
rec_score=95,
entry_price=100,
stop_loss=95,
tp1=112,
signals=["当前15min即刻入场信号"],
entry_plan={
"entry_action": "可即刻买入",
"entry_trigger_confirmed": True,
"risk_reward_ok": True,
"rr1": 2.4,
"score_components": {"opportunity_score": 14, "entry_score": 4, "risk_score": 1},
},
)
rec = next(r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False) if r["id"] == rec_id)
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
events = list_paper_trade_events(symbol="REGIME/USDT")["items"]
assert result["opened"] is True
assert events[0]["event_type"] == "open"
assert events[0]["detail"]["market_regime"]["regime"] == "altcoin_rotation"
assert events[0]["detail"]["score_components"]["opportunity_score"] == 14
def test_observe_only_wait_pullback_does_not_create_order(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="OBSWAIT/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={
"entry_action": "等回踩",
"entry_price": 95,
"stop_loss": 90,
"tp1": 105,
"risk_reward_ok": True,
"rr1": 2.0,
"opportunity_level": "theme_trend",
},
)
rec = {"id": rec_id, "symbol": "OBSWAIT/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0, "opportunity_level": "theme_trend"}}
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["reason"] == "paper_order_gate_rejected"
assert "observe_only_opportunity" in result["gate_reasons"]
assert list_paper_orders()["total"] == 0
def test_wait_pullback_paper_order_fills_when_price_touches(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="FILL/USDT",
rec_state="蓄力",
rec_score=22,
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},
)
with altcoin_db.get_conn() as conn:
conn.execute(
"UPDATE recommendation SET execution_status='wait_pullback', action_status='等回踩', display_bucket='watch_pool' WHERE id=%s",
(rec_id,),
)
conn.commit()
rec = {"id": rec_id, "symbol": "FILL/USDT", "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")
filled = sync_recommendation(rec, 94.9, event_time="2026-05-16T10:05:00")
assert created["reason"] == "paper_order_created"
assert filled["opened"] is True
assert filled["paper_order"]["filled"] is True
trade = list_paper_trades()["items"][0]
assert trade["entry_price"] == pytest.approx(95)
order = list_paper_orders(status="filled")["items"][0]
assert order["fill_price"] == pytest.approx(95)
def test_pending_paper_order_reconciles_from_latest_price_cache(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="CACHEFILL/USDT",
rec_state="蓄力",
rec_score=22,
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": "CACHEFILL/USDT",
"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},
}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
altcoin_db.update_latest_price_cache("CACHEFILL/USDT", 94.9, updated_at="2026-05-16T10:05:00", source="test")
result = sync_pending_paper_orders(event_time="2026-05-16T10:05:00")
assert result["filled_count"] == 1
assert result["results"][0]["paper_order"]["filled"] is True
trade = list_paper_trades()["items"][0]
assert trade["symbol"] == "CACHEFILL/USDT"
order = list_paper_orders(status="filled")["items"][0]
assert order["fill_price"] == pytest.approx(95)
def test_touched_wait_pullback_order_ignores_market_only_critical_pause(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_critical",
"risk_level": "critical",
"position_multiplier": 0,
"reasons": ["critical 市场环境下推荐分不足"],
"portfolio": {"unrealized_drawdown_pct": 0.0},
"critical_drawdown_pct": 6.0,
},
)
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="RISKPAUSE/USDT",
rec_state="蓄力",
rec_score=22,
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": "RISKPAUSE/USDT",
"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")
filled = sync_recommendation(rec, 94.9, event_time="2026-05-16T10:05:00")
assert created["reason"] == "paper_order_created"
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_orders(status="pending")["total"] == 0
canceled = list_paper_orders(status="canceled")["items"][0]
assert canceled["symbol"] == "HARDLIMIT/USDT"
assert canceled["cancel_reason"] == "touch_max_open_positions"
def test_touched_order_soft_global_risk_gate_fills_instead_of_canceling(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_high_weak_score",
"risk_level": "high",
"position_multiplier": 0.5,
"reasons": ["高风险环境下推荐分不足"],
},
)
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="SOFTOUCH/USDT",
rec_state="蓄力",
rec_score=60,
entry_price=95,
stop_loss=90,
tp1=105,
tp2=112,
signals=["等待回踩"],
entry_plan={"side": "long", "entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
rec = {
"id": rec_id,
"symbol": "SOFTOUCH/USDT",
"rec_score": 60,
"execution_status": "wait_pullback",
"action_status": "等回踩",
"entry_price": 95,
"stop_loss": 90,
"tp1": 105,
"tp2": 112,
"entry_plan": {"side": "long", "entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
result = sync_recommendation(rec, 94.9, event_time="2026-05-16T10:05:00")
assert result.get("opened") is True
assert result["global_risk"]["touch_soft_gate_overridden"] is True
assert list_paper_orders(status="canceled")["total"] == 0
assert list_paper_trades()["total"] == 1
def test_touched_order_uses_order_snapshot_side_for_global_risk(monkeypatch, pg_conn):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", "1")
captured = []
def fake_global_risk(**kwargs):
captured.append(dict(kwargs.get("rec") or {}))
return {
"allow_new_entries": True,
"decision": "allow",
"risk_level": "medium",
"position_multiplier": 1,
"reasons": ["测试允许成交"],
}
monkeypatch.setattr("app.db.paper_trading.evaluate_global_risk", fake_global_risk)
altcoin_db.init_db()
pg_conn.execute(
"""
INSERT INTO recommendation (
id, symbol, rec_time, rec_state, rec_score, entry_price,
status, execution_status, action_status, display_bucket, entry_triggered,
stop_loss, tp1, tp2, strategy_code, entry_plan_json
) VALUES (
301, 'SNAPSHORT/USDT', '2026-05-16T10:00:00', '蓄力', 88, 105,
'active', 'wait_pullback', '等回踩', 'watch_pool', 0,
95, 115, 122, 'long_momentum_breakout_15m_1h_v1', %s
)
""",
(json.dumps({"side": "long", "entry_action": "等回踩", "entry_price": 105, "stop_loss": 95, "tp1": 115, "rr1": 2.0}, ensure_ascii=False),),
)
pg_conn.execute(
"""
INSERT INTO paper_orders (
recommendation_id, symbol, side, order_type, status,
source_status, source_action, target_price, current_price_at_create,
notional_usdt, stop_loss, tp1, tp2, strategy_version, strategy_code,
strategy_signal_id, strategy_snapshot_json, factor_roles_json,
entry_plan_snapshot_json, created_at, updated_at, expires_at
) VALUES (
301, 'SNAPSHORT/USDT', 'short', 'limit', 'pending',
'wait_pullback', '等反抽', 105, 100,
5000, 110, 95, 90, 'v-test', 'short_breakdown_retest_1h_v1',
0, %s, '{}', %s, '2026-05-16T10:00:00', '2026-05-16T10:00:00', '2026-05-17T10:00:00'
)
""",
(
json.dumps({"strategy_code": "short_breakdown_retest_1h_v1"}, ensure_ascii=False),
json.dumps({"side": "short", "entry_action": "等反抽", "entry_price": 105, "stop_loss": 110, "tp1": 95, "rr1": 2.0}, ensure_ascii=False),
),
)
pg_conn.commit()
altcoin_db.update_latest_price_cache("SNAPSHORT/USDT", 106, updated_at="2026-05-16T10:05:00", source="test")
result = sync_pending_paper_orders(event_time="2026-05-16T10:05:00")
assert result["filled_count"] == 1
assert captured
assert all(x.get("side") == "short" for x in captured)
assert all(x.get("strategy_code") == "short_breakdown_retest_1h_v1" for x in captured)
trade = list_paper_trades()["items"][0]
assert trade["side"] == "short"
assert trade["strategy_code"] == "short_breakdown_retest_1h_v1"
def test_wait_pullback_order_cancels_when_recommendation_invalid(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="CANCEL/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
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": "CANCEL/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
with altcoin_db.get_conn() as conn:
conn.execute("UPDATE recommendation SET status='invalid', execution_status='invalid' WHERE id=%s", (rec_id,))
conn.commit()
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:05:00")
assert result["reason"] == "paper_order_recommendation_invalid"
order = list_paper_orders(status="canceled")["items"][0]
assert order["cancel_reason"] == "recommendation_invalid"
def test_wait_pullback_order_cancels_when_price_runs_too_far(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="FAR/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
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": "FAR/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
result = sync_recommendation(rec, 108, event_time="2026-05-16T10:05:00")
assert result["reason"] == "paper_order_too_far_from_entry"
order = list_paper_orders(status="canceled")["items"][0]
assert order["cancel_reason"] == "too_far_from_entry"
assert list_paper_trades()["total"] == 0
def test_wait_pullback_order_fills_before_same_tick_stop_loss(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="GAP/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
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": "GAP/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
result = sync_recommendation(rec, 89, event_time="2026-05-16T10:05:00")
assert result["opened"] is True
assert result["closed"] is True
assert result["same_tick_stop_loss"] is True
assert result["exit_reason"] == "stop_loss_same_tick"
order = list_paper_orders(status="filled")["items"][0]
assert order["fill_price"] == pytest.approx(95)
trade = list_paper_trades()["items"][0]
assert trade["status"] == "closed"
assert trade["entry_price"] == pytest.approx(95)
assert trade["exit_price"] == pytest.approx(89)
def test_wait_pullback_paper_order_fill_pushes_single_combined_card(monkeypatch):
pushed = []
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
monkeypatch.setattr("app.db.paper_trading.push_card", lambda card: pushed.append(card) or (True, {"StatusCode": 0}))
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="FILLPUSH/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
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": "FILLPUSH/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
sync_recommendation(rec, 94.9, event_time="2026-05-16T10:05:00")
event_types = [card["metadata"]["event_type"] for card in pushed]
assert event_types == ["paper_order_create", "paper_order_fill"]
assert "挂单成交并开仓" in pushed[1]["header"]["title"]["content"]
_assert_no_paper_trading_copy(pushed[1])
assert "open" not in event_types
def test_paper_trade_open_push_card_is_structured(monkeypatch, buy_now_rec):
pushed = []
monkeypatch.setattr("app.db.paper_trading.push_card", lambda card: pushed.append(card) or (True, {"StatusCode": 0}))
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
assert pushed
card = pushed[0]
assert card["metadata"]["event_type"] == "open"
assert "交易开仓" in card["header"]["title"]["content"]
_assert_no_paper_trading_copy(card)
assert card["elements"][0]["tag"] == "column_set"
def test_trailing_move_push_is_throttled_but_stop_still_updates(monkeypatch, buy_now_rec):
pushed = []
rec = dict(buy_now_rec)
rec["tp1"] = 200
rec["tp2"] = 220
rec["entry_plan"] = {
"entry_action": "可即刻买入",
"entry_price": 100,
"stop_loss": 95,
"tp1": 200,
"tp2": 220,
"entry_trigger_confirmed": True,
"risk_reward_ok": True,
}
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MODE", "fixed")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT", "3")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MOVE_PUSH_MIN_INTERVAL_SECONDS", "300")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MOVE_PUSH_MIN_STEP_PCT", "2")
monkeypatch.setattr("app.db.paper_trading.push_card", lambda card: pushed.append(card) or (True, {"StatusCode": 0}))
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
activated = sync_recommendation(rec, 105, event_time="2026-05-16T10:01:00")
small_move = sync_recommendation(rec, 105.5, event_time="2026-05-16T10:01:05")
large_move = sync_recommendation(rec, 108, event_time="2026-05-16T10:01:10")
assert small_move["moved"] is True
assert small_move["trailing_stop"] > activated["trailing_stop"]
assert small_move["notification_emitted"] is False
assert large_move["notification_emitted"] is True
assert [card["metadata"]["event_type"] for card in pushed] == ["open", "trailing_activate", "trailing_move"]
_assert_no_paper_trading_copy(pushed[-1])
def test_send_paper_trading_report_pushes_performance_summary(monkeypatch, buy_now_rec):
pushed = []
monkeypatch.setattr("app.db.paper_trading.push_card", lambda card: pushed.append(card) or (True, {"StatusCode": 0}))
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
sync_recommendation(buy_now_rec, 106, event_time="2026-05-16T10:05:00")
result = send_paper_trading_report(days=30)
assert result["ok"] is True
assert result["running_days"] >= 1
assert result["monthly_return_pct"] is None
assert result["annualized_return_pct"] is None
assert result["monthly_return_label"] == "样本不足"
assert result["annualized_return_label"] == "样本不足"
assert pushed[-1]["metadata"]["event_type"] == "trade_report"
text = _visible_card_text(pushed[-1])
assert "交易报告" in text
assert "初始资金" in text
assert "当前资金" in text
assert "运行天数" in text
assert "账户收益率" in text
assert "月化收益率" in text
assert "年化收益率" in text
assert "成功率" in text
assert "战绩概览" in text
_assert_no_paper_trading_copy(pushed[-1])
def test_summary_counts_pending_paper_orders(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="COUNT/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
with altcoin_db.get_conn() as conn:
conn.execute(
"UPDATE recommendation SET execution_status='wait_pullback', action_status='等回踩', display_bucket='watch_pool' WHERE id=%s",
(rec_id,),
)
conn.commit()
rec = {"id": rec_id, "symbol": "COUNT/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert get_paper_trading_summary(days=30)["pending_order_count"] == 1
def test_open_paper_trade_closes_on_tp1_and_summary_counts_win(buy_now_rec):
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
result = sync_recommendation(buy_now_rec, 106, event_time="2026-05-16T10:05:00")
assert result["closed"] is True
assert result["exit_reason"] == "tp1"
assert result["pnl_pct"] == pytest.approx(6.0)
summary = get_paper_trading_summary(days=30)
assert summary["closed_count"] == 1
assert summary["win_count"] == 1
assert summary["win_rate"] == pytest.approx(100.0)
def test_short_paper_trade_opens_and_closes_on_take_profit(monkeypatch, buy_now_rec):
rec = dict(buy_now_rec)
rec["side"] = "short"
rec["stop_loss"] = 105
rec["tp1"] = 94
rec["tp2"] = 90
rec["entry_plan"] = {
"side": "short",
"entry_action": "可即刻买入",
"entry_price": 100,
"stop_loss": 105,
"tp1": 94,
"tp2": 90,
"risk_reward_ok": True,
"rr1": 1.2,
"entry_trigger_confirmed": True,
}
opened = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
closed = sync_recommendation(rec, 94, event_time="2026-05-16T10:10:00")
assert opened["opened"] is True
assert closed["closed"] is True
assert closed["exit_reason"] == "tp1"
assert closed["pnl_pct"] > 0
trade = list_paper_trades()["items"][0]
assert trade["side"] == "short"
assert trade["status"] == "closed"
assert list_paper_trades(side="short")["total"] == 1
assert list_paper_trades(side="long")["total"] == 0
def test_short_paper_trade_closes_on_stop_loss(monkeypatch, buy_now_rec):
rec = dict(buy_now_rec)
rec["side"] = "short"
rec["stop_loss"] = 105
rec["tp1"] = 94
rec["entry_plan"] = {
"side": "short",
"entry_action": "可即刻买入",
"entry_price": 100,
"stop_loss": 105,
"tp1": 94,
"risk_reward_ok": True,
"rr1": 1.2,
"entry_trigger_confirmed": True,
}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
result = sync_recommendation(rec, 106, event_time="2026-05-16T10:10:00")
assert result["closed"] is True
assert result["exit_reason"] == "stop_loss"
assert result["pnl_pct"] < 0
def test_paper_trading_trailing_stop_activates_moves_and_closes(monkeypatch, buy_now_rec):
pushed = []
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MODE", "fixed")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT", "3")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MIN_LOCK_PROFIT_PCT", "0.5")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_DISTANCE_PCT", "1.5")
monkeypatch.setattr("app.db.paper_trading.push_card", lambda card: pushed.append(card) or (True, {"StatusCode": 0}))
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
activated = sync_recommendation(buy_now_rec, 105, event_time="2026-05-16T10:01:00")
moved = sync_recommendation(buy_now_rec, 105.5, event_time="2026-05-16T10:02:00")
closed = sync_recommendation(buy_now_rec, moved["trailing_stop"] * 0.999, event_time="2026-05-16T10:03:00")
assert activated["activated"] is True
assert activated["trailing_stop"] >= 100.5
assert moved["moved"] is True
assert moved["trailing_stop"] > activated["trailing_stop"]
assert closed["closed"] is True
assert closed["exit_reason"] == "trailing_stop"
trade = list_paper_trades()["items"][0]
assert trade["status"] == "closed"
assert trade["exit_reason"] == "trailing_stop"
assert trade["exit_price"] > trade["entry_price"]
assert pushed[-1]["metadata"]["event_type"] == "close"
assert "移动止盈成交平仓" in pushed[-1]["header"]["title"]["content"]
_assert_no_paper_trading_copy(pushed[-1])
def test_volatility_trailing_uses_wider_distance_for_choppy_coin(monkeypatch, buy_now_rec):
rec = dict(buy_now_rec)
rec["tp1"] = 200
rec["tp2"] = 220
rec["entry_plan"] = {
"entry_action": "可即刻买入",
"entry_price": 100,
"stop_loss": 95,
"tp1": 200,
"tp2": 220,
"entry_trigger_confirmed": True,
"risk_reward_ok": True,
}
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MODE", "volatility")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT", "3")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_DISTANCE_PCT", "1.5")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_VOL_ACTIVATE_MULT", "0.6")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_VOL_DISTANCE_MULT", "0.7")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_VOL_MAX_DISTANCE_PCT", "8")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_VOL_MAX_ACTIVATE_PCT", "8")
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
sync_recommendation(rec, 98, event_time="2026-05-16T10:01:00")
result = sync_recommendation(rec, 110, event_time="2026-05-16T10:02:00")
assert result["activated"] is True
assert result["trailing_mode"] == "volatility"
assert result["volatility_pct"] == pytest.approx(12.0)
assert result["activate_pnl_pct"] == pytest.approx(7.2)
assert result["distance_pct"] == pytest.approx(8.0)
assert result["trailing_stop"] == pytest.approx(101.2)
def test_volatility_trailing_stays_tighter_for_smooth_coin(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MODE", "volatility")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT", "3")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_DISTANCE_PCT", "1.5")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_VOL_DISTANCE_MULT", "0.7")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_VOL_MIN_DISTANCE_PCT", "1.2")
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
result = sync_recommendation(buy_now_rec, 104, event_time="2026-05-16T10:01:00")
assert result["activated"] is True
assert result["trailing_mode"] == "volatility"
assert result["volatility_pct"] == pytest.approx(4.0)
assert result["distance_pct"] == pytest.approx(2.8)
assert result["trailing_stop"] == pytest.approx(101.088)
def test_paper_push_failure_is_recorded(monkeypatch, buy_now_rec):
errors = []
monkeypatch.setattr("app.db.paper_trading.push_card", lambda card: (False, "webhook failed"))
monkeypatch.setattr("app.db.paper_trading.record_system_error", lambda **kwargs: errors.append(kwargs) or 1)
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
assert errors
assert errors[0]["error_type"] == "FeishuPushFailed"
assert errors[0]["context"]["event_type"] == "open"
def test_paper_trading_trailing_stop_never_moves_down(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MODE", "fixed")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT", "3")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MIN_LOCK_PROFIT_PCT", "0.5")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_DISTANCE_PCT", "1.5")
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
high = sync_recommendation(buy_now_rec, 105, event_time="2026-05-16T10:01:00")
pullback = sync_recommendation(buy_now_rec, 104, event_time="2026-05-16T10:02:00")
assert high["activated"] is True
assert pullback["updated"] is True
assert pullback.get("trailing_stop", high["trailing_stop"]) == pytest.approx(high["trailing_stop"])
assert pullback.get("moved") is False
def test_position_guard_tightens_when_trade_does_not_launch(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "0")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_SOFT_HOURS", "6")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_SOFT_MIN_MAX_PNL_PCT", "1.5")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_HARD_HOURS", "24")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_TIGHTEN_LOCK_PROFIT_PCT", "0.15")
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
result = sync_recommendation(buy_now_rec, 100.4, event_time="2026-05-16T16:01:00")
assert result["updated"] is True
assert result["tightened"] is True
assert result["trailing_stop"] == pytest.approx(100.15)
assert result["position_health"]["reason"] == "position_timeout_soft"
trade = list_paper_trades(status="open")["items"][0]
assert trade["trailing_stop"] == pytest.approx(100.15)
events = list_paper_trade_events(symbol="PAPER/USDT", limit=20)["items"]
assert "position_guard_tighten" in [e["event_type"] for e in events]
def test_position_guard_closes_stale_unlaunched_trade(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "0")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_SOFT_HOURS", "0")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_HARD_HOURS", "1")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_HARD_MIN_MAX_PNL_PCT", "2.5")
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
result = sync_recommendation(buy_now_rec, 100.4, event_time="2026-05-16T11:10:00")
assert result["closed"] is True
assert result["exit_reason"] == "position_timeout_weak"
trade = list_paper_trades()["items"][0]
assert trade["status"] == "closed"
assert trade["exit_reason"] == "position_timeout_weak"
def test_position_guard_closes_profit_giveback_before_trailing(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "0")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_SOFT_HOURS", "0")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_HARD_HOURS", "0")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_GIVEBACK_MIN_MAX_PNL_PCT", "2")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_GIVEBACK_EXIT_PCT", "70")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_GIVEBACK_EXIT_CURRENT_PNL_PCT", "0.6")
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
sync_recommendation(buy_now_rec, 102.5, event_time="2026-05-16T10:20:00")
result = sync_recommendation(buy_now_rec, 100.5, event_time="2026-05-16T10:40:00")
assert result["closed"] is True
assert result["exit_reason"] == "profit_giveback_before_trailing"
assert result["pnl_pct"] == pytest.approx(0.5)
def test_position_guard_closes_unprotected_trade_when_market_turns_critical(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "0")
monkeypatch.setenv("ALPHAX_PAPER_GLOBAL_RISK_GATE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_CRITICAL_MIN_AGE_HOURS", "0.5")
monkeypatch.setenv("ALPHAX_PAPER_POSITION_GUARD_CRITICAL_MAX_PNL_PCT", "1")
monkeypatch.setattr(
"app.db.paper_trading.evaluate_global_risk",
lambda **kwargs: {
"enabled": True,
"allow_new_entries": True,
"risk_level": "critical",
"decision": "allow_reduced_size",
"reasons": ["测试环境进入 critical"],
},
)
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
result = sync_recommendation(buy_now_rec, 100.5, event_time="2026-05-16T10:40:00")
assert result["closed"] is True
assert result["exit_reason"] == "market_risk_unprotected"
def test_paper_trading_events_capture_open_close_and_trailing(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MODE", "fixed")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_ACTIVATE_PNL_PCT", "3")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_MIN_LOCK_PROFIT_PCT", "0.5")
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_DISTANCE_PCT", "1.5")
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
sync_recommendation(buy_now_rec, 105, event_time="2026-05-16T10:01:00")
sync_recommendation(buy_now_rec, 104.9, event_time="2026-05-16T10:02:00")
events = list_paper_trade_events(limit=20)["items"]
types = [e["event_type"] for e in events]
assert "open" in types
assert "trailing_activate" in types
assert "trailing_move" not in types or isinstance(types, list)
def test_closed_paper_trade_keeps_exit_price_and_shows_latest_market_price(buy_now_rec):
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
sync_recommendation(buy_now_rec, 106, event_time="2026-05-16T10:05:00")
altcoin_db.update_latest_price_cache("PAPER/USDT", 103.25, updated_at="2026-05-16T10:10:00", source="unit")
trade = list_paper_trades()["items"][0]
assert trade["status"] == "closed"
assert trade["exit_price"] == pytest.approx(106.0)
assert trade["current_price"] == pytest.approx(106.0)
assert trade["latest_price"] == pytest.approx(103.25)
assert trade["latest_price_updated_at"] == "2026-05-16T10:10:00"
def test_disabled_paper_trading_skips_without_writing(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "0")
result = sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
assert result["skipped"] is True
assert result["reason"] == "disabled"
assert list_paper_trades()["total"] == 0
def test_delete_paper_trade_removes_related_events(buy_now_rec):
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
trade = list_paper_trades()["items"][0]
assert list_paper_trade_events()["total"] == 1
result = delete_paper_trade(trade["id"])
assert result["deleted"] is True
assert result["deleted_events"] == 1
assert list_paper_trades()["total"] == 0
assert list_paper_trade_events()["total"] == 0
def test_delete_paper_order_removes_order_only(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="DELORD/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
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": "DELORD/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
order = list_paper_orders()["items"][0]
result = delete_paper_order(order["id"])
assert result["deleted"] is True
assert list_paper_orders()["total"] == 0
def test_pending_order_distance_never_negative_for_touched_long_or_short(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
long_rec_id = altcoin_db.create_recommendation(
symbol="LONGTOUCH/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
short_rec_id = altcoin_db.create_recommendation(
symbol="SHORTTOUCH/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=105,
stop_loss=110,
tp1=95,
signals=["等待反抽"],
entry_plan={"entry_action": "等反抽", "side": "short", "entry_price": 105, "stop_loss": 110, "tp1": 95, "risk_reward_ok": True, "rr1": 2.0},
)
conn = altcoin_db.get_conn()
try:
conn.execute(
"""
INSERT INTO latest_price_cache (symbol, price, updated_at, source)
VALUES (%s, %s, %s, %s), (%s, %s, %s, %s)
""",
(
"LONGTOUCH/USDT", 94, "2026-05-16T10:00:00", "unit",
"SHORTTOUCH/USDT", 106, "2026-05-16T10:00:00", "unit",
),
)
conn.execute(
"""
INSERT INTO paper_orders (
recommendation_id, symbol, side, order_type, status, source_status,
source_action, target_price, current_price_at_create, notional_usdt,
stop_loss, tp1, tp2, created_at, updated_at, expires_at
) VALUES
(%s, %s, %s, 'limit', 'pending', 'wait_pullback', '等回踩', %s, %s, %s, %s, %s, %s, %s, %s, %s),
(%s, %s, %s, 'limit', 'pending', 'wait_pullback', '等反抽', %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
long_rec_id, "LONGTOUCH/USDT", "long", 95, 100, 5000, 90, 105, 110, "2026-05-16T09:00:00", "2026-05-16T09:00:00", "2026-05-17T09:00:00",
short_rec_id, "SHORTTOUCH/USDT", "short", 105, 100, 5000, 110, 95, 90, "2026-05-16T09:00:00", "2026-05-16T09:00:00", "2026-05-17T09:00:00",
),
)
conn.commit()
finally:
conn.close()
rows = list_paper_orders(status="pending", limit=10)["items"]
by_symbol = {item["symbol"]: item for item in rows}
assert by_symbol["LONGTOUCH/USDT"]["distance_to_target_pct"] == pytest.approx(0)
assert by_symbol["SHORTTOUCH/USDT"]["distance_to_target_pct"] == pytest.approx(0)
def test_reset_paper_trading_data_all_clears_ledger(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="RESETORD/USDT",
rec_state="蓄力",
rec_score=22,
entry_price=95,
stop_loss=90,
tp1=105,
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": "RESETORD/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
result = reset_paper_trading_data("all")
assert result["reset"] is True
assert result["deleted"]["trades"] == 1
assert result["deleted"]["orders"] == 1
assert result["deleted"]["events"] == 1
assert list_paper_trades()["total"] == 0
assert list_paper_orders()["total"] == 0
assert list_paper_trade_events()["total"] == 0