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:
2026-05-27 00:55:13 +02:00
parent 49ee092c7f
commit 988739b2f5
29 changed files with 3300 additions and 0 deletions
View File
+209
View File
@@ -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,
)