Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 52 additions & 20 deletions notebooks/evaluate.ipynb

Large diffs are not rendered by default.

53 changes: 32 additions & 21 deletions notebooks/experiments/2.ipynb

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions scripts/test_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def test_strategy(strategy_path: str, return_results: bool = False):
print(f"Max Drawdown: {dev_results['max_drawdown']*100:.2f}%")
print(f"Number of Trades: {dev_results['n_trades']}")
print(f"Win Rate: {dev_results['win_rate']*100:.2f}%")
print(f"R^2: {dev_results['r_squared']:.2f}")

# Run backtest on holdout period
if not return_results:
Expand All @@ -103,6 +104,7 @@ def test_strategy(strategy_path: str, return_results: bool = False):
print(f"Max Drawdown: {holdout_results['max_drawdown']*100:.2f}%")
print(f"Number of Trades: {holdout_results['n_trades']}")
print(f"Win Rate: {holdout_results['win_rate']*100:.2f}%")
print(f"R^2: {holdout_results['r_squared']:.2f}")

if return_results:
return dev_results, holdout_results
Expand Down
26 changes: 16 additions & 10 deletions src/core/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

from .metrics import (
sharpe_ratio, total_return, n_trades, win_rate, max_drawdown, annualized_return,
rolling_sharpe_ratio, unrealized_drawdown_series, realized_drawdown_series
rolling_sharpe_ratio, unrealized_drawdown_series, realized_drawdown_series, r_squared_equity_curve
)

def run_backtest(strategy_class, df: pd.DataFrame, initial_capital: float = 10000,
start_date: Optional[str] = None, end_date: Optional[str] = None):
start_date: Optional[str] = None, end_date: Optional[str] = None,
slippage: float = 0.003):
"""
Run a backtest for a trading strategy.

Expand Down Expand Up @@ -69,12 +70,13 @@ def run_backtest(strategy_class, df: pd.DataFrame, initial_capital: float = 1000
# Process each bar
for i in range(len(df)):
if entries.iloc[i]:
# Enter position
# Enter position with slippage (buy price is higher)
current_position = 1
current_coins = equity_curve.iloc[i-1] / df['close'].iloc[i]
entry_price = df['close'].iloc[i] * (1 + slippage) # Apply slippage to buy price
current_coins = equity_curve.iloc[i-1] / entry_price
coins.iloc[i:] = current_coins
elif exits.iloc[i]:
# Exit position
# Exit position with slippage (sell price is lower)
current_position = 0
current_coins = 0
coins.iloc[i:] = 0
Expand All @@ -93,9 +95,9 @@ def run_backtest(strategy_class, df: pd.DataFrame, initial_capital: float = 1000
for i in range(len(df)):
if entries.iloc[i]:
entry_idx = i
entry_price = df['close'].iloc[i]
entry_price = df['close'].iloc[i] * (1 + slippage) # Apply slippage to buy price
elif exits.iloc[i] and entry_idx is not None:
exit_price = df['close'].iloc[i]
exit_price = df['close'].iloc[i] * (1 - slippage) # Apply slippage to sell price
pnl = (exit_price - entry_price) * coins.iloc[entry_idx]

trades.append({
Expand All @@ -115,9 +117,9 @@ def run_backtest(strategy_class, df: pd.DataFrame, initial_capital: float = 1000
if entry_idx is None:
# Find the last entry point if we're in position but don't have an entry record
entry_idx = entries[entries].index[-1] if any(entries) else 0
entry_price = df['close'].iloc[entry_idx]
entry_price = df['close'].iloc[entry_idx] * (1 + slippage) # Apply slippage to buy price

exit_price = df['close'].iloc[-1]
exit_price = df['close'].iloc[-1] * (1 - slippage) # Apply slippage to sell price
pnl = (exit_price - entry_price) * coins.iloc[entry_idx]

trades.append({
Expand All @@ -137,6 +139,7 @@ def run_backtest(strategy_class, df: pd.DataFrame, initial_capital: float = 1000
returns = equity_curve.pct_change().fillna(0).values

# Metrics
r_squared, slope, intercept = r_squared_equity_curve(equity_curve)
results = {
'sharpe': sharpe_ratio(returns),
'total_return': total_return(equity_curve),
Expand All @@ -149,5 +152,8 @@ def run_backtest(strategy_class, df: pd.DataFrame, initial_capital: float = 1000
'rolling_sharpe': rolling_sharpe_ratio(equity_curve),
'unrealized_drawdown': unrealized_drawdown_series(equity_curve),
'realized_drawdown': realized_drawdown_series(equity_curve, trades),
'r_squared': r_squared,
'slope': slope,
'intercept': intercept,
}
return results
return results
28 changes: 28 additions & 0 deletions src/core/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,34 @@ def annualized_return(equity_curve, periods_per_year=8760):
ann_ret = total_ret ** (periods_per_year / n_periods) - 1
return ann_ret

def r_squared_equity_curve(equity_curve):
"""
Calculate R-squared of equity curve against a linear regression line.
Higher values indicate more consistent returns (closer to a straight line).
Returns a tuple of (r_squared, slope, intercept) where r_squared is between 0 and 1.
"""
if len(equity_curve) <= 2:
return 0.0, 0.0, 0.0

# Create x values (time indices)
x = np.arange(len(equity_curve))
y = np.array(equity_curve)

# Calculate linear regression
slope, intercept = np.polyfit(x, y, 1)
y_pred = slope * x + intercept

# Calculate R-squared
ss_total = np.sum((y - np.mean(y))**2)
ss_residual = np.sum((y - y_pred)**2)

if ss_total == 0:
return 0.0, slope, intercept

r_squared = 1 - (ss_residual / ss_total)
return r_squared, slope, intercept


def rolling_sharpe_ratio(equity_curve, window=720, periods_per_year=8760):
"""
Calculate the rolling Sharpe ratio (annualized) on log returns.
Expand Down