feat(backtest): Order/Position/Trade dataclasses with fees

Side StrEnum (long/short/flat), frozen dataclasses con calcolo
unrealized_pnl per Position e gross/fees/net_pnl per Trade
(fees in basis point, default 5bp). 4 test TDD passing,
ruff + mypy strict clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 19:11:27 +02:00
parent d30f981421
commit 36e05233d0
3 changed files with 97 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
class Side(StrEnum):
LONG = "long"
SHORT = "short"
FLAT = "flat"
@dataclass(frozen=True)
class Order:
ts: datetime
side: Side
size: float
@dataclass(frozen=True)
class Position:
side: Side
entry_price: float
size: float
def unrealized_pnl(self, current_price: float) -> float:
if self.side == Side.LONG:
return (current_price - self.entry_price) * self.size
if self.side == Side.SHORT:
return (self.entry_price - current_price) * self.size
return 0.0
@dataclass(frozen=True)
class Trade:
entry_ts: datetime
exit_ts: datetime
side: Side
size: float
entry_price: float
exit_price: float
fees_bp: float = 5.0
@property
def gross_pnl(self) -> float:
if self.side == Side.LONG:
return (self.exit_price - self.entry_price) * self.size
return (self.entry_price - self.exit_price) * self.size
@property
def fees(self) -> float:
notional_in = self.entry_price * self.size
notional_out = self.exit_price * self.size
return (self.fees_bp / 10000.0) * (notional_in + notional_out)
@property
def net_pnl(self) -> float:
return self.gross_pnl - self.fees