import json import pytest from app.db import altcoin_db from app.db.paper_trading import get_paper_trading_summary, list_paper_trades, sync_recommendation @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_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_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