complete paper trading: auto-close on stop/target + My Trades realized record
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 38s
Deploy / deploy (push) Successful in 25s

resolve_open_trades walks the daily bars after each open trade and closes it at
the target (target hit) or stop (stop/ambiguous), leaving undecided trades open.
Runs nightly inside the outcome evaluator (so it's coordinated with fresh OHLCV)
and on its manual trigger. New "My Trades" section at the top of Signals → Track
Record shows realized hit-rate, expectancy (avg R), total R, total P&L, and a
closed-trades table — your actual results, separate from the theoretical signal
record below it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 08:49:28 +02:00
parent 7e87a15a12
commit fb3b8d18d7
6 changed files with 220 additions and 3 deletions
+56
View File
@@ -11,6 +11,13 @@ from app.exceptions import NotFoundError, ValidationError
from app.models.ohlcv import OHLCVRecord
from app.models.paper_trade import PaperTrade
from app.models.ticker import Ticker
from app.services.outcome_service import (
OUTCOME_AMBIGUOUS,
OUTCOME_STOP_HIT,
OUTCOME_TARGET_HIT,
Bar,
evaluate_setup_against_bars,
)
async def _get_ticker(db: AsyncSession, symbol: str) -> Ticker:
@@ -146,3 +153,52 @@ async def close_trade(
await db.commit()
await db.refresh(trade)
return trade
async def resolve_open_trades(db: AsyncSession) -> int:
"""Auto-close open trades whose stop or target was hit in the daily bars.
Walks the bars after each trade's open (same logic as the outcome evaluator).
Target hit → close at the target; stop (or an ambiguous same-bar touch) →
close at the stop. Trades that have hit neither stay open. Returns the count
closed.
"""
result = await db.execute(select(PaperTrade).where(PaperTrade.status == "open"))
open_trades = list(result.scalars().all())
if not open_trades:
return 0
closed = 0
for trade in open_trades:
bars_result = await db.execute(
select(OHLCVRecord.date, OHLCVRecord.high, OHLCVRecord.low)
.where(
OHLCVRecord.ticker_id == trade.ticker_id,
OHLCVRecord.date > trade.opened_at.date(),
)
.order_by(OHLCVRecord.date.asc())
)
bars = [Bar(date=d, high=h, low=lo) for d, h, lo in bars_result.all()]
if not bars:
continue
# max_bars beyond the data so a still-open trade returns undecided (not "expired").
outcome, outcome_date = evaluate_setup_against_bars(
trade.direction, trade.stop_loss, trade.target, bars, max_bars=len(bars) + 1
)
if outcome == OUTCOME_TARGET_HIT:
trade.close_price = trade.target
elif outcome in (OUTCOME_STOP_HIT, OUTCOME_AMBIGUOUS):
trade.close_price = trade.stop_loss
else:
continue
trade.status = "closed"
trade.closed_at = datetime.combine(
outcome_date, datetime.min.time(), tzinfo=timezone.utc
)
closed += 1
if closed:
await db.commit()
return closed