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}, ) 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}} 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}, ) rec = {"id": rec_id, "symbol": "PUSHORD/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "entry_plan": {"entry_action": "等回踩", "entry_price": 95}} 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_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 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_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