feat: strategie 1-10, framework analisi frattale, download dati storici BTC/ETH
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
"""Backtesting engine with fee support and performance metrics."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class Side(Enum):
|
||||
LONG = 1
|
||||
SHORT = -1
|
||||
|
||||
|
||||
@dataclass
|
||||
class Trade:
|
||||
entry_idx: int
|
||||
exit_idx: int
|
||||
side: Side
|
||||
entry_price: float
|
||||
exit_price: float
|
||||
size: float
|
||||
fee_pct: float
|
||||
|
||||
@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 fee(self) -> float:
|
||||
return self.fee_pct * (self.entry_price + self.exit_price) * self.size
|
||||
|
||||
@property
|
||||
def net_pnl(self) -> float:
|
||||
return self.gross_pnl - self.fee
|
||||
|
||||
@property
|
||||
def net_return(self) -> float:
|
||||
cost = self.entry_price * self.size + self.fee_pct * self.entry_price * self.size
|
||||
if cost == 0:
|
||||
return 0.0
|
||||
return self.net_pnl / cost
|
||||
|
||||
|
||||
@dataclass
|
||||
class BacktestResult:
|
||||
trades: list[Trade]
|
||||
initial_capital: float
|
||||
final_capital: float
|
||||
equity_curve: list[float]
|
||||
|
||||
@property
|
||||
def total_trades(self) -> int:
|
||||
return len(self.trades)
|
||||
|
||||
@property
|
||||
def win_rate(self) -> float:
|
||||
if not self.trades:
|
||||
return 0.0
|
||||
wins = sum(1 for t in self.trades if t.net_pnl > 0)
|
||||
return wins / len(self.trades)
|
||||
|
||||
@property
|
||||
def total_return(self) -> float:
|
||||
if self.initial_capital == 0:
|
||||
return 0.0
|
||||
return (self.final_capital - self.initial_capital) / self.initial_capital
|
||||
|
||||
@property
|
||||
def annualized_return(self) -> float:
|
||||
if not self.trades or self.total_return <= -1:
|
||||
return -1.0
|
||||
days = (self.trades[-1].exit_idx - self.trades[0].entry_idx) / 24
|
||||
if days <= 0:
|
||||
return 0.0
|
||||
years = days / 365.25
|
||||
if years == 0:
|
||||
return 0.0
|
||||
return (1 + self.total_return) ** (1 / years) - 1
|
||||
|
||||
@property
|
||||
def max_drawdown(self) -> float:
|
||||
if not self.equity_curve:
|
||||
return 0.0
|
||||
peak = self.equity_curve[0]
|
||||
max_dd = 0.0
|
||||
for val in self.equity_curve:
|
||||
if val > peak:
|
||||
peak = val
|
||||
dd = (peak - val) / peak if peak > 0 else 0
|
||||
if dd > max_dd:
|
||||
max_dd = dd
|
||||
return max_dd
|
||||
|
||||
@property
|
||||
def sharpe_ratio(self) -> float:
|
||||
if len(self.equity_curve) < 2:
|
||||
return 0.0
|
||||
eq = np.array(self.equity_curve)
|
||||
returns = np.diff(eq) / eq[:-1]
|
||||
returns = returns[np.isfinite(returns)]
|
||||
if len(returns) == 0 or np.std(returns) == 0:
|
||||
return 0.0
|
||||
return float(np.mean(returns) / np.std(returns) * np.sqrt(252 * 24))
|
||||
|
||||
@property
|
||||
def profit_factor(self) -> float:
|
||||
gross_wins = sum(t.net_pnl for t in self.trades if t.net_pnl > 0)
|
||||
gross_losses = abs(sum(t.net_pnl for t in self.trades if t.net_pnl < 0))
|
||||
if gross_losses == 0:
|
||||
return float("inf") if gross_wins > 0 else 0.0
|
||||
return gross_wins / gross_losses
|
||||
|
||||
def summary(self) -> dict:
|
||||
return {
|
||||
"total_trades": self.total_trades,
|
||||
"win_rate": round(self.win_rate * 100, 1),
|
||||
"total_return_pct": round(self.total_return * 100, 1),
|
||||
"annualized_return_pct": round(self.annualized_return * 100, 1),
|
||||
"max_drawdown_pct": round(self.max_drawdown * 100, 1),
|
||||
"sharpe_ratio": round(self.sharpe_ratio, 2),
|
||||
"profit_factor": round(self.profit_factor, 2),
|
||||
"initial_capital": self.initial_capital,
|
||||
"final_capital": round(self.final_capital, 2),
|
||||
}
|
||||
|
||||
|
||||
def run_backtest(
|
||||
df: pd.DataFrame,
|
||||
signals: pd.Series,
|
||||
initial_capital: float = 1000.0,
|
||||
fee_pct: float = 0.001,
|
||||
position_size_pct: float = 1.0,
|
||||
max_hold_candles: int = 24,
|
||||
) -> BacktestResult:
|
||||
"""Run backtest on signals.
|
||||
|
||||
signals: Series with same index as df.
|
||||
+1 = go long, -1 = go short, 0 = no signal
|
||||
"""
|
||||
capital = initial_capital
|
||||
trades: list[Trade] = []
|
||||
equity_curve: list[float] = [capital]
|
||||
in_position = False
|
||||
entry_idx = 0
|
||||
entry_price = 0.0
|
||||
current_side = Side.LONG
|
||||
size = 0.0
|
||||
|
||||
for i in range(len(df)):
|
||||
sig = signals.iloc[i] if i < len(signals) else 0
|
||||
|
||||
if in_position:
|
||||
hold_time = i - entry_idx
|
||||
exit_price = df["close"].iloc[i]
|
||||
should_exit = (
|
||||
hold_time >= max_hold_candles
|
||||
or (current_side == Side.LONG and sig == -1)
|
||||
or (current_side == Side.SHORT and sig == 1)
|
||||
)
|
||||
|
||||
if should_exit:
|
||||
trade = Trade(
|
||||
entry_idx=entry_idx,
|
||||
exit_idx=i,
|
||||
side=current_side,
|
||||
entry_price=entry_price,
|
||||
exit_price=exit_price,
|
||||
size=size,
|
||||
fee_pct=fee_pct,
|
||||
)
|
||||
capital += trade.net_pnl
|
||||
trades.append(trade)
|
||||
in_position = False
|
||||
|
||||
if not in_position and sig != 0 and capital > 0:
|
||||
entry_idx = i
|
||||
entry_price = df["close"].iloc[i]
|
||||
current_side = Side.LONG if sig > 0 else Side.SHORT
|
||||
alloc = capital * position_size_pct
|
||||
size = alloc / entry_price
|
||||
in_position = True
|
||||
|
||||
equity_curve.append(capital)
|
||||
|
||||
if in_position:
|
||||
exit_price = df["close"].iloc[-1]
|
||||
trade = Trade(
|
||||
entry_idx=entry_idx,
|
||||
exit_idx=len(df) - 1,
|
||||
side=current_side,
|
||||
entry_price=entry_price,
|
||||
exit_price=exit_price,
|
||||
size=size,
|
||||
fee_pct=fee_pct,
|
||||
)
|
||||
capital += trade.net_pnl
|
||||
trades.append(trade)
|
||||
equity_curve.append(capital)
|
||||
|
||||
return BacktestResult(
|
||||
trades=trades,
|
||||
initial_capital=initial_capital,
|
||||
final_capital=capital,
|
||||
equity_curve=equity_curve,
|
||||
)
|
||||
Reference in New Issue
Block a user