Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 809f46fe36 | |||
| 413abafe0f | |||
| 5d46c1e32c | |||
| 889f7ba1c3 | |||
| 1515b2d077 | |||
| b4ae3e715d | |||
| 6f845d32c6 | |||
| a04e8878fb |
+1
-2
@@ -1,11 +1,10 @@
|
||||
# SpecStory explanation file
|
||||
__pycache__/
|
||||
__OLD__/
|
||||
.specstory/
|
||||
.history/
|
||||
.cursorindexingignore
|
||||
data
|
||||
.vscode/
|
||||
####.vscode/
|
||||
cvttpy
|
||||
# SpecStory explanation file
|
||||
.specstory/.what-is-this.md
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"security_type": "EQUITY",
|
||||
"data_directory": "./data/equity",
|
||||
"datafiles": [
|
||||
"20250618.mktdata.ohlcv.db",
|
||||
],
|
||||
"db_table_name": "md_1min_bars",
|
||||
"exchange_id": "ALPACA",
|
||||
"instrument_id_pfx": "STOCK-",
|
||||
"trading_hours": {
|
||||
"begin_session": "9:30:00",
|
||||
"end_session": "16:00:00",
|
||||
"timezone": "America/New_York"
|
||||
},
|
||||
"price_column": "close",
|
||||
"min_required_points": 30,
|
||||
"zero_threshold": 1e-10,
|
||||
"dis-equilibrium_open_trshld": 2.0,
|
||||
"dis-equilibrium_close_trshld": 1.0,
|
||||
"training_minutes": 120,
|
||||
"funding_per_pair": 2000.0,
|
||||
# "fit_method_class": "pt_trading.sliding_fit.SlidingFit",
|
||||
"fit_method_class": "pt_trading.static_fit.StaticFit",
|
||||
"exclude_instruments": ["CAN"],
|
||||
"close_outstanding_positions": false
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"security_type": "EQUITY",
|
||||
"data_directory": "./data/equity",
|
||||
"datafiles": [
|
||||
"20250602.mktdata.ohlcv.db",
|
||||
],
|
||||
"db_table_name": "md_1min_bars",
|
||||
"exchange_id": "ALPACA",
|
||||
"instrument_id_pfx": "STOCK-",
|
||||
"trading_hours": {
|
||||
"begin_session": "9:30:00",
|
||||
"end_session": "16:00:00",
|
||||
"timezone": "America/New_York"
|
||||
},
|
||||
"price_column": "close",
|
||||
"min_required_points": 30,
|
||||
"zero_threshold": 1e-10,
|
||||
"dis-equilibrium_open_trshld": 2.0,
|
||||
"dis-equilibrium_close_trshld": 1.0,
|
||||
"training_minutes": 120,
|
||||
"funding_per_pair": 2000.0,
|
||||
"fit_method_class": "pt_trading.fit_methods.StaticFit",
|
||||
"exclude_instruments": ["CAN"]
|
||||
}
|
||||
# "fit_method_class": "pt_trading.fit_methods.SlidingFit",
|
||||
# "fit_method_class": "pt_trading.fit_methods.StaticFit",
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
07.11.2025
|
||||
pairs_trading/configuration <---- directory for config
|
||||
equity_lg.cfg <-------- copy of equity.cfg
|
||||
How to run a Program: TRIANGLEsquare ----> triangle EQUITY backtest
|
||||
Results are in > results (timestamp table for all runs)
|
||||
table "...timestamp... .pt_backtest_results.equity.db"
|
||||
going to table using sqlite
|
||||
> sqlite3 '/home/coder/results/20250721_175750.pt_backtest_results.equity.db'
|
||||
|
||||
sqlite> .databases
|
||||
main: /home/coder/results/20250717_180122.pt_backtest_results.equity.db r/w
|
||||
sqlite> .tables
|
||||
config outstanding_positions pt_bt_results
|
||||
|
||||
sqlite> PRAGMA table_info('pt_bt_results');
|
||||
0|date|DATE|0||0
|
||||
1|pair|TEXT|0||0
|
||||
2|symbol|TEXT|0||0
|
||||
3|open_time|DATETIME|0||0
|
||||
4|open_side|TEXT|0||0
|
||||
5|open_price|REAL|0||0
|
||||
6|open_quantity|INTEGER|0||0
|
||||
7|open_disequilibrium|REAL|0||0
|
||||
8|close_time|DATETIME|0||0
|
||||
9|close_side|TEXT|0||0
|
||||
10|close_price|REAL|0||0
|
||||
11|close_quantity|INTEGER|0||0
|
||||
12|close_disequilibrium|REAL|0||0
|
||||
13|symbol_return|REAL|0||0
|
||||
14|pair_return|REAL|0||0
|
||||
|
||||
select count(*) as cnt from pt_bt_results;
|
||||
8
|
||||
|
||||
select * from pt_bt_results;
|
||||
|
||||
select
|
||||
date, close_time, pair, symbol, symbol_return, pair_return
|
||||
from pt_bt_results ;
|
||||
|
||||
select date, sum(symbol_return) as daily_return
|
||||
from pt_bt_results where date = '2025-06-18' group by date;
|
||||
|
||||
.quit
|
||||
|
||||
sqlite3 '/home/coder/results/20250717_172435.pt_backtest_results.equity.db'
|
||||
|
||||
sqlite> select date, sum(symbol_return) as daily_return
|
||||
from pt_bt_results group by date;
|
||||
|
||||
2025-06-02|1.29845390060828
|
||||
...
|
||||
2025-06-18|-43.5084977104115 <========== ????? ==========>
|
||||
2025-06-20|11.8605547517183
|
||||
|
||||
|
||||
select
|
||||
date, close_time, pair, symbol, symbol_return, pair_return
|
||||
from pt_bt_results ;
|
||||
|
||||
select date, close_time, pair, symbol, symbol_return, pair_return
|
||||
from pt_bt_results where date = '2025-06-18';
|
||||
|
||||
|
||||
./scripts/load_equity_pair_intraday.sh -A NVDA -B QQQ -d 20250701 -T ./intraday_md
|
||||
|
||||
to inspect exactly what sources, formats, and processing steps you can open the script with:
|
||||
head -n 50 ./scripts/load_equity_pair_intraday.sh
|
||||
|
||||
|
||||
|
||||
✓ Data file found: /home/coder/pairs_trading/data/crypto/20250605.mktdata.ohlcv.db
|
||||
|
||||
sqlite3 '/home/coder/results/20250722_201930.pt_backtest_results.crypto.db'
|
||||
|
||||
sqlite3 '/home/coder/results/xxxxxxxx_yyyyyy.pt_backtest_results.pseudo.db'
|
||||
|
||||
11111111
|
||||
=== At your terminal, run these commands:
|
||||
sqlite3 '/home/coder/results/20250722_201930.pt_backtest_results.crypto.db'
|
||||
=== Then inside the SQLite prompt:
|
||||
.mode csv
|
||||
.headers on
|
||||
.output results_20250722.csv
|
||||
SELECT * FROM pt_bt_results;
|
||||
.output stdout
|
||||
.quit
|
||||
|
||||
cd /home/coder/
|
||||
|
||||
# === mode csv formats output as CSV
|
||||
# === headers on includes column names
|
||||
# === output my_table.csv directs output to that file
|
||||
# === Run your SELECT query, then revert output
|
||||
# === Open my_table.csv in Excel directly
|
||||
|
||||
# ======== Using scp (Secure Copy)
|
||||
# === On your local machine, open a terminal and run:
|
||||
scp cvtt@953f6e8df266:/home/coder/results_20250722.csv ~/Downloads/
|
||||
|
||||
|
||||
# ===== convert cvs pandas dataframe ====== -->
|
||||
import pandas as pd
|
||||
# Replace with the actual path to your CSV file
|
||||
file_path = '/home/coder/results_20250722.csv'
|
||||
# Read the CSV file into a DataFrame
|
||||
df = pd.read_csv(file_path)
|
||||
# Show the first few rows
|
||||
print(df.head())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -427,14 +427,16 @@ class BacktestResult:
|
||||
trd["open_scaled_disequilibrium"] is not None
|
||||
and trd["open_scaled_disequilibrium"] is not None
|
||||
):
|
||||
disequil_info = f" | Open Dis-eq: {trd['open_scaled_disequilibrium']:.2f},"
|
||||
f" Close Dis-eq: {trd['open_scaled_disequilibrium']:.2f}"
|
||||
disequil_info = (
|
||||
f' | Open Dis-eq: {trd["open_scaled_disequilibrium"]:.2f},'
|
||||
f' Close Dis-eq: {trd["close_scaled_disequilibrium"]:.2f}'
|
||||
)
|
||||
|
||||
print(
|
||||
f" {trd['open_time'].time()}-{trd['close_time'].time()} {trd['symbol']}: "
|
||||
f" {trd['open_side']} @ ${trd['open_price']:.2f},"
|
||||
f" {trd["close_side"]} @ ${trd["close_price"]:.2f},"
|
||||
f" Return: {trd['symbol_return']:.2f}%{disequil_info}"
|
||||
f' {trd["open_time"].time()}-{trd["close_time"].time()} {trd["symbol"]}: '
|
||||
f' {trd["open_side"]} @ ${trd["open_price"]:.2f},'
|
||||
f' {trd["close_side"]} @ ${trd["close_price"]:.2f},'
|
||||
f' Return: {trd["symbol_return"]:.2f}%{disequil_info}'
|
||||
)
|
||||
pair_return += trd["symbol_return"]
|
||||
|
||||
|
||||
@@ -188,7 +188,8 @@ class RollingFit(PairsTradingFitMethod):
|
||||
open_px_b = open_row[f"{colname_b}"]
|
||||
|
||||
# creating the trades
|
||||
print(f"OPEN_TRADES: {row["tstamp"]} {open_scaled_disequilibrium=}")
|
||||
# use outer single quotes so we can reference DataFrame keys with double quotes inside
|
||||
print(f'OPEN_TRADES: {open_tstamp} open_scaled_disequilibrium={open_scaled_disequilibrium}')
|
||||
if open_disequilibrium > 0:
|
||||
open_side_a = "SELL"
|
||||
open_side_b = "BUY"
|
||||
@@ -237,10 +238,7 @@ class RollingFit(PairsTradingFitMethod):
|
||||
),
|
||||
]
|
||||
# Create DataFrame with explicit dtypes to avoid concatenation warnings
|
||||
df = pd.DataFrame(
|
||||
trd_signal_tuples,
|
||||
columns=self.TRADES_COLUMNS,
|
||||
)
|
||||
df = pd.DataFrame(trd_signal_tuples, columns=self.TRADES_COLUMNS)
|
||||
# Ensure consistent dtypes
|
||||
return df.astype(
|
||||
{
|
||||
|
||||
@@ -1,14 +1,47 @@
|
||||
# original script moved to vecm_rolling_fit_01.py
|
||||
|
||||
# 09.09.25 Added GARCH model - predicting volatility
|
||||
|
||||
# Rule of thumb:
|
||||
# alpha + beta ≈ 1 → strong volatility clustering, persistence.
|
||||
# If much lower → volatility mean reverts quickly.
|
||||
# If > 1 → model is unstable / non-stationary (bad).
|
||||
|
||||
# the VECM disequilibrium (mean reversion signal) and
|
||||
# the GARCH volatility forecast (risk measure).
|
||||
# combine them → e.g., only enter trades when:
|
||||
|
||||
# high_volatility = 1 → persistence > 0.95 or volatility > 2 (rule of thumb: unstable / risky regime).
|
||||
# high_volatility = 0 → stable regime.
|
||||
|
||||
|
||||
# VECM disequilibrium z-score > threshold and
|
||||
# GARCH-forecasted volatility is not too high (avoid noise-driven signals).
|
||||
# This creates a volatility-adjusted pairs trading strategy, more robust than plain VECM
|
||||
|
||||
# now pair_predict_result_ DataFrame includes:
|
||||
# disequilibrium, scaled_disequilibrium, z-scores, garch_alpha, garch_beta, garch_persistence (α+β rule-of-thumb)
|
||||
# garch_vol_forecast (1-step volatility forecast)
|
||||
|
||||
# Would you like me to also add a warning flag column
|
||||
# (e.g., "high_volatility" = 1 if persistence > 0.95 or vol_forecast > threshold)
|
||||
# so you can easily detect unstable regimes?
|
||||
|
||||
# VECM/GARCH
|
||||
# vecm_rolling_fit.py:
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from typing import Any, Dict, Optional
|
||||
from pt_trading.results import BacktestResult
|
||||
from pt_trading.rolling_window_fit import RollingFit
|
||||
from pt_trading.trading_pair import TradingPair
|
||||
from statsmodels.tsa.vector_ar.vecm import VECM, VECMResults
|
||||
from arch import arch_model
|
||||
|
||||
NanoPerMin = 1e9
|
||||
|
||||
|
||||
class VECMTradingPair(TradingPair):
|
||||
vecm_fit_: Optional[VECMResults]
|
||||
pair_predict_result_: Optional[pd.DataFrame]
|
||||
@@ -23,20 +56,23 @@ class VECMTradingPair(TradingPair):
|
||||
super().__init__(config, market_data, symbol_a, symbol_b)
|
||||
self.vecm_fit_ = None
|
||||
self.pair_predict_result_ = None
|
||||
self.garch_fit_ = None
|
||||
self.sigma_spread_forecast_ = None
|
||||
self.garch_alpha_ = None
|
||||
self.garch_beta_ = None
|
||||
self.garch_persistence_ = None
|
||||
self.high_volatility_flag_ = None
|
||||
|
||||
def _train_pair(self) -> None:
|
||||
self._fit_VECM()
|
||||
assert self.vecm_fit_ is not None
|
||||
|
||||
diseq_series = self.training_df_[self.colnames()] @ self.vecm_fit_.beta
|
||||
# print(diseq_series.shape)
|
||||
self.training_mu_ = float(diseq_series[0].mean())
|
||||
self.training_std_ = float(diseq_series[0].std())
|
||||
|
||||
self.training_df_["dis-equilibrium"] = (
|
||||
self.training_df_[self.colnames()] @ self.vecm_fit_.beta
|
||||
)
|
||||
# Normalize the dis-equilibrium
|
||||
self.training_df_["scaled_dis-equilibrium"] = (
|
||||
self.training_df_["disequilibrium"] = diseq_series
|
||||
self.training_df_["scaled_disequilibrium"] = (
|
||||
diseq_series - self.training_mu_
|
||||
) / self.training_std_
|
||||
|
||||
@@ -45,61 +81,96 @@ class VECMTradingPair(TradingPair):
|
||||
vecm_df = self.training_df_[self.colnames()].reset_index(drop=True)
|
||||
vecm_model = VECM(vecm_df, coint_rank=1)
|
||||
vecm_fit = vecm_model.fit()
|
||||
|
||||
assert vecm_fit is not None
|
||||
|
||||
# URGENT check beta and alpha
|
||||
|
||||
# Check if the model converged properly
|
||||
if not hasattr(vecm_fit, "beta") or vecm_fit.beta is None:
|
||||
print(f"{self}: VECM model failed to converge properly")
|
||||
|
||||
self.vecm_fit_ = vecm_fit
|
||||
pass
|
||||
|
||||
# Error Correction Term (spread)
|
||||
ect_series = (vecm_df @ vecm_fit.beta).iloc[:, 0]
|
||||
|
||||
# Difference the spread for stationarity
|
||||
dz = ect_series.diff().dropna()
|
||||
|
||||
if len(dz) < 30:
|
||||
print("Not enough data for GARCH fitting.")
|
||||
return
|
||||
|
||||
# Rescale if variance too small
|
||||
if dz.std() < 0.1:
|
||||
dz = dz * 1000
|
||||
# print("Scale check:", dz.std())
|
||||
|
||||
try:
|
||||
garch = arch_model(dz, vol="GARCH", p=1, q=1, mean="Zero", dist="normal")
|
||||
garch_fit = garch.fit(disp="off")
|
||||
self.garch_fit_ = garch_fit
|
||||
|
||||
# Extract parameters
|
||||
params = garch_fit.params
|
||||
self.garch_alpha_ = params.get("alpha[1]", np.nan)
|
||||
self.garch_beta_ = params.get("beta[1]", np.nan)
|
||||
self.garch_persistence_ = self.garch_alpha_ + self.garch_beta_
|
||||
|
||||
# print (f"GARCH α: {self.garch_alpha_:.4f}, β: {self.garch_beta_:.4f}, "
|
||||
# f"α+β (persistence): {self.garch_persistence_:.4f}")
|
||||
|
||||
# One-step-ahead volatility forecast
|
||||
forecast = garch_fit.forecast(horizon=1)
|
||||
sigma_next = np.sqrt(forecast.variance.iloc[-1, 0])
|
||||
self.sigma_spread_forecast_ = float(sigma_next)
|
||||
# print("GARCH sigma forecast:", self.sigma_spread_forecast_)
|
||||
|
||||
# Rule of thumb: persistence close to 1 or large volatility forecast
|
||||
self.high_volatility_flag_ = int(
|
||||
(self.garch_persistence_ is not None and self.garch_persistence_ > 0.95)
|
||||
or (self.sigma_spread_forecast_ is not None and self.sigma_spread_forecast_ > 2)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"GARCH fit failed: {e}")
|
||||
self.garch_fit_ = None
|
||||
self.sigma_spread_forecast_ = None
|
||||
self.high_volatility_flag_ = None
|
||||
|
||||
def predict(self) -> pd.DataFrame:
|
||||
self._train_pair()
|
||||
|
||||
assert self.testing_df_ is not None
|
||||
assert self.vecm_fit_ is not None
|
||||
|
||||
# VECM predictions
|
||||
predicted_prices = self.vecm_fit_.predict(steps=len(self.testing_df_))
|
||||
|
||||
# Convert prediction to a DataFrame for readability
|
||||
predicted_df = pd.DataFrame(
|
||||
predicted_prices, columns=pd.Index(self.colnames()), dtype=float
|
||||
)
|
||||
|
||||
predicted_df = pd.merge(
|
||||
self.testing_df_.reset_index(drop=True),
|
||||
pd.DataFrame(
|
||||
predicted_prices, columns=pd.Index(self.colnames()), dtype=float
|
||||
),
|
||||
pd.DataFrame(predicted_prices, columns=pd.Index(self.colnames()), dtype=float),
|
||||
left_index=True,
|
||||
right_index=True,
|
||||
suffixes=("", "_pred"),
|
||||
).dropna()
|
||||
|
||||
# Disequilibrium and z-scores
|
||||
predicted_df["disequilibrium"] = (
|
||||
predicted_df[self.colnames()] @ self.vecm_fit_.beta
|
||||
)
|
||||
|
||||
predicted_df["signed_scaled_disequilibrium"] = (
|
||||
predicted_df["disequilibrium"] - self.training_mu_
|
||||
) / self.training_std_
|
||||
|
||||
predicted_df["scaled_disequilibrium"] = abs(
|
||||
predicted_df["signed_scaled_disequilibrium"]
|
||||
)
|
||||
|
||||
predicted_df = predicted_df.reset_index(drop=True)
|
||||
# Add GARCH parameters + volatility forecast
|
||||
predicted_df["garch_alpha"] = self.garch_alpha_
|
||||
predicted_df["garch_beta"] = self.garch_beta_
|
||||
predicted_df["garch_persistence"] = self.garch_persistence_
|
||||
predicted_df["garch_vol_forecast"] = self.sigma_spread_forecast_
|
||||
predicted_df["high_volatility"] = self.high_volatility_flag_
|
||||
|
||||
# Save results
|
||||
if self.pair_predict_result_ is None:
|
||||
self.pair_predict_result_ = predicted_df
|
||||
else:
|
||||
self.pair_predict_result_ = pd.concat(
|
||||
[self.pair_predict_result_, predicted_df], ignore_index=True
|
||||
)
|
||||
# Reset index to ensure proper indexing
|
||||
self.pair_predict_result_ = self.pair_predict_result_.reset_index(drop=True)
|
||||
|
||||
return self.pair_predict_result_
|
||||
|
||||
|
||||
@@ -117,6 +188,6 @@ class VECMRollingFit(RollingFit):
|
||||
return VECMTradingPair(
|
||||
config=config,
|
||||
market_data=market_data,
|
||||
symbol_a=symbol_a,
|
||||
symbol_b=symbol_b,
|
||||
symbol_a = symbol_a,
|
||||
symbol_b = symbol_b,
|
||||
)
|
||||
@@ -1,18 +1,28 @@
|
||||
from typing import Any, Dict, Optional, cast
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import pandas as pd
|
||||
from pt_trading.results import BacktestResult
|
||||
import statsmodels.api as sm
|
||||
|
||||
from pt_trading.rolling_window_fit import RollingFit
|
||||
from pt_trading.trading_pair import TradingPair
|
||||
import statsmodels.api as sm
|
||||
|
||||
NanoPerMin = 1e9
|
||||
|
||||
|
||||
class ZScoreTradingPair(TradingPair):
|
||||
"""TradingPair implementation that fits a hedge ratio with OLS and
|
||||
computes a standardized spread (z-score).
|
||||
|
||||
The class stores training spread mean/std and hedge ratio so the model
|
||||
can be applied to testing data consistently.
|
||||
"""
|
||||
|
||||
zscore_model_: Optional[sm.regression.linear_model.RegressionResultsWrapper]
|
||||
pair_predict_result_: Optional[pd.DataFrame]
|
||||
zscore_df_: Optional[pd.DataFrame]
|
||||
zscore_df_: Optional[pd.Series]
|
||||
hedge_ratio_: Optional[float]
|
||||
spread_mean_: Optional[float]
|
||||
spread_std_: Optional[float]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -25,47 +35,79 @@ class ZScoreTradingPair(TradingPair):
|
||||
self.zscore_model_ = None
|
||||
self.pair_predict_result_ = None
|
||||
self.zscore_df_ = None
|
||||
self.hedge_ratio_ = None
|
||||
self.spread_mean_ = None
|
||||
self.spread_std_ = None
|
||||
|
||||
def _fit_zscore(self) -> None:
|
||||
"""Fit OLS on the training window and compute training z-score."""
|
||||
assert self.training_df_ is not None
|
||||
symbol_a_px_series = self.training_df_[self.colnames()].iloc[:, 0]
|
||||
symbol_b_px_series = self.training_df_[self.colnames()].iloc[:, 1]
|
||||
|
||||
symbol_a_px_series, symbol_b_px_series = symbol_a_px_series.align(
|
||||
symbol_b_px_series, axis=0
|
||||
)
|
||||
# Extract price series for the two symbols from the training frame.
|
||||
px_df = self.training_df_[self.colnames()]
|
||||
symbol_a_px = px_df.iloc[:, 0]
|
||||
symbol_b_px = px_df.iloc[:, 1]
|
||||
|
||||
X = sm.add_constant(symbol_b_px_series)
|
||||
self.zscore_model_ = sm.OLS(symbol_a_px_series, X).fit()
|
||||
assert self.zscore_model_ is not None
|
||||
hedge_ratio = self.zscore_model_.params.iloc[1]
|
||||
# Align indexes and fit OLS: symbol_a ~ const + symbol_b
|
||||
symbol_a_px, symbol_b_px = symbol_a_px.align(symbol_b_px, join="inner")
|
||||
X = sm.add_constant(symbol_b_px)
|
||||
self.zscore_model_ = sm.OLS(symbol_a_px, X).fit()
|
||||
|
||||
# Calculate spread and Z-score
|
||||
spread = symbol_a_px_series - hedge_ratio * symbol_b_px_series
|
||||
self.zscore_df_ = (spread - spread.mean()) / spread.std()
|
||||
# Hedge ratio is the slope on symbol_b
|
||||
params = self.zscore_model_.params
|
||||
self.hedge_ratio_ = float(params.iloc[1]) if len(params) > 1 else 0.0
|
||||
|
||||
# Training spread and its standardized z-score
|
||||
spread = symbol_a_px - self.hedge_ratio_ * symbol_b_px
|
||||
self.spread_mean_ = float(spread.mean())
|
||||
self.spread_std_ = float(spread.std(ddof=0)) if spread.std(ddof=0) != 0 else 1.0
|
||||
self.zscore_df_ = (spread - self.spread_mean_) / self.spread_std_
|
||||
|
||||
def predict(self) -> pd.DataFrame:
|
||||
"""Apply fitted hedge ratio to the testing frame and return a
|
||||
dataframe with canonical columns:
|
||||
- disequilibrium: signed z-score
|
||||
- scaled_disequilibrium: absolute z-score
|
||||
- signed_scaled_disequilibrium: same as disequilibrium (keeps sign)
|
||||
"""
|
||||
# Fit on training window
|
||||
self._fit_zscore()
|
||||
assert self.zscore_df_ is not None
|
||||
self.training_df_["dis-equilibrium"] = self.zscore_df_
|
||||
self.training_df_["scaled_dis-equilibrium"] = abs(self.zscore_df_)
|
||||
assert self.hedge_ratio_ is not None
|
||||
assert self.spread_mean_ is not None and self.spread_std_ is not None
|
||||
|
||||
# Keep training columns for inspection
|
||||
self.training_df_["disequilibrium"] = self.zscore_df_
|
||||
self.training_df_["scaled_disequilibrium"] = self.zscore_df_.abs()
|
||||
|
||||
# Apply model to testing frame
|
||||
assert self.testing_df_ is not None
|
||||
assert self.zscore_df_ is not None
|
||||
predicted_df = self.testing_df_
|
||||
test_df = self.testing_df_.copy()
|
||||
px_test = test_df[self.colnames()]
|
||||
a_test = px_test.iloc[:, 0]
|
||||
b_test = px_test.iloc[:, 1]
|
||||
a_test, b_test = a_test.align(b_test, join="inner")
|
||||
|
||||
predicted_df["disequilibrium"] = self.zscore_df_
|
||||
predicted_df["signed_scaled_disequilibrium"] = self.zscore_df_
|
||||
predicted_df["scaled_disequilibrium"] = abs(self.zscore_df_)
|
||||
# Compute test spread and standardize using training mean/std
|
||||
test_spread = a_test - self.hedge_ratio_ * b_test
|
||||
test_zscore = (test_spread - self.spread_mean_) / self.spread_std_
|
||||
|
||||
predicted_df = predicted_df.reset_index(drop=True)
|
||||
# Attach canonical columns
|
||||
# Align back to test_df index if needed
|
||||
test_zscore = test_zscore.reindex(test_df.index)
|
||||
test_df["disequilibrium"] = test_zscore
|
||||
test_df["signed_scaled_disequilibrium"] = test_zscore
|
||||
test_df["scaled_disequilibrium"] = test_zscore.abs()
|
||||
|
||||
# Reset index and accumulate results across windows
|
||||
test_df = test_df.reset_index(drop=True)
|
||||
if self.pair_predict_result_ is None:
|
||||
self.pair_predict_result_ = predicted_df
|
||||
self.pair_predict_result_ = test_df
|
||||
else:
|
||||
self.pair_predict_result_ = pd.concat(
|
||||
[self.pair_predict_result_, predicted_df], ignore_index=True
|
||||
[self.pair_predict_result_, test_df], ignore_index=True
|
||||
)
|
||||
# Reset index to ensure proper indexing
|
||||
|
||||
self.pair_predict_result_ = self.pair_predict_result_.reset_index(drop=True)
|
||||
return self.pair_predict_result_.dropna()
|
||||
|
||||
@@ -78,8 +120,5 @@ class ZScoreRollingFit(RollingFit):
|
||||
self, config: Dict, market_data: pd.DataFrame, symbol_a: str, symbol_b: str
|
||||
) -> TradingPair:
|
||||
return ZScoreTradingPair(
|
||||
config=config,
|
||||
market_data=market_data,
|
||||
symbol_a=symbol_a,
|
||||
symbol_b=symbol_b,
|
||||
config=config, market_data=market_data, symbol_a=symbol_a, symbol_b=symbol_b
|
||||
)
|
||||
|
||||
+106
-106
@@ -61,7 +61,7 @@ protobuf>=3.12.4
|
||||
psutil>=5.9.0
|
||||
ptyprocess>=0.7.0
|
||||
pycurl>=7.44.1
|
||||
pyelftools>=0.27
|
||||
# pyelftools>=0.27
|
||||
Pygments>=2.11.2
|
||||
pyparsing>=2.4.7
|
||||
pyrsistent>=0.18.1
|
||||
@@ -69,7 +69,7 @@ python-debian>=0.1.43 #+ubuntu1.1
|
||||
python-dotenv>=0.19.2
|
||||
python-magic>=0.4.24
|
||||
python-xlib>=0.29
|
||||
pyxdg>=0.27
|
||||
# pyxdg>=0.27
|
||||
PyYAML>=6.0
|
||||
reportlab>=3.6.8
|
||||
requests>=2.25.1
|
||||
@@ -82,113 +82,113 @@ six>=1.16.0
|
||||
soupsieve>=2.3.1
|
||||
ssh-import-id>=5.11
|
||||
statsmodels>=0.14.4
|
||||
texttable>=1.6.4
|
||||
# texttable>=1.6.4
|
||||
tldextract>=3.1.2
|
||||
tomli>=1.2.2
|
||||
######## typed-ast>=1.4.3
|
||||
types-aiofiles>=0.1
|
||||
types-annoy>=1.17
|
||||
types-appdirs>=1.4
|
||||
types-atomicwrites>=1.4
|
||||
types-aws-xray-sdk>=2.8
|
||||
types-babel>=2.9
|
||||
types-backports-abc>=0.5
|
||||
types-backports.ssl-match-hostname>=3.7
|
||||
types-beautifulsoup4>=4.10
|
||||
types-bleach>=4.1
|
||||
types-boto>=2.49
|
||||
types-braintree>=4.11
|
||||
types-cachetools>=4.2
|
||||
types-caldav>=0.8
|
||||
types-certifi>=2020.4
|
||||
types-characteristic>=14.3
|
||||
types-chardet>=4.0
|
||||
types-click>=7.1
|
||||
types-click-spinner>=0.1
|
||||
types-colorama>=0.4
|
||||
types-commonmark>=0.9
|
||||
types-contextvars>=0.1
|
||||
types-croniter>=1.0
|
||||
types-cryptography>=3.3
|
||||
types-dataclasses>=0.1
|
||||
types-dateparser>=1.0
|
||||
types-DateTimeRange>=0.1
|
||||
types-decorator>=0.1
|
||||
types-Deprecated>=1.2
|
||||
types-docopt>=0.6
|
||||
types-docutils>=0.17
|
||||
types-editdistance>=0.5
|
||||
types-emoji>=1.2
|
||||
types-entrypoints>=0.3
|
||||
types-enum34>=1.1
|
||||
types-filelock>=3.2
|
||||
types-first>=2.0
|
||||
types-Flask>=1.1
|
||||
types-freezegun>=1.1
|
||||
types-frozendict>=0.1
|
||||
types-futures>=3.3
|
||||
types-html5lib>=1.1
|
||||
types-httplib2>=0.19
|
||||
types-humanfriendly>=9.2
|
||||
types-ipaddress>=1.0
|
||||
types-itsdangerous>=1.1
|
||||
types-JACK-Client>=0.1
|
||||
types-Jinja2>=2.11
|
||||
types-jmespath>=0.10
|
||||
types-jsonschema>=3.2
|
||||
types-Markdown>=3.3
|
||||
types-MarkupSafe>=1.1
|
||||
types-mock>=4.0
|
||||
types-mypy-extensions>=0.4
|
||||
types-mysqlclient>=2.0
|
||||
types-oauthlib>=3.1
|
||||
types-orjson>=3.6
|
||||
types-paramiko>=2.7
|
||||
types-Pillow>=8.3
|
||||
types-polib>=1.1
|
||||
types-prettytable>=2.1
|
||||
types-protobuf>=3.17
|
||||
types-psutil>=5.8
|
||||
types-psycopg2>=2.9
|
||||
types-pyaudio>=0.2
|
||||
types-pycurl>=0.1
|
||||
types-pyfarmhash>=0.2
|
||||
types-Pygments>=2.9
|
||||
types-PyMySQL>=1.0
|
||||
types-pyOpenSSL>=20.0
|
||||
types-pyRFC3339>=0.1
|
||||
types-pysftp>=0.2
|
||||
types-pytest-lazy-fixture>=0.6
|
||||
types-python-dateutil>=2.8
|
||||
types-python-gflags>=3.1
|
||||
types-python-nmap>=0.6
|
||||
types-python-slugify>=5.0
|
||||
types-pytz>=2021.1
|
||||
types-pyvmomi>=7.0
|
||||
types-PyYAML>=5.4
|
||||
types-redis>=3.5
|
||||
types-requests>=2.25
|
||||
types-retry>=0.9
|
||||
types-selenium>=3.141
|
||||
types-Send2Trash>=1.8
|
||||
types-setuptools>=57.4
|
||||
types-simplejson>=3.17
|
||||
types-singledispatch>=3.7
|
||||
types-six>=1.16
|
||||
types-slumber>=0.7
|
||||
types-stripe>=2.59
|
||||
types-tabulate>=0.8
|
||||
types-termcolor>=1.1
|
||||
types-toml>=0.10
|
||||
types-toposort>=1.6
|
||||
types-ttkthemes>=3.2
|
||||
types-typed-ast>=1.4
|
||||
types-tzlocal>=0.1
|
||||
types-ujson>=0.1
|
||||
types-vobject>=0.9
|
||||
types-waitress>=0.1
|
||||
types-Werkzeug>=1.0
|
||||
types-xxhash>=2.0
|
||||
# types-aiofiles>=0.1
|
||||
# types-annoy>=1.17
|
||||
# types-appdirs>=1.4
|
||||
# types-atomicwrites>=1.4
|
||||
# types-aws-xray-sdk>=2.8
|
||||
# types-babel>=2.9
|
||||
# types-backports-abc>=0.5
|
||||
# types-backports.ssl-match-hostname>=3.7
|
||||
# types-beautifulsoup4>=4.10
|
||||
# types-bleach>=4.1
|
||||
# types-boto>=2.49
|
||||
# types-braintree>=4.11
|
||||
# types-cachetools>=4.2
|
||||
# types-caldav>=0.8
|
||||
# types-certifi>=2020.4
|
||||
# types-characteristic>=14.3
|
||||
# types-chardet>=4.0
|
||||
# types-click>=7.1
|
||||
# types-click-spinner>=0.1
|
||||
# types-colorama>=0.4
|
||||
# types-commonmark>=0.9
|
||||
# types-contextvars>=0.1
|
||||
# types-croniter>=1.0
|
||||
# types-cryptography>=3.3
|
||||
# types-dataclasses>=0.1
|
||||
# types-dateparser>=1.0
|
||||
# types-DateTimeRange>=0.1
|
||||
# types-decorator>=0.1
|
||||
# types-Deprecated>=1.2
|
||||
# types-docopt>=0.6
|
||||
# types-docutils>=0.17
|
||||
# types-editdistance>=0.5
|
||||
# types-emoji>=1.2
|
||||
# types-entrypoints>=0.3
|
||||
# types-enum34>=1.1
|
||||
# types-filelock>=3.2
|
||||
# types-first>=2.0
|
||||
# types-Flask>=1.1
|
||||
# types-freezegun>=1.1
|
||||
# types-frozendict>=0.1
|
||||
# types-futures>=3.3
|
||||
# types-html5lib>=1.1
|
||||
# types-httplib2>=0.19
|
||||
# types-humanfriendly>=9.2
|
||||
# types-ipaddress>=1.0
|
||||
# types-itsdangerous>=1.1
|
||||
# types-JACK-Client>=0.1
|
||||
# types-Jinja2>=2.11
|
||||
# types-jmespath>=0.10
|
||||
# types-jsonschema>=3.2
|
||||
# types-Markdown>=3.3
|
||||
# types-MarkupSafe>=1.1
|
||||
# types-mock>=4.0
|
||||
# types-mypy-extensions>=0.4
|
||||
# types-mysqlclient>=2.0
|
||||
# types-oauthlib>=3.1
|
||||
# types-orjson>=3.6
|
||||
# types-paramiko>=2.7
|
||||
# types-Pillow>=8.3
|
||||
# types-polib>=1.1
|
||||
# types-prettytable>=2.1
|
||||
# types-protobuf>=3.17
|
||||
# types-psutil>=5.8
|
||||
# types-psycopg2>=2.9
|
||||
# types-pyaudio>=0.2
|
||||
# types-pycurl>=0.1
|
||||
# types-pyfarmhash>=0.2
|
||||
# types-Pygments>=2.9
|
||||
# types-PyMySQL>=1.0
|
||||
# types-pyOpenSSL>=20.0
|
||||
# types-pyRFC3339>=0.1
|
||||
# types-pysftp>=0.2
|
||||
# types-pytest-lazy-fixture>=0.6
|
||||
# types-python-dateutil>=2.8
|
||||
# types-python-gflags>=3.1
|
||||
# types-python-nmap>=0.6
|
||||
# types-python-slugify>=5.0
|
||||
# types-pytz>=2021.1
|
||||
# types-pyvmomi>=7.0
|
||||
# types-PyYAML>=5.4
|
||||
# types-redis>=3.5
|
||||
# types-requests>=2.25
|
||||
# types-retry>=0.9
|
||||
# types-selenium>=3.141
|
||||
# types-Send2Trash>=1.8
|
||||
# types-setuptools>=57.4
|
||||
# types-simplejson>=3.17
|
||||
# types-singledispatch>=3.7
|
||||
# types-six>=1.16
|
||||
# types-slumber>=0.7
|
||||
# types-stripe>=2.59
|
||||
# types-tabulate>=0.8
|
||||
# types-termcolor>=1.1
|
||||
# types-toml>=0.10
|
||||
# types-toposort>=1.6
|
||||
# types-ttkthemes>=3.2
|
||||
# types-typed-ast>=1.4
|
||||
# types-tzlocal>=0.1
|
||||
# types-ujson>=0.1
|
||||
# types-vobject>=0.9
|
||||
# types-waitress>=0.1
|
||||
#types-Werkzeug>=1.0
|
||||
#types-xxhash>=2.0
|
||||
typing-extensions>=3.10.0.2
|
||||
Unidecode>=1.3.3
|
||||
urllib3>=1.26.5
|
||||
|
||||
@@ -9,18 +9,19 @@ import pandas as pd
|
||||
|
||||
from tools.config import expand_filename, load_config
|
||||
from tools.data_loader import get_available_instruments_from_db
|
||||
|
||||
from pt_trading.results import (
|
||||
BacktestResult,
|
||||
create_result_database,
|
||||
store_config_in_database,
|
||||
store_results_in_database,
|
||||
)
|
||||
|
||||
from pt_trading.fit_method import PairsTradingFitMethod
|
||||
from pt_trading.trading_pair import TradingPair
|
||||
|
||||
from research.research_tools import create_pairs, resolve_datafiles
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Run pairs trading backtest.")
|
||||
parser.add_argument(
|
||||
@@ -36,7 +37,7 @@ def main() -> None:
|
||||
"--instruments",
|
||||
type=str,
|
||||
required=False,
|
||||
help="Comma-separated list of instrument symbols (e.g., COIN,GBTC). If not provided, auto-detects from database.",
|
||||
help = "Comma-separated list of instrument symbols (e.g., COIN,GBTC). If not provided, auto-detects from database.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"cells": [],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.12.5"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,7 +5,6 @@ from typing import Dict, List, Optional
|
||||
import pandas as pd
|
||||
from pt_trading.fit_method import PairsTradingFitMethod
|
||||
|
||||
|
||||
def resolve_datafiles(config: Dict, cli_datafiles: Optional[str] = None) -> List[str]:
|
||||
"""
|
||||
Resolve the list of data files to process.
|
||||
@@ -69,9 +68,9 @@ def create_pairs(
|
||||
|
||||
for datafile in datafiles:
|
||||
md_df = load_market_data(
|
||||
datafile=datafile,
|
||||
instruments=instruments,
|
||||
db_table_name=config_copy["market_data_loading"][instruments[0]["instrument_type"]]["db_table_name"],
|
||||
datafile = datafile,
|
||||
instruments = instruments,
|
||||
db_table_name = config_copy["market_data_loading"][instruments[0]["instrument_type"]]["db_table_name"],
|
||||
trading_hours=config_copy["trading_hours"],
|
||||
extra_minutes=extra_minutes,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user