alphax/tests/test_paper_trading.py
2026-05-21 08:42:08 +08:00

643 lines
27 KiB
Python

import json
import pytest
from app.db import altcoin_db
from app.db.paper_trading import (
get_paper_trading_summary,
list_paper_orders,
list_paper_trade_events,
list_paper_trades,
send_paper_trading_report,
sync_recommendation,
)
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
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=95,
tp1=106,
tp2=112,
signals=["当前15min即刻入场信号"],
entry_plan={
"entry_action": "可即刻买入",
"entry_price": 100,
"stop_loss": 95,
"tp1": 106,
"tp2": 112,
"risk_reward_ok": True,
"rr1": 1.2,
"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_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)
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_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
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_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},
)
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}}
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_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},
)
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}}
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},
)
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}}
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},
)
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}}
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},
)
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}}
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_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},
)
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}}
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_paper_trading_trailing_stop_activates_moves_and_closes(monkeypatch, buy_now_rec):
monkeypatch.setenv("ALPHAX_PAPER_TRAILING_STOP_ENABLED", "1")
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")
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"]
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_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_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_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