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:
@@ -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
|
||||
Reference in New Issue
Block a user