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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ __pycache__/
*.py[cod]
*$py.class
src/quant_research_starter.egg-info/PKG-INFO

myenv/
58 changes: 43 additions & 15 deletions src/quant_research_starter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import click
import matplotlib.pyplot as plt
import pandas as pd
from tqdm import tqdm

from .backtest import VectorizedBacktest
from .data import SampleDataLoader, SyntheticDataGenerator
Expand All @@ -30,9 +31,14 @@ def generate_data(output, symbols, days):
click.echo("Generating synthetic price data...")

generator = SyntheticDataGenerator()
prices = generator.generate_price_data(
n_symbols=symbols, days=days, start_date="2020-01-01"
)
all_prices = []
for _ in tqdm(range(symbols), desc="Generating price series"):
prices = generator.generate_price_data(
n_symbols=1, days=days, start_date="2020-01-01"
)
all_prices.append(prices)

prices = pd.concat(all_prices, axis=1)

# Ensure output directory exists
output_path = Path(output)
Expand Down Expand Up @@ -94,8 +100,13 @@ def compute_factors(data_file, factors, output):
vol = VolatilityFactor(lookback=21)
factor_data["volatility"] = vol.compute(prices)

# Combine factors (simple average for demo)
combined_signals = pd.DataFrame({k: v.mean(axis=1) for k, v in factor_data.items()})
combined_signals = pd.DataFrame(
{
k: tqdm(v.mean(axis=1), desc=f"Averaging {k} factor")
for k, v in factor_data.items()
}
)

combined_signals["composite"] = combined_signals.mean(axis=1)

# Save results
Expand Down Expand Up @@ -148,7 +159,6 @@ def backtest(data_file, signals_file, initial_capital, output, plot, plotly):
# Load signals
if Path(signals_file).exists():
signals_data = pd.read_csv(signals_file, index_col=0, parse_dates=True)
# Use composite signal if available, otherwise first column
if "composite" in signals_data.columns:
signals = signals_data["composite"]
else:
Expand All @@ -158,27 +168,48 @@ def backtest(data_file, signals_file, initial_capital, output, plot, plotly):
momentum = MomentumFactor(lookback=63)
signals = momentum.compute(prices).mean(axis=1)

# Ensure signals align with prices
# Align dates
common_dates = prices.index.intersection(signals.index)
prices = prices.loc[common_dates]
signals = signals.loc[common_dates]

# Expand signals to all symbols (simplified - same signal for all)
# Expand signals across symbols
signal_matrix = pd.DataFrame(
dict.fromkeys(prices.columns, signals), index=signals.index
)

# Run backtest
def run_with_progress(self, weight_scheme="rank"):
returns = []
idx = self.prices.index

for i in tqdm(range(1, len(idx)), desc="Running backtest"):
ret = self._compute_daily_return(
self.prices.iloc[i - 1],
self.prices.iloc[i],
weight_scheme,
)
returns.append(ret)

results = pd.DataFrame({"returns": returns}, index=idx[1:])
results["portfolio_value"] = (
self.initial_capital * (1 + results["returns"]).cumprod()
)
results["final_value"] = results["portfolio_value"].iloc[-1]
results["total_return"] = results["final_value"] / self.initial_capital - 1

return results

VectorizedBacktest.run = run_with_progress

backtest = VectorizedBacktest(
prices=prices,
signals=signal_matrix,
initial_capital=initial_capital,
transaction_cost=0.001,
)

results = backtest.run(weight_scheme="rank")

# Calculate metrics
# Metrics
metrics_calc = RiskMetrics(results["returns"])
metrics = metrics_calc.calculate_all()

Expand All @@ -195,18 +226,16 @@ def backtest(data_file, signals_file, initial_capital, output, plot, plotly):
with open(output_path, "w") as f:
json.dump(results_dict, f, indent=2)

# Generate plot
# Plotting
if plot:
plt.figure(figsize=(12, 8))

# Plot portfolio value
plt.subplot(2, 1, 1)
plt.plot(results["portfolio_value"].index, results["portfolio_value"].values)
plt.title("Portfolio Value")
plt.ylabel("USD")
plt.grid(True)

# Plot returns
plt.subplot(2, 1, 2)
plt.bar(results["returns"].index, results["returns"].values, alpha=0.7)
plt.title("Daily Returns")
Expand All @@ -220,7 +249,6 @@ def backtest(data_file, signals_file, initial_capital, output, plot, plotly):

click.echo(f"Plot saved -> {plot_path}")

# Generate Plotly HTML chart if requested
if plotly:
html_path = output_path.parent / "backtest_plot.html"

Expand Down
Loading