Skip to content

Commit 1863272

Browse files
authored
Merge pull request #18 from AKKI0511/codex/create-backtester-script-with-metrics
Add simple backtesting utilities
2 parents b205de8 + 36143cb commit 1863272

File tree

3 files changed

+112
-0
lines changed

3 files changed

+112
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,20 @@ python -m src.main evaluate -m models/trained/MY_MODEL # evaluate
137137
the full training pipeline. `evaluate` loads an existing model directory and
138138
generates evaluation metrics.
139139

140+
## Backtesting
141+
142+
Use the backtester to evaluate label-based trading signals:
143+
144+
```python
145+
from backtest.backtester import simulate_trades, compute_metrics
146+
import pandas as pd
147+
148+
df = pd.read_parquet("data/processed/AAPL.parquet")
149+
df = simulate_trades(df)
150+
metrics = compute_metrics(df)
151+
print(metrics)
152+
```
153+
140154
## Competition Evaluation Metrics
141155

142156
### Trade-Level Analytics

src/backtest/backtester.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import pandas as pd
2+
3+
from utils.metrics import sharpe_ratio, max_drawdown
4+
5+
6+
def simulate_trades(df: pd.DataFrame) -> pd.DataFrame:
7+
"""Simulate trades using label signals.
8+
9+
Parameters
10+
----------
11+
df : pd.DataFrame
12+
DataFrame containing ``Close`` prices and a ``label`` column where
13+
1 indicates a long position, -1 a short position and 0 no position.
14+
15+
Returns
16+
-------
17+
pd.DataFrame
18+
Input DataFrame with additional ``strategy_return`` and
19+
``equity_curve`` columns.
20+
"""
21+
data = df.copy()
22+
data["price_return"] = data["Close"].pct_change()
23+
data["strategy_return"] = data["price_return"].shift(-1) * data["label"]
24+
data["strategy_return"] = data["strategy_return"].fillna(0.0)
25+
data["equity_curve"] = (1 + data["strategy_return"]).cumprod()
26+
return data
27+
28+
29+
def compute_metrics(data: pd.DataFrame, risk_free_rate: float = 0.0) -> dict:
30+
"""Calculate basic performance metrics for a strategy.
31+
32+
Parameters
33+
----------
34+
data : pd.DataFrame
35+
Output from :func:`simulate_trades` containing ``strategy_return`` and
36+
``equity_curve`` columns.
37+
risk_free_rate : float, optional
38+
Annual risk free rate used in Sharpe ratio, by default 0.0.
39+
40+
Returns
41+
-------
42+
dict
43+
Dictionary with ``cumulative_return``, ``sharpe_ratio`` and
44+
``max_drawdown`` keys.
45+
"""
46+
returns = data["strategy_return"]
47+
equity = data["equity_curve"]
48+
cumulative_return = equity.iloc[-1] - 1
49+
sharpe = sharpe_ratio(returns, risk_free_rate)
50+
mdd = max_drawdown(equity)
51+
return {
52+
"cumulative_return": cumulative_return,
53+
"sharpe_ratio": sharpe,
54+
"max_drawdown": mdd,
55+
}

tests/backtest/test_backtester.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import unittest
2+
import os
3+
import sys
4+
import pandas as pd
5+
6+
# add src to path
7+
sys.path.insert(
8+
0,
9+
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "src")),
10+
)
11+
12+
from backtest.backtester import simulate_trades, compute_metrics # noqa: E402
13+
14+
15+
class TestBacktester(unittest.TestCase):
16+
def setUp(self):
17+
close = [10, 11, 10, 12, 11]
18+
labels = [1, 0, -1, 1, -1]
19+
self.df = pd.DataFrame({"Close": close, "label": labels})
20+
21+
def test_simulate_trades(self):
22+
result = simulate_trades(self.df)
23+
expected_returns = pd.Series(
24+
[0.1, 0.0, -0.2, -0.083333, 0.0],
25+
name="strategy_return",
26+
)
27+
pd.testing.assert_series_equal(
28+
result["strategy_return"].round(6).reset_index(drop=True),
29+
expected_returns.round(6),
30+
check_names=False,
31+
)
32+
self.assertIn("equity_curve", result.columns)
33+
34+
def test_compute_metrics(self):
35+
result = simulate_trades(self.df)
36+
metrics = compute_metrics(result)
37+
self.assertAlmostEqual(metrics["cumulative_return"], -0.193333, places=6)
38+
self.assertIn("sharpe_ratio", metrics)
39+
self.assertIn("max_drawdown", metrics)
40+
41+
42+
if __name__ == "__main__":
43+
unittest.main()

0 commit comments

Comments
 (0)