Skip to content

Commit c9f2942

Browse files
committed
added new portfolio alternative
1 parent c88d0b1 commit c9f2942

5 files changed

Lines changed: 576 additions & 230 deletions

File tree

scripts/backtest.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import logging
2+
from datetime import datetime, timedelta
3+
from typing import Callable, Dict, Any
4+
import pandas as pd
5+
6+
from .portfolio import Portfolio
7+
from .risk_manager import generate_signals
8+
from . import database
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
def run_backtest(
14+
strategy_fn: Callable,
15+
start_date: str,
16+
end_date: str,
17+
initial_cash: float = 10000.0,
18+
) -> pd.DataFrame:
19+
"""
20+
Backtest semplificato:
21+
- Genera segnali ogni venerdì
22+
- Esegue trade il lunedì successivo
23+
- Tiene traccia del valore del portfolio
24+
- Restituisce DataFrame con timeline
25+
26+
Parametri
27+
---------
28+
strategy_fn : Callable
29+
Funzione di strategia, legge i parametri da config.
30+
start_date : str
31+
end_date : str
32+
initial_cash : float
33+
34+
Ritorna
35+
-------
36+
pd.DataFrame
37+
Colonne: [date, cash, positions_value, total_value, trades]
38+
"""
39+
# Setup
40+
portfolio = Portfolio("backtest", start_date, initial_cash, backtest=True)
41+
42+
# Carica tutto il dataset una sola volta
43+
df = database.load_price_history(start_date, end_date)
44+
45+
analysis_dates, execution_dates = _generate_calendar(start_date, end_date)
46+
records = []
47+
48+
for analysis_date, execution_date in zip(analysis_dates, execution_dates):
49+
# Slice dati fino alla data di analisi
50+
df_analysis = df[df["date"] <= analysis_date]
51+
52+
# 1. Aggiorna portfolio alla data di analisi
53+
portfolio.update(analysis_date, df_analysis)
54+
55+
# 2. Genera segnali
56+
signals = generate_signals(strategy_fn, df_analysis, analysis_date, portfolio)
57+
58+
# Slice dati fino alla data di esecuzione
59+
df_exec = df[df["date"] <= execution_date]
60+
61+
# 3. Aggiorna portfolio alla data di esecuzione
62+
portfolio.update(execution_date, df_exec)
63+
64+
# 4. Esegui trade lunedì
65+
trades = portfolio.execute_trades(signals, execution_date)
66+
67+
# 5. Registra snapshot
68+
records.append({
69+
"date": execution_date,
70+
"cash": portfolio.cash,
71+
"positions_value": portfolio.positions_value,
72+
"total_value": portfolio.value(),
73+
"trades": trades
74+
})
75+
76+
return pd.DataFrame(records)
77+
78+
79+
def _generate_calendar(start_date: str, end_date: str):
80+
"""Genera coppie (venerdì, lunedì successivo)."""
81+
start = datetime.strptime(start_date, "%Y-%m-%d")
82+
end = datetime.strptime(end_date, "%Y-%m-%d")
83+
84+
# primo venerdì
85+
while start.weekday() != 4: # 4 = Friday
86+
start += timedelta(days=1)
87+
88+
analysis_dates, execution_dates = [], []
89+
current = start
90+
while current <= end:
91+
analysis_dates.append(current.strftime("%Y-%m-%d"))
92+
monday = current + timedelta(days=3)
93+
execution_dates.append(monday.strftime("%Y-%m-%d"))
94+
current += timedelta(days=7)
95+
96+
return analysis_dates, execution_dates

scripts/config.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,36 @@
160160
# =========================
161161
DEFAULT_PORTFOLIO_NAME = "default"
162162

163+
163164
# =============================================================================
164-
# BACKTESTING (Future)
165+
# BACKTEST CONFIGURATION
165166
# =============================================================================
166-
# Placeholder for backtesting parameters
167-
BACKTEST_START_DATE = "2023-01-01"
168-
BACKTEST_INITIAL_CAPITAL = 100000 # $100k
169-
BACKTEST_COMMISSION = 0.001 # 0.1% commission per trade
167+
168+
# Backtest execution timing
169+
BACKTEST_EXECUTION_DAY = 4 # 0=Monday, 4=Friday - giorno per analisi segnali
170+
BACKTEST_TRADE_DAY = 0 # 0=Monday - giorno per esecuzione trade
171+
172+
# Backtest data management
173+
BACKTEST_SAVE_DAILY_SNAPSHOTS = True # Salva snapshot ogni giorno
174+
BACKTEST_SAVE_WEEKLY_SNAPSHOTS = True # Salva snapshot settimanali
175+
BACKTEST_CALCULATE_METRICS = True # Calcola metriche performance
176+
177+
# Backtest validation
178+
BACKTEST_MIN_DATA_DAYS = 30 # Minimo giorni dati per iniziare backtest
179+
BACKTEST_SKIP_HOLIDAYS = True # Salta giorni festivi automaticamente
180+
181+
# Backtest logging
182+
BACKTEST_LOG_LEVEL = 'INFO'
183+
BACKTEST_LOG_TRADES = True
184+
BACKTEST_LOG_SIGNALS = False # Può essere verboso
185+
186+
# Backtest default parameters
187+
BACKTEST_DEFAULT_INITIAL_CASH = 10000.0
188+
BACKTEST_DEFAULT_COMMISSION = 0.0 # Commissioni per trade (per ora 0)
189+
190+
# Performance calculation
191+
BACKTEST_RISK_FREE_RATE = 0.02 # Tasso risk-free per Sharpe ratio
192+
BACKTEST_BENCHMARK_RETURN = 0.08 # Return benchmark per confronti
170193

171194

172195
# =============================================================================

scripts/database.py

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,30 +88,104 @@ def get_available_tickers() -> List[str]:
8888
rows = execute_query(query)
8989
return [row[0] for row in rows]
9090

91-
def get_last_close(ticker: str) -> Optional[float]:
91+
def get_last_close(ticker: str, date: str = None) -> Optional[float]:
9292
"""
93-
Ritorna l'ultimo prezzo di chiusura per un ticker dal database UNIVERSE.
93+
Ritorna l'ultimo prezzo di chiusura per un ticker.
9494
9595
Parameters
9696
----------
9797
ticker : str
9898
Il simbolo del titolo (es. "AAPL").
99+
date : str, optional
100+
Data limite (YYYY-MM-DD). Se None, usa l'ultima data disponibile.
99101
100102
Returns
101103
-------
102104
close : float | None
103105
L'ultimo prezzo di chiusura, oppure None se il ticker non esiste.
104106
"""
105-
query = """
106-
SELECT date, close
107-
FROM UNIVERSE
108-
WHERE ticker = %s
109-
ORDER BY date DESC
110-
LIMIT 1
111-
"""
112-
rows, _ = execute_query(query, (ticker,))
107+
if date is None:
108+
query = """
109+
SELECT date, close
110+
FROM UNIVERSE
111+
WHERE ticker = %s
112+
ORDER BY date DESC
113+
LIMIT 1
114+
"""
115+
params = (ticker,)
116+
else:
117+
query = """
118+
SELECT date, close
119+
FROM UNIVERSE
120+
WHERE ticker = %s AND date <= %s
121+
ORDER BY date DESC
122+
LIMIT 1
123+
"""
124+
params = (ticker, date)
125+
126+
rows, _ = execute_query(query, params)
113127
return rows[0][1] if rows else None
114128

129+
130+
def get_business_days_between(start_date: str, end_date: str) -> List[str]:
131+
"""
132+
Ritorna lista di giorni lavorativi tra due date (esclude weekend).
133+
134+
Parameters
135+
----------
136+
start_date : str
137+
Data inizio (YYYY-MM-DD)
138+
end_date : str
139+
Data fine (YYYY-MM-DD)
140+
141+
Returns
142+
-------
143+
List[str]
144+
Lista di date in formato YYYY-MM-DD
145+
"""
146+
from datetime import datetime, timedelta
147+
148+
start = datetime.strptime(start_date, '%Y-%m-%d')
149+
end = datetime.strptime(end_date, '%Y-%m-%d')
150+
151+
business_days = []
152+
current = start
153+
154+
while current <= end:
155+
# 0=Monday, 6=Sunday -> escludiamo 5=Saturday, 6=Sunday
156+
if current.weekday() < 5:
157+
business_days.append(current.strftime('%Y-%m-%d'))
158+
current += timedelta(days=1)
159+
160+
return business_days
161+
162+
def get_available_dates_in_universe(start_date: str = None, end_date: str = None) -> List[str]:
163+
"""
164+
Ritorna le date effettivamente disponibili nel DB universe.
165+
Utile per sincronizzare il backtest con i dati reali.
166+
"""
167+
conditions = []
168+
params = []
169+
170+
if start_date:
171+
conditions.append("date >= %s")
172+
params.append(start_date)
173+
if end_date:
174+
conditions.append("date <= %s")
175+
params.append(end_date)
176+
177+
where_clause = " AND ".join(conditions) if conditions else "TRUE"
178+
179+
query = f"""
180+
SELECT DISTINCT date
181+
FROM universe
182+
WHERE {where_clause}
183+
ORDER BY date
184+
"""
185+
186+
rows, _ = execute_query(query, tuple(params))
187+
return [row[0].strftime('%Y-%m-%d') for row in rows]
188+
115189
def get_universe_data(start_date: Optional[str] = None,
116190
end_date: Optional[str] = None,
117191
tickers: Optional[Union[str, List[str]]] = None) -> pd.DataFrame:

0 commit comments

Comments
 (0)