|
|
import pandas as pd |
|
|
import numpy as np |
|
|
from datetime import datetime, timedelta |
|
|
from typing import Dict, List, Optional |
|
|
|
|
|
class PnLTracker: |
|
|
""" |
|
|
Tracks hypothetical P&L for all predictions to demonstrate value. |
|
|
Simulates a portfolio that invests based on model confidence. |
|
|
""" |
|
|
|
|
|
def __init__(self, initial_capital: float = 1_000_000.0): |
|
|
self.initial_capital = initial_capital |
|
|
self.current_capital = initial_capital |
|
|
self.predictions = [] |
|
|
self.positions = [] |
|
|
self.closed_trades = [] |
|
|
self.win_count = 0 |
|
|
self.loss_count = 0 |
|
|
|
|
|
|
|
|
self.total_pnl = 0.0 |
|
|
self.roi_pct = 0.0 |
|
|
|
|
|
def calculate_position_size(self, confidence: float) -> float: |
|
|
""" |
|
|
Kelly Criterion-inspired position sizing based on confidence. |
|
|
Higher confidence = larger bet size. |
|
|
""" |
|
|
if confidence < 0.5: |
|
|
return 0.0 |
|
|
|
|
|
|
|
|
|
|
|
max_position_pct = 0.10 |
|
|
scale_factor = (confidence - 0.5) * 2 |
|
|
|
|
|
return self.current_capital * max_position_pct * scale_factor |
|
|
|
|
|
def record_prediction(self, prediction_id: str, vertical: str, target: str, |
|
|
predicted_value: any, confidence: float, |
|
|
expected_timeline_days: int): |
|
|
""" |
|
|
Log a new prediction and "open" a hypothetical position. |
|
|
""" |
|
|
position_size = self.calculate_position_size(confidence) |
|
|
|
|
|
record = { |
|
|
'id': prediction_id, |
|
|
'date': datetime.now().isoformat(), |
|
|
'vertical': vertical, |
|
|
'target': target, |
|
|
'prediction': predicted_value, |
|
|
'confidence': confidence, |
|
|
'position_size': position_size, |
|
|
'status': 'OPEN', |
|
|
'expected_close_date': (datetime.now() + timedelta(days=expected_timeline_days)).isoformat() |
|
|
} |
|
|
|
|
|
self.predictions.append(record) |
|
|
if position_size > 0: |
|
|
self.positions.append(record) |
|
|
|
|
|
return position_size |
|
|
|
|
|
def resolve_prediction(self, prediction_id: str, actual_value: any, success: bool, pnl_pct: float): |
|
|
""" |
|
|
Close a position based on real-world outcome. |
|
|
pnl_pct: The simulated return on the position (e.g., 0.20 for 20% gain) |
|
|
""" |
|
|
|
|
|
position = next((p for p in self.positions if p['id'] == prediction_id), None) |
|
|
|
|
|
if not position: |
|
|
return |
|
|
|
|
|
|
|
|
invested_amount = position['position_size'] |
|
|
pnl_amount = invested_amount * pnl_pct |
|
|
|
|
|
self.current_capital += pnl_amount |
|
|
self.total_pnl += pnl_amount |
|
|
self.roi_pct = (self.current_capital - self.initial_capital) / self.initial_capital * 100 |
|
|
|
|
|
if success: |
|
|
self.win_count += 1 |
|
|
else: |
|
|
self.loss_count += 1 |
|
|
|
|
|
|
|
|
position['status'] = 'CLOSED' |
|
|
position['actual_value'] = actual_value |
|
|
position['pnl_amount'] = pnl_amount |
|
|
position['pnl_pct'] = pnl_pct |
|
|
position['close_date'] = datetime.now().isoformat() |
|
|
|
|
|
self.closed_trades.append(position) |
|
|
self.positions.remove(position) |
|
|
|
|
|
def get_performance_metrics(self) -> Dict: |
|
|
""" |
|
|
Return comprehensive performance stats for the dashboard. |
|
|
""" |
|
|
total_trades = self.win_count + self.loss_count |
|
|
win_rate = (self.win_count / total_trades * 100) if total_trades > 0 else 0.0 |
|
|
|
|
|
return { |
|
|
'current_capital': self.current_capital, |
|
|
'total_pnl': self.total_pnl, |
|
|
'roi_pct': round(self.roi_pct, 2), |
|
|
'win_rate': round(win_rate, 1), |
|
|
'total_trades': total_trades, |
|
|
'active_positions': len(self.positions), |
|
|
'avg_win_pct': self._calculate_avg_pnl(wins_only=True), |
|
|
'avg_loss_pct': self._calculate_avg_pnl(losses_only=True) |
|
|
} |
|
|
|
|
|
def _calculate_avg_pnl(self, wins_only=False, losses_only=False) -> float: |
|
|
trades = self.closed_trades |
|
|
if wins_only: |
|
|
trades = [t for t in trades if t['pnl_amount'] > 0] |
|
|
if losses_only: |
|
|
trades = [t for t in trades if t['pnl_amount'] <= 0] |
|
|
|
|
|
if not trades: |
|
|
return 0.0 |
|
|
|
|
|
avg = sum(t['pnl_pct'] for t in trades) / len(trades) * 100 |
|
|
return round(avg, 1) |
|
|
|