diff --git a/configuration/crypto.cfg b/configuration/crypto_vecm.cfg similarity index 100% rename from configuration/crypto.cfg rename to configuration/crypto_vecm.cfg diff --git a/configuration/crypto_zscore.cfg b/configuration/crypto_zscore.cfg new file mode 100644 index 0000000..e58cf87 --- /dev/null +++ b/configuration/crypto_zscore.cfg @@ -0,0 +1,33 @@ +{ + "security_type": "CRYPTO", + "data_directory": "./data/crypto", + "datafiles": [ + "2025*.mktdata.ohlcv.db" + ], + "db_table_name": "md_1min_bars", + "exchange_id": "BNBSPOT", + "instrument_id_pfx": "PAIR-", + "funding_per_pair": 2000.0, + + # ====== Trading Parameters ====== + "price_column": "close", + "dis-equilibrium_open_trshld": 2.0, + "dis-equilibrium_close_trshld": 0.5, + "training_minutes": 120, + "fit_method_class": "pt_trading.z-score_rolling_fit.ZScoreRollingFit", + + # ====== Stop Conditions ====== + "stop_close_conditions": { + "profit": 2.0, + "loss": -0.5 + } + + # ====== End of Session Closeout ====== + "close_outstanding_positions": true, + # "close_outstanding_positions": false, + "trading_hours": { + "begin_session": "9:30:00", + "end_session": "21:30:00", + "timezone": "America/New_York" + } +} \ No newline at end of file diff --git a/configuration/equity.cfg b/configuration/equity_vecm.cfg similarity index 100% rename from configuration/equity.cfg rename to configuration/equity_vecm.cfg diff --git a/configuration/equity_zscore.cfg b/configuration/equity_zscore.cfg new file mode 100644 index 0000000..fc5c71c --- /dev/null +++ b/configuration/equity_zscore.cfg @@ -0,0 +1,35 @@ +{ + "security_type": "EQUITY", + "data_directory": "./data/equity", + "datafiles": [ + "202506*.mktdata.ohlcv.db", + ], + "db_table_name": "md_1min_bars", + "exchange_id": "ALPACA", + "instrument_id_pfx": "STOCK-", + "exclude_instruments": ["CAN"], + + "funding_per_pair": 2000.0, + + # ====== Trading Parameters ====== + "price_column": "close", + "dis-equilibrium_open_trshld": 2.0, + "dis-equilibrium_close_trshld": 1.0, + "training_minutes": 120, + "fit_method_class": "pt_trading.z-score_rolling_fit.ZScoreRollingFit", + + # ====== Stop Conditions ====== + "stop_close_conditions": { + "profit": 2.0, + "loss": -0.5 + } + + # ====== End of Session Closeout ====== + "close_outstanding_positions": true, + # "close_outstanding_positions": false, + "trading_hours": { + "begin_session": "9:30:00", + "end_session": "15:30:00", + "timezone": "America/New_York" + } +} \ No newline at end of file diff --git a/lib/pt_trading/rolling_window_fit.py b/lib/pt_trading/rolling_window_fit.py index 843fe81..6b71332 100644 --- a/lib/pt_trading/rolling_window_fit.py +++ b/lib/pt_trading/rolling_window_fit.py @@ -11,6 +11,14 @@ from statsmodels.tsa.vector_ar.vecm import VECM, VECMResults NanoPerMin = 1e9 class RollingFit(PairsTradingFitMethod): + ''' + N O T E: + ========= + - This class remains to be abstract + - The following methods are to be implemented in the subclass: + - create_trading_pair() + ========= + ''' def __init__(self) -> None: super().__init__() diff --git a/lib/pt_trading/vecm_rolling_fit.py b/lib/pt_trading/vecm_rolling_fit.py index a8d5d2f..d4cc64c 100644 --- a/lib/pt_trading/vecm_rolling_fit.py +++ b/lib/pt_trading/vecm_rolling_fit.py @@ -17,9 +17,8 @@ class VECMTradingPair(TradingPair): self.pair_predict_result_ = None def _train_pair(self) -> None: - # print('*' * 80 + '\n' + f"**************** {self} IS COINTEGRATED ****************\n" + '*' * 80) self._fit_VECM() - assert self.training_df_ is not None and self.vecm_fit_ is not None + 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()) diff --git a/lib/pt_trading/z-score_rolling_fit.py b/lib/pt_trading/z-score_rolling_fit.py new file mode 100644 index 0000000..3c68c1d --- /dev/null +++ b/lib/pt_trading/z-score_rolling_fit.py @@ -0,0 +1,78 @@ +from typing import Any, Dict, Optional, cast + +import pandas as pd +from pt_trading.results import BacktestResult +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): + zscore_model_: Optional[sm.regression.linear_model.RegressionResultsWrapper] + pair_predict_result_: Optional[pd.DataFrame] + zscore_df_: Optional[pd.DataFrame] + + def __init__(self, config: Dict[str, Any], market_data: pd.DataFrame, symbol_a: str, symbol_b: str, price_column: str): + super().__init__(config, market_data, symbol_a, symbol_b, price_column) + self.zscore_model_ = None + self.pair_predict_result_ = None + self.zscore_df_ = None + + def _fit_zscore(self) -> None: + assert self.training_df_ is not None + a = self.training_df_[self.colnames()].iloc[:, 0] + b = self.training_df_[self.colnames()].iloc[:, 1] + + a,b = a.align(b, axis=0) + + + X = sm.add_constant(b) + self.zscore_model_ = sm.OLS(a, X).fit() + assert self.zscore_model_ is not None + hedge_ratio = self.zscore_model_.params.iloc[1] + + # Calculate spread and Z-score + spread = a - hedge_ratio * b + self.zscore_df_ = (spread - spread.mean()) / spread.std() + + def predict(self) -> pd.DataFrame: + 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.testing_df_ is not None + assert self.zscore_df_ is not None + predicted_df = self.testing_df_ + + predicted_df["disequilibrium"] = self.zscore_df_ + predicted_df["scaled_disequilibrium"] = abs(self.zscore_df_) + + predicted_df = predicted_df.reset_index(drop=True) + 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_ + + +class ZScoreRollingFit(RollingFit): + def __init__(self) -> None: + super().__init__() + + def run_pair( + self, pair: TradingPair, bt_result: BacktestResult + ) -> Optional[pd.DataFrame]: + return super().run_pair(pair, bt_result) + def create_trading_pair( + self, config: Dict, market_data: pd.DataFrame, symbol_a: str, symbol_b: str, price_column: str + ) -> TradingPair: + return ZScoreTradingPair( + config=config, + market_data=market_data, + symbol_a=symbol_a, + symbol_b=symbol_b, + price_column=price_column + ) diff --git a/requirements.txt b/requirements.txt index c1208f7..57f7220 100644 --- a/requirements.txt +++ b/requirements.txt @@ -74,6 +74,7 @@ PyYAML>=6.0 reportlab>=3.6.8 requests>=2.25.1 requests-file>=1.5.1 +scipy<1.13.0 seaborn>=0.13.2 SecretStorage>=3.3.1 setproctitle>=1.2.2 diff --git a/research/notebooks/pt_sliding.ipynb b/research/notebooks/single_pair_test.ipynb similarity index 91% rename from research/notebooks/pt_sliding.ipynb rename to research/notebooks/single_pair_test.ipynb index f8400e6..9d03d31 100644 --- a/research/notebooks/pt_sliding.ipynb +++ b/research/notebooks/single_pair_test.ipynb @@ -35,7 +35,7 @@ "global bt_result\n", "\n", "# ================================ E Q U I T Y ================================\n", - "CONFIG_FILE = \"equity\" # Options: \"equity\", \"crypto\", or custom filename (without .cfg extension)\n", + "CONFIG_FILE = \"/home/oleg/develop/pairs_trading/configuration/equity_zscore.cfg\"\n", "\n", "# Date for data file selection (format: YYYYMMDD)\n", "TRADING_DATE = \"20250618\" # Change this to your desired date\n", @@ -46,10 +46,10 @@ "# ================================ E Q U I T Y ================================\n", "\n", "# ================================ C R Y P T O ================================\n", - "# CONFIG_FILE = \"crypto\" # Options: \"equity\", \"crypto\", or custom filename (without .cfg extension)\n", + "# CONFIG_FILE = \"/home/oleg/develop/pairs_trading/configuration/crypto.cfg\"\n", "\n", "# # Date for data file selection (format: YYYYMMDD)\n", - "# TRADING_DATE = \"20250605\" # Change this to your desired date\n", + "# TRADING_DATE = \"20250618\" # Change this to your desired date\n", "\n", "# # Trading pair symbols\n", "# SYMBOL_A = \"BTC-USDT\" # Change this to your desired symbol A\n", @@ -130,7 +130,7 @@ "\n", "def load_config_from_file() -> Optional[Dict]:\n", " \"\"\"Load configuration from configuration files using HJSON\"\"\"\n", - " config_file = f\"../../configuration/{CONFIG_FILE}.cfg\"\n", + " config_file = CONFIG_FILE\n", " \n", " try:\n", " with open(config_file, 'r') as f:\n", @@ -1261,12 +1261,12 @@ "text": [ "Setup complete!\n", "Trading Parameters:\n", - " Configuration: equity\n", + " Configuration: /home/oleg/develop/pairs_trading/configuration/equity_zscore.cfg\n", " Symbol A: COIN\n", " Symbol B: MSTR\n", " Trading Date: 2025-06-18\n", "\n", - "Loading equity configuration using HJSON...\n", + "Loading /home/oleg/develop/pairs_trading/configuration/equity_zscore.cfg configuration using HJSON...\n", "✓ Successfully loaded EQUITY configuration\n", " Data directory: /home/oleg/develop/pairs_trading/data/equity\n", " Database table: md_1min_bars\n", @@ -1274,9 +1274,9 @@ " Training window: 120 minutes\n", " Open threshold: 2\n", " Close threshold: 1\n", - "Fit Model: pt_trading.vecm_rolling_fit.VECMRollingFit\n", + "Fit Model: pt_trading.z-score_rolling_fit.ZScoreRollingFit\n", "Load configuration SUCCESS\n", - " Fit Method: VECMRollingFit\n", + " Fit Method: ZScoreRollingFit\n", "\n", "Data Configuration:\n", " Data File: 20250618.mktdata.ohlcv.db\n", @@ -1499,15 +1499,15 @@ "=== SLIDING FIT ANALYSIS ===\n", "Processing first 200 iterations for demonstration...\n", "***COIN & MSTR*** STARTING....\n", - "OPEN_TRADES: 2025-06-18 15:30:00 open_scaled_disequilibrium=np.float64(4.435087777514003)\n", + "OPEN_TRADES: 2025-06-18 15:35:00 open_scaled_disequilibrium=2.395325098245104\n", "OPEN TRADES:\n", " time action symbol price disequilibrium scaled_disequilibrium pair status\n", - "0 2025-06-18 15:30:00 SELL COIN 265.7900 10.226451 4.435088 COIN & MSTR OPEN\n", - "1 2025-06-18 15:30:00 BUY MSTR 372.9349 10.226451 4.435088 COIN & MSTR OPEN\n", + "0 2025-06-18 15:35:00 BUY COIN 270.0100 -2.395325 2.395325 COIN & MSTR OPEN\n", + "1 2025-06-18 15:35:00 SELL MSTR 373.5822 -2.395325 2.395325 COIN & MSTR OPEN\n", "STOP CLOSE TRADES:\n", - " time action symbol price disequilibrium scaled_disequilibrium pair status\n", - "0 2025-06-18 15:34:00 BUY COIN 268.47 13.457921 4.608004 COIN & MSTR CLOSE_STOP_LOSS\n", - "1 2025-06-18 15:34:00 SELL MSTR 373.08 13.457921 4.608004 COIN & MSTR CLOSE_STOP_LOSS\n", + " time action symbol price disequilibrium scaled_disequilibrium pair status\n", + "0 2025-06-18 15:49:00 SELL COIN 275.4269 -1.873059 1.873059 COIN & MSTR CLOSE_STOP_PROFIT\n", + "1 2025-06-18 15:49:00 BUY MSTR 373.5200 -1.873059 1.873059 COIN & MSTR CLOSE_STOP_PROFIT\n", "***COIN & MSTR*** FINISHED *** Num Trades:4\n", "Generated 4 trading signals\n", "\n", @@ -1542,14 +1542,14 @@ "Timeline range: 2025-06-18 13:30:00 to 2025-06-18 19:30:00\n", "\n", "Symbol_A trades:\n", - " time action symbol price disequilibrium scaled_disequilibrium pair status\n", - "0 2025-06-18 15:30:00 SELL COIN 265.79 10.226451 4.435088 COIN & MSTR OPEN\n", - "2 2025-06-18 15:34:00 BUY COIN 268.47 13.457921 4.608004 COIN & MSTR CLOSE_STOP_LOSS\n", + " time action symbol price disequilibrium scaled_disequilibrium pair status\n", + "0 2025-06-18 15:35:00 BUY COIN 270.0100 -2.395325 2.395325 COIN & MSTR OPEN\n", + "2 2025-06-18 15:49:00 SELL COIN 275.4269 -1.873059 1.873059 COIN & MSTR CLOSE_STOP_PROFIT\n", "\n", "Symbol_B trades:\n", - " time action symbol price disequilibrium scaled_disequilibrium pair status\n", - "1 2025-06-18 15:30:00 BUY MSTR 372.9349 10.226451 4.435088 COIN & MSTR OPEN\n", - "3 2025-06-18 15:34:00 SELL MSTR 373.0800 13.457921 4.608004 COIN & MSTR CLOSE_STOP_LOSS\n" + " time action symbol price disequilibrium scaled_disequilibrium pair status\n", + "1 2025-06-18 15:35:00 SELL MSTR 373.5822 -2.395325 2.395325 COIN & MSTR OPEN\n", + "3 2025-06-18 15:49:00 BUY MSTR 373.5200 -1.873059 1.873059 COIN & MSTR CLOSE_STOP_PROFIT\n" ] }, { @@ -1934,7 +1934,7 @@ ], "xaxis": "x", "y": { - "bdata": "AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAA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", + "bdata": "AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAA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", "dtype": "f8" }, "yaxis": "y" @@ -1949,8 +1949,8 @@ "name": "OPEN", "type": "scatter", "x": [ - "2025-06-18T15:30:00.000000000", - "2025-06-18T15:30:00.000000000" + "2025-06-18T15:35:00.000000000", + "2025-06-18T15:35:00.000000000" ], "xaxis": "x2", "y": [ @@ -2346,15 +2346,15 @@ "symbol": "triangle-up" }, "mode": "markers", - "name": "COIN BUY CLOSE", + "name": "COIN BUY OPEN", "showlegend": true, "type": "scatter", "x": [ - "2025-06-18T15:34:00.000000000" + "2025-06-18T15:35:00.000000000" ], "xaxis": "x3", "y": { - "bdata": "7FG4HoXHcEA=", + "bdata": "XI/C9SjgcEA=", "dtype": "f8" }, "yaxis": "y3" @@ -2366,15 +2366,15 @@ "symbol": "triangle-down" }, "mode": "markers", - "name": "COIN SELL OPEN", + "name": "COIN SELL CLOSE", "showlegend": true, "type": "scatter", "x": [ - "2025-06-18T15:30:00.000000000" + "2025-06-18T15:49:00.000000000" ], "xaxis": "x3", "y": { - "bdata": "cT0K16OccEA=", + "bdata": "mSoYldQ2cUA=", "dtype": "f8" }, "yaxis": "y3" @@ -2764,15 +2764,15 @@ "symbol": "triangle-up" }, "mode": "markers", - "name": "MSTR BUY OPEN", + "name": "MSTR BUY CLOSE", "showlegend": true, "type": "scatter", "x": [ - "2025-06-18T15:30:00.000000000" + "2025-06-18T15:49:00.000000000" ], "xaxis": "x4", "y": { - "bdata": "fdCzWfVOd0A=", + "bdata": "uB6F61FYd0A=", "dtype": "f8" }, "yaxis": "y4" @@ -2784,15 +2784,15 @@ "symbol": "triangle-down" }, "mode": "markers", - "name": "MSTR SELL CLOSE", + "name": "MSTR SELL OPEN", "showlegend": true, "type": "scatter", "x": [ - "2025-06-18T15:34:00.000000000" + "2025-06-18T15:35:00.000000000" ], "xaxis": "x4", "y": { - "bdata": "4XoUrkdRd0A=", + "bdata": "s3vysFBZd0A=", "dtype": "f8" }, "yaxis": "y4" @@ -3803,9 +3803,9 @@ }, "text/html": [ "