complete paper trading: auto-close on stop/target + My Trades realized record
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user