4ffde8cc06
- Database migration 018 with 13 tables for trading engine state - Trading engine service (services/trading/) with 12 pure computation modules: position sizer, stop-loss manager, reserve pool, circuit breaker, risk tier controller, correlation matrix, tax lots, trading window, gradual entry, notifications, micro-trading, backtester - Core TradingEngine with pre-trade evaluation pipeline and integration wiring - FastAPI HTTP service with 14 endpoints (health, config, decisions, metrics, backtest) - Performance tracker with Sharpe ratio, drawdown, profit factor computation - 194 Python tests (165 property-based + 29 integration) - Frontend: 13 TanStack Query hooks, 7 dashboard panels, tabbed Trading Engine page - Helm chart entry, network policy, nginx proxy, ingress for trading-engine - Shared infrastructure: enums, Redis keys, TradingConfig in AppConfig
133 lines
3.5 KiB
Python
133 lines
3.5 KiB
Python
"""Tax lot tracking for cost basis and wash sale detection.
|
|
|
|
Feature: autonomous-trading-engine
|
|
|
|
Pure computation module for FIFO tax lot closing and wash sale
|
|
detection within the 30-day window. Used by the Trading Engine
|
|
for tax-loss harvesting awareness.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import date, timedelta
|
|
|
|
|
|
@dataclass
|
|
class TaxLot:
|
|
"""A single tax lot representing a purchase of shares."""
|
|
|
|
ticker: str
|
|
quantity: int
|
|
cost_basis_per_share: float
|
|
acquisition_date: date
|
|
status: str = "open" # open | closed | washed
|
|
|
|
|
|
@dataclass
|
|
class ClosedLot:
|
|
"""Result of closing a tax lot via FIFO."""
|
|
|
|
ticker: str
|
|
quantity: int
|
|
cost_basis_per_share: float
|
|
exit_price: float
|
|
realized_pnl: float
|
|
acquisition_date: date
|
|
closed_date: date
|
|
|
|
|
|
class TaxLotTracker:
|
|
"""Pure computation for FIFO lot closing and wash sale detection."""
|
|
|
|
def close_lots_fifo(
|
|
self,
|
|
lots: list[TaxLot],
|
|
quantity: int,
|
|
exit_price: float,
|
|
exit_date: date,
|
|
) -> list[ClosedLot]:
|
|
"""Close lots in FIFO order (earliest acquired first).
|
|
|
|
Processes open lots sorted by acquisition_date ascending,
|
|
closing shares until the requested quantity is fulfilled.
|
|
Returns a list of ClosedLot records with realized P&L.
|
|
|
|
Parameters
|
|
----------
|
|
lots:
|
|
All tax lots for the ticker (open ones will be selected).
|
|
quantity:
|
|
Number of shares to close.
|
|
exit_price:
|
|
Price per share at exit.
|
|
exit_date:
|
|
Date the lots are being closed.
|
|
|
|
Returns
|
|
-------
|
|
list[ClosedLot]
|
|
Closed lot records in FIFO order.
|
|
"""
|
|
open_lots = sorted(
|
|
[lot for lot in lots if lot.status == "open"],
|
|
key=lambda lot: lot.acquisition_date,
|
|
)
|
|
|
|
closed: list[ClosedLot] = []
|
|
remaining = quantity
|
|
|
|
for lot in open_lots:
|
|
if remaining <= 0:
|
|
break
|
|
|
|
close_qty = min(lot.quantity, remaining)
|
|
realized_pnl = (exit_price - lot.cost_basis_per_share) * close_qty
|
|
|
|
closed.append(
|
|
ClosedLot(
|
|
ticker=lot.ticker,
|
|
quantity=close_qty,
|
|
cost_basis_per_share=lot.cost_basis_per_share,
|
|
exit_price=exit_price,
|
|
realized_pnl=realized_pnl,
|
|
acquisition_date=lot.acquisition_date,
|
|
closed_date=exit_date,
|
|
)
|
|
)
|
|
|
|
remaining -= close_qty
|
|
|
|
return closed
|
|
|
|
def check_wash_sale(
|
|
self,
|
|
loss_date: date,
|
|
purchases: list[TaxLot],
|
|
) -> bool:
|
|
"""Check whether any purchase falls within the 30-day wash sale window.
|
|
|
|
A wash sale occurs when the same ticker is purchased within
|
|
30 days before or after a loss-closing date.
|
|
|
|
Parameters
|
|
----------
|
|
loss_date:
|
|
The date the loss was realized.
|
|
purchases:
|
|
Tax lots representing purchases of the same ticker.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
True if any purchase is within the 30-day window.
|
|
"""
|
|
window_start = loss_date - timedelta(days=30)
|
|
window_end = loss_date + timedelta(days=30)
|
|
|
|
for lot in purchases:
|
|
if window_start <= lot.acquisition_date <= window_end:
|
|
return True
|
|
|
|
return False
|