add paper trading: mark a setup as taken, track open P&L, sell
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 24s

New paper_trades table (migration 007) + service/router. "Mark as taken" on each
setup card (shares prefilled from position sizing, entry from current price, both
editable) records a simulated trade. Overview gains an Open Trades table that
marks each position to the latest close — P&L in $, %, and R-multiples — with a
total unrealized P&L footer and a Sell button to close at the current price.
Closed trades are retained for future realized-P&L reporting.

Deploy: alembic upgrade (new paper_trades table).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 06:33:56 +02:00
parent 050abc6f71
commit a69557f5d8
16 changed files with 736 additions and 1 deletions
+83
View File
@@ -0,0 +1,83 @@
"""Tests for the paper-trading service."""
from __future__ import annotations
from datetime import date, datetime, timezone
import pytest
from app.exceptions import ValidationError
from app.models.ohlcv import OHLCVRecord
from app.models.ticker import Ticker
from app.models.user import User
from app.services import paper_trade_service as svc
from tests.conftest import _test_session_factory # type: ignore
@pytest.fixture
async def session():
async with _test_session_factory() as s:
yield s
async def _seed(session, symbol: str, close: float) -> int:
user = await session.get(User, 1)
if user is None:
session.add(User(id=1, username="u", password_hash="x", role="user", has_access=True))
await session.flush()
t = Ticker(symbol=symbol)
session.add(t)
await session.flush()
session.add(OHLCVRecord(ticker_id=t.id, date=date.today(),
open=close, high=close, low=close, close=close, volume=1))
await session.commit()
return t.id
async def test_create_and_list_open(session):
await _seed(session, "AAA", close=110.0)
await svc.create_trade(session, 1, symbol="AAA", direction="long",
entry_price=100.0, shares=10, stop_loss=95.0, target=120.0)
rows = await svc.list_trades(session, 1)
assert len(rows) == 1
row = rows[0]
assert row["symbol"] == "AAA"
assert row["status"] == "open"
assert row["current_price"] == 110.0 # marked to the latest close
async def test_close_uses_current_price(session):
await _seed(session, "AAA", close=112.0)
trade = await svc.create_trade(session, 1, symbol="AAA", direction="long",
entry_price=100.0, shares=5, stop_loss=95.0, target=120.0)
closed = await svc.close_trade(session, 1, trade.id)
assert closed.status == "closed"
assert closed.close_price == 112.0
assert closed.closed_at is not None
rows = await svc.list_trades(session, 1, status="closed")
assert rows[0]["current_price"] == 112.0 # closed → realized exit
async def test_close_with_explicit_price(session):
await _seed(session, "AAA", close=112.0)
trade = await svc.create_trade(session, 1, symbol="AAA", direction="short",
entry_price=100.0, shares=5, stop_loss=105.0, target=90.0)
closed = await svc.close_trade(session, 1, trade.id, close_price=93.0)
assert closed.close_price == 93.0
async def test_invalid_direction_rejected(session):
await _seed(session, "AAA", close=100.0)
with pytest.raises(ValidationError):
await svc.create_trade(session, 1, symbol="AAA", direction="sideways",
entry_price=100.0, shares=1, stop_loss=95.0, target=110.0)
async def test_double_close_rejected(session):
await _seed(session, "AAA", close=100.0)
trade = await svc.create_trade(session, 1, symbol="AAA", direction="long",
entry_price=100.0, shares=1, stop_loss=95.0, target=110.0)
await svc.close_trade(session, 1, trade.id)
with pytest.raises(ValidationError):
await svc.close_trade(session, 1, trade.id)