From b87b40a6edcdda62f6c0423ed9dee836bdf66dd8 Mon Sep 17 00:00:00 2001 From: Oleg Sheynin Date: Mon, 21 Jul 2025 05:15:33 +0000 Subject: [PATCH] progress --- configuration/crypto.cfg | 2 +- configuration/equity.cfg | 2 +- lib/pt_trading/{ => __DELETE__}/static_fit.py | 0 lib/pt_trading/fit_method.py | 6 +- .../{sliding_fit.py => rolling_window_fit.py} | 157 +------ lib/pt_trading/trading_pair.py | 93 +---- lib/pt_trading/vecm_rolling_fit.py | 111 +++++ research/notebooks/pt_sliding.ipynb | 382 +++++------------- research/pt_backtest.py | 2 +- research/research_tools.py | 6 +- strategy/pair_strategy.py | 2 +- 11 files changed, 253 insertions(+), 510 deletions(-) rename lib/pt_trading/{ => __DELETE__}/static_fit.py (100%) rename lib/pt_trading/{sliding_fit.py => rolling_window_fit.py} (65%) create mode 100644 lib/pt_trading/vecm_rolling_fit.py diff --git a/configuration/crypto.cfg b/configuration/crypto.cfg index 00d0861..fdadd81 100644 --- a/configuration/crypto.cfg +++ b/configuration/crypto.cfg @@ -14,7 +14,7 @@ "dis-equilibrium_open_trshld": 2.0, "dis-equilibrium_close_trshld": 1.0, "training_minutes": 120, - "fit_method_class": "pt_trading.sliding_fit.SlidingFit", + "fit_method_class": "pt_trading.vecm_rolling_fit.VECMRollingFit", # ====== Stop Conditions ====== "stop_close_conditions": { diff --git a/configuration/equity.cfg b/configuration/equity.cfg index d572676..7f4ac03 100644 --- a/configuration/equity.cfg +++ b/configuration/equity.cfg @@ -16,7 +16,7 @@ "dis-equilibrium_open_trshld": 2.0, "dis-equilibrium_close_trshld": 1.0, "training_minutes": 120, - "fit_method_class": "pt_trading.sliding_fit.SlidingFit", + "fit_method_class": "pt_trading.vecm_rolling_fit.VECMRollingFit", # ====== Stop Conditions ====== "stop_close_conditions": { diff --git a/lib/pt_trading/static_fit.py b/lib/pt_trading/__DELETE__/static_fit.py similarity index 100% rename from lib/pt_trading/static_fit.py rename to lib/pt_trading/__DELETE__/static_fit.py diff --git a/lib/pt_trading/fit_method.py b/lib/pt_trading/fit_method.py index 9375ab2..6742f1d 100644 --- a/lib/pt_trading/fit_method.py +++ b/lib/pt_trading/fit_method.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from enum import Enum from typing import Dict, Optional, cast -import pandas as pd # type: ignore[import] +import pandas as pd from pt_trading.results import BacktestResult from pt_trading.trading_pair import TradingPair @@ -28,4 +28,8 @@ class PairsTradingFitMethod(ABC): @abstractmethod def reset(self) -> None: ... + @abstractmethod + def create_trading_pair( + self, config: Dict, market_data: pd.DataFrame, symbol_a: str, symbol_b: str, price_column: str + ) -> TradingPair: ... diff --git a/lib/pt_trading/sliding_fit.py b/lib/pt_trading/rolling_window_fit.py similarity index 65% rename from lib/pt_trading/sliding_fit.py rename to lib/pt_trading/rolling_window_fit.py index f940d3c..843fe81 100644 --- a/lib/pt_trading/sliding_fit.py +++ b/lib/pt_trading/rolling_window_fit.py @@ -1,15 +1,16 @@ from abc import ABC, abstractmethod from enum import Enum -from typing import Dict, Optional, cast +from typing import Any, Dict, Optional, cast import pandas as pd # type: ignore[import] from pt_trading.fit_method import PairsTradingFitMethod from pt_trading.results import BacktestResult -from pt_trading.trading_pair import CointegrationData, TradingPair, PairState +from pt_trading.trading_pair import PairState, TradingPair +from statsmodels.tsa.vector_ar.vecm import VECM, VECMResults NanoPerMin = 1e9 - -class SlidingFit(PairsTradingFitMethod): + +class RollingFit(PairsTradingFitMethod): def __init__(self) -> None: super().__init__() @@ -52,17 +53,11 @@ class SlidingFit(PairsTradingFitMethod): ) break - try: - # ================================ TRAINING ================================ - pair.train_pair() - except Exception as e: - raise RuntimeError(f"{pair}: Training failed: {str(e)}") from e - try: # ================================ PREDICTION ================================ - pair.predict() + self.pair_predict_result_ = pair.predict() except Exception as e: - raise RuntimeError(f"{pair}: Prediction failed: {str(e)}") from e + raise RuntimeError(f"{pair}: TrainingPrediction failed: {str(e)}") from e # break @@ -72,20 +67,21 @@ class SlidingFit(PairsTradingFitMethod): curr_predicted_row_idx += 1 self._create_trading_signals(pair, config, bt_result) - print(f"***{pair}*** FINISHED *** Num Trades:{len(pair.user_data_['trades'])}") + print(f"***{pair}*** FINISHED *** Num Trades:{len(pair.user_data_['trades'])}") + return pair.get_trades() def _create_trading_signals( self, pair: TradingPair, config: Dict, bt_result: BacktestResult ) -> None: - if pair.predicted_df_ is None: - print(f"{pair.market_data_.iloc[0]['tstamp']} {pair}: No predicted data") - return + + predicted_df = self.pair_predict_result_ + assert predicted_df is not None open_threshold = config["dis-equilibrium_open_trshld"] close_threshold = config["dis-equilibrium_close_trshld"] - for curr_predicted_row_idx in range(len(pair.predicted_df_)): - pred_row = pair.predicted_df_.iloc[curr_predicted_row_idx] + for curr_predicted_row_idx in range(len(predicted_df)): + pred_row = predicted_df.iloc[curr_predicted_row_idx] scaled_disequilibrium = pred_row["scaled_disequilibrium"] if pair.user_data_["state"] in [PairState.INITIAL, PairState.CLOSE, PairState.CLOSE_POSITION]: @@ -141,10 +137,10 @@ class SlidingFit(PairsTradingFitMethod): pair.user_data_["state"] = PairState.CLOSE_POSITION pair.on_close_trades(close_position_trades) else: - if pair.predicted_df_ is not None: + if predicted_df is not None: bt_result.handle_outstanding_position( pair=pair, - pair_result_df=pair.predicted_df_, + pair_result_df=predicted_df, last_row_index=0, open_side_a=pair.user_data_["open_side_a"], open_side_b=pair.user_data_["open_side_b"], @@ -158,13 +154,6 @@ class SlidingFit(PairsTradingFitMethod): ) -> Optional[pd.DataFrame]: colname_a, colname_b = pair.colnames() - assert pair.predicted_df_ is not None - predicted_df = pair.predicted_df_ - - # Check if we have any data to work with - if len(predicted_df) == 0: - return None - open_row = row open_tstamp = open_row["tstamp"] open_disequilibrium = open_row["disequilibrium"] @@ -238,10 +227,6 @@ class SlidingFit(PairsTradingFitMethod): ) -> Optional[pd.DataFrame]: colname_a, colname_b = pair.colnames() - assert pair.predicted_df_ is not None - if len(pair.predicted_df_) == 0: - return None - close_row = row close_tstamp = close_row["tstamp"] close_disequilibrium = close_row["disequilibrium"] @@ -289,114 +274,6 @@ class SlidingFit(PairsTradingFitMethod): "pair": "object" }) - # def _get_stop_close_trades( - # self, pair: TradingPair, row: pd.Series, close_threshold: float - # ) -> Optional[pd.DataFrame]: - # colname_a, colname_b = pair.colnames() - # assert pair.predicted_df_ is not None - # if len(pair.predicted_df_) == 0: - # return None - - # stop_close_row = row - # stop_close_tstamp = stop_close_row["tstamp"] - # stop_close_disequilibrium = stop_close_row["disequilibrium"] - # stop_close_scaled_disequilibrium = stop_close_row["scaled_disequilibrium"] - # stop_close_px_a = stop_close_row[f"{colname_a}"] - # stop_close_px_b = stop_close_row[f"{colname_b}"] - - # stop_close_side_a = pair.user_data_["close_side_a"] - # stop_close_side_b = pair.user_data_["close_side_b"] - - # trd_signal_tuples = [ - # ( - # stop_close_tstamp, - # stop_close_side_a, - # pair.symbol_a_, - # stop_close_px_a, - # stop_close_disequilibrium, - # stop_close_scaled_disequilibrium, - # pair, - # ), - # ( - # stop_close_tstamp, - # stop_close_side_b, - # pair.symbol_b_, - # stop_close_px_b, - # stop_close_disequilibrium, - # stop_close_scaled_disequilibrium, - # pair, - # ), - # ] - # df = pd.DataFrame( - # trd_signal_tuples, - # columns=self.TRADES_COLUMNS, - # ) - # # Ensure consistent dtypes - # return df.astype({ - # "time": "datetime64[ns]", - # "action": "string", - # "symbol": "string", - # "price": "float64", - # "disequilibrium": "float64", - # "scaled_disequilibrium": "float64", - # "pair": "object" - # }) - - # def _get_close_position_trades( - # self, pair: TradingPair, row: pd.Series, close_threshold: float - # ) -> Optional[pd.DataFrame]: - # colname_a, colname_b = pair.colnames() - - # assert pair.predicted_df_ is not None - # if len(pair.predicted_df_) == 0: - # return None - - # close_position_row = row - # close_position_tstamp = close_position_row["tstamp"] - # close_position_disequilibrium = close_position_row["disequilibrium"] - # close_position_scaled_disequilibrium = close_position_row["scaled_disequilibrium"] - # close_position_px_a = close_position_row[f"{colname_a}"] - # close_position_px_b = close_position_row[f"{colname_b}"] - - # close_position_side_a = pair.user_data_["close_side_a"] - # close_position_side_b = pair.user_data_["close_side_b"] - - # trd_signal_tuples = [ - # ( - # close_position_tstamp, - # close_position_side_a, - # pair.symbol_a_, - # close_position_px_a, - # close_position_disequilibrium, - # close_position_scaled_disequilibrium, - # pair, - # ), - # ( - # close_position_tstamp, - # close_position_side_b, - # pair.symbol_b_, - # close_position_px_b, - # close_position_disequilibrium, - # close_position_scaled_disequilibrium, - # pair, - # ), - # ] - - # # Add tuples to data frame with explicit dtypes to avoid concatenation warnings - # df = pd.DataFrame( - # trd_signal_tuples, - # columns=self.TRADES_COLUMNS, - # ) - # # Ensure consistent dtypes - # return df.astype({ - # "time": "datetime64[ns]", - # "action": "string", - # "symbol": "string", - # "price": "float64", - # "disequilibrium": "float64", - # "scaled_disequilibrium": "float64", - # "pair": "object" - # }) - def reset(self) -> None: curr_training_start_idx = 0 + diff --git a/lib/pt_trading/trading_pair.py b/lib/pt_trading/trading_pair.py index 3578800..543719c 100644 --- a/lib/pt_trading/trading_pair.py +++ b/lib/pt_trading/trading_pair.py @@ -1,10 +1,11 @@ from __future__ import annotations +from abc import ABC, abstractmethod from enum import Enum from typing import Any, Dict, List, Optional import pandas as pd # type:ignore -from statsmodels.tsa.vector_ar.vecm import VECM, VECMResults + class PairState(Enum): INITIAL = 1 @@ -40,7 +41,7 @@ class CointegrationData: self.johansen_is_cointegrated_ = self.johansen_lr1_ > self.johansen_cvt_ # Run Engle-Granger cointegration test - from statsmodels.tsa.stattools import coint #type: ignore + from statsmodels.tsa.stattools import coint # type: ignore col1, col2 = pair.colnames() assert training_df is not None @@ -68,7 +69,7 @@ class CointegrationData: return f"CointegrationData(tstamp={self.tstamp_}, pair={self.pair_}, eg_pvalue={self.eg_pvalue_}, johansen_lr1={self.johansen_lr1_}, johansen_cvt={self.johansen_cvt_}, eg_is_cointegrated={self.eg_is_cointegrated_}, johansen_is_cointegrated={self.johansen_is_cointegrated_})" -class TradingPair: +class TradingPair(ABC): market_data_: pd.DataFrame symbol_a_: str symbol_b_: str @@ -80,11 +81,9 @@ class TradingPair: training_df_: pd.DataFrame testing_df_: pd.DataFrame - vecm_fit_: VECMResults - user_data_: Dict[str, Any] - predicted_df_: Optional[pd.DataFrame] + # predicted_df_: Optional[pd.DataFrame] def __init__( self, config: Dict[str, Any], market_data: pd.DataFrame, symbol_a: str, symbol_b: str, price_column: str @@ -193,42 +192,6 @@ class TradingPair: f"{self.price_column_}_{self.symbol_b_}", ] - def fit_VECM(self) -> None: - assert self.training_df_ is not None - 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 - # print(f"{self}: beta={self.vecm_fit_.beta} alpha={self.vecm_fit_.alpha}" ) - # print(f"{self}: {self.vecm_fit_.summary()}") - pass - - 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 - 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"] = ( - diseq_series - self.training_mu_ - ) / self.training_std_ - def add_trades(self, trades: pd.DataFrame) -> None: if self.user_data_["trades"] is None or len(self.user_data_["trades"]) == 0: # If trades is empty or None, just assign the new trades directly @@ -267,45 +230,6 @@ class TradingPair: def get_trades(self) -> pd.DataFrame: return self.user_data_["trades"] if "trades" in self.user_data_ else pd.DataFrame() - def predict(self) -> pd.DataFrame: - assert self.testing_df_ is not None - assert self.vecm_fit_ is not None - 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 - ), - left_index=True, - right_index=True, - suffixes=("", "_pred"), - ).dropna() - - predicted_df["disequilibrium"] = ( - predicted_df[self.colnames()] @ self.vecm_fit_.beta - ) - - predicted_df["scaled_disequilibrium"] = ( - abs(predicted_df["disequilibrium"] - self.training_mu_) - / self.training_std_ - ) - - predicted_df = predicted_df.reset_index(drop=True) - if self.predicted_df_ is None: - self.predicted_df_ = predicted_df - else: - self.predicted_df_ = pd.concat([self.predicted_df_, predicted_df], ignore_index=True) - # Reset index to ensure proper indexing - self.predicted_df_ = self.predicted_df_.reset_index(drop=True) - return self.predicted_df_ - def cointegration_check(self) -> Optional[pd.DataFrame]: print(f"***{self}*** STARTING....") config = self.config_ @@ -394,3 +318,10 @@ class TradingPair: def name(self) -> str: return f"{self.symbol_a_} & {self.symbol_b_}" # return f"{self.symbol_a_} & {self.symbol_b_}" + + @abstractmethod + def predict(self) -> pd.DataFrame: ... + + # @abstractmethod + # def predicted_df(self) -> Optional[pd.DataFrame]: ... + diff --git a/lib/pt_trading/vecm_rolling_fit.py b/lib/pt_trading/vecm_rolling_fit.py new file mode 100644 index 0000000..a8d5d2f --- /dev/null +++ b/lib/pt_trading/vecm_rolling_fit.py @@ -0,0 +1,111 @@ +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 +from statsmodels.tsa.vector_ar.vecm import VECM, VECMResults + +NanoPerMin = 1e9 +class VECMTradingPair(TradingPair): + vecm_fit_: Optional[VECMResults] + pair_predict_result_: 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.vecm_fit_ = None + 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 + 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"] = ( + diseq_series - self.training_mu_ + ) / self.training_std_ + + def _fit_VECM(self) -> None: + assert self.training_df_ is not None + 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 + + def predict(self) -> pd.DataFrame: + self._train_pair() + + assert self.testing_df_ is not None + assert self.vecm_fit_ is not None + 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 + ), + left_index=True, + right_index=True, + suffixes=("", "_pred"), + ).dropna() + + predicted_df["disequilibrium"] = ( + predicted_df[self.colnames()] @ self.vecm_fit_.beta + ) + + predicted_df["scaled_disequilibrium"] = ( + abs(predicted_df["disequilibrium"] - self.training_mu_) + / self.training_std_ + ) + + 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 VECMRollingFit(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 VECMTradingPair( + config=config, + market_data=market_data, + symbol_a=symbol_a, + symbol_b=symbol_b, + price_column=price_column + ) diff --git a/research/notebooks/pt_sliding.ipynb b/research/notebooks/pt_sliding.ipynb index 9411563..f8400e6 100644 --- a/research/notebooks/pt_sliding.ipynb +++ b/research/notebooks/pt_sliding.ipynb @@ -1,34 +1,5 @@ { "cells": [ - { - "cell_type": "markdown", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "# Pairs Trading Backtest Notebook\n", - "\n", - "This comprehensive notebook supports both StaticFit and SlidingFit.\n", - "It automatically adapts its analysis and visualization based on the strategy specified in the configuration file.\n", - "\n", - "## Key Features:\n", - "\n", - "1. **Configuration-Driven**: Loads strategy and parameters from HJSON configuration files\n", - "2. **Dual Model Support**: Works with both StaticFit and SlidingFit\n", - "3. **Adaptive Visualization**: Different visualizations based on selected strategy\n", - "4. **Comprehensive Analysis**: Deep analysis of trading pairs and dis-equilibrium\n", - "5. **Interactive Configuration**: Easy parameter adjustment and re-running\n", - "\n", - "## Usage:\n", - "\n", - "1. **Configure Parameters**: Set CONFIG_FILE, SYMBOL_A, SYMBOL_B, and TRADING_DATE\n", - "2. **Run Analysis**: Execute cells step by step\n", - "3. **View Results**: Comprehensive visualizations and trading signals\n", - "4. **Experiment**: Modify parameters and re-run for different scenarios\n" - ] - }, { "cell_type": "markdown", "metadata": { @@ -85,7 +56,7 @@ "# SYMBOL_B = \"ETH-USDT\" # Change this to your desired symbol B\n", "# ================================ C R Y P T O ================================\n", "\n", - "FIT_METHOD_TYPE = \"SlidingFit\"\n", + "FIT_METHOD_TYPE = \"RollingFit\"\n", "TRD_DATE = f\"{TRADING_DATE[0:4]}-{TRADING_DATE[4:6]}-{TRADING_DATE[6:8]}\"\n" ] }, @@ -122,7 +93,7 @@ " from IPython.display import clear_output\n", "\n", " # Import our modules\n", - " from pt_trading.sliding_fit import SlidingFit\n", + " from pt_trading.rolling_window_fit import RollingFit\n", " from pt_trading.trading_pair import TradingPair, PairState\n", " # from pt_trading.results import BacktestResult\n", "\n", @@ -188,8 +159,7 @@ "def instantiate_fit_method_from_config(config: Dict):\n", " \"\"\"Dynamically instantiate strategy from config\"\"\"\n", " fit_method_class_name = config.get(\"fit_method_class\", None)\n", - " if fit_method_class_name is None or fit_method_class_name[-10:] != \"SlidingFit\":\n", - " raise ValueError(f\"Only SidingFit is supported, got {fit_method_class_name}\")\n", + " print(f\"Fit Model: {fit_method_class_name}\")\n", " \n", " try:\n", " # Split module and class name\n", @@ -339,7 +309,7 @@ " print(f\"Time range: {market_data_df['tstamp'].min()} to {market_data_df['tstamp'].max()}\")\n", "\n", " # Create trading pair\n", - " pair = TradingPair(\n", + " pair = FIT_MODEL.create_trading_pair(\n", " config=PT_BT_CONFIG,\n", " market_data=market_data_df,\n", " symbol_a=SYMBOL_A,\n", @@ -383,7 +353,7 @@ "global pair\n", "\n", "def print_strategy_specifics() -> None: # Determine analysis approach based on strategy type\n", - " print(f\"Analysis for SlidingFit ...\")\n", + " print(f\"Analysis for RollingFit ...\")\n", "\n", " print(\"\\n=== SLIDING FIT FIT_MODEL ANALYSIS ===\")\n", " print(\"This strategy:\")\n", @@ -394,7 +364,7 @@ " # Calculate maximum possible iterations for sliding window\n", " training_minutes = PT_BT_CONFIG[\"training_minutes\"]\n", " max_iterations = len(pair.market_data_) - training_minutes\n", - " print(f\"\\nSliding window analysis parameters:\")\n", + " print(f\"\\nRolling window analysis parameters:\")\n", " print(f\" Training window size: {training_minutes} minutes\")\n", " print(f\" Maximum iterations: {max_iterations}\")\n", " print(f\" Total analysis time: ~{max_iterations} minutes\")\n", @@ -523,6 +493,7 @@ " global FIT_MODEL\n", " global bt_result\n", " global pair_trades\n", + " global PREDICTED_RESULT\n", "\n", " import pandas as pd\n", " from pt_trading.results import BacktestResult\n", @@ -553,6 +524,7 @@ " # Run the sliding fit method\n", " # ==========================================================================\n", " pair_trades = FIT_MODEL.run_pair(pair=pair, bt_result=bt_result)\n", + " PREDICTED_RESULT = pair.pair_predict_result_ # TODO make abstract function\n", " # ==========================================================================\n", "\n", " if pair_trades is not None and len(pair_trades) > 0:\n", @@ -567,7 +539,6 @@ " print(\"BACKTEST RESULTS\")\n", " print(\"=\"*80)\n", "\n", - " assert pair.predicted_df_ is not None\n", "\n", "# run_analysis()" ] @@ -593,6 +564,7 @@ " global SYMBOL_A\n", " global SYMBOL_B\n", " global TRD_DATE\n", + " global PREDICTED_RESULT\n", "\n", " import plotly.graph_objects as go\n", " from plotly.subplots import make_subplots\n", @@ -606,14 +578,13 @@ "\n", " # Strategy-specific interactive visualization\n", " assert PT_BT_CONFIG is not None\n", - " assert pair.predicted_df_ is not None\n", "\n", " print(\"=== SLIDING FIT INTERACTIVE VISUALIZATION ===\")\n", - " print(\"Note: Sliding strategy visualization with interactive plotly charts\")\n", + " print(\"Note: Rolling Fit strategy visualization with interactive plotly charts\")\n", "\n", " # Create consistent timeline - superset of timestamps from both dataframes\n", " market_timestamps = set(pair.market_data_['tstamp'])\n", - " predicted_timestamps = set(pair.predicted_df_['tstamp'])\n", + " predicted_timestamps = set(PREDICTED_RESULT['tstamp'])\n", "\n", " # Create superset of all timestamps\n", " all_timestamps = sorted(market_timestamps.union(predicted_timestamps))\n", @@ -622,7 +593,7 @@ " timeline_df = pd.DataFrame({'tstamp': all_timestamps})\n", "\n", " # Merge with predicted data to get dis-equilibrium values\n", - " timeline_df = timeline_df.merge(pair.predicted_df_[['tstamp', 'disequilibrium', 'scaled_disequilibrium']], \n", + " timeline_df = timeline_df.merge(PREDICTED_RESULT[['tstamp', 'disequilibrium', 'scaled_disequilibrium']], \n", " on='tstamp', how='left')\n", "\n", " # Get Symbol_A and Symbol_B market data\n", @@ -926,7 +897,7 @@ " # Update layout\n", " fig.update_layout(\n", " height=1200,\n", - " title_text=f\"Sliding Fit Strategy Analysis - {SYMBOL_A} & {SYMBOL_B} ({TRD_DATE})\",\n", + " title_text=f\"Strategy Analysis - {SYMBOL_A} & {SYMBOL_B} ({TRD_DATE})\",\n", " showlegend=True,\n", " template=\"plotly_white\",\n", " plot_bgcolor='lightgray',\n", @@ -1152,12 +1123,12 @@ " print(f\" Funding per pair: ${PT_BT_CONFIG['funding_per_pair']}\")\n", "\n", " # Strategy-specific summary\n", - " print(f\"\\nSliding Window Analysis:\")\n", + " print(f\"\\nRolling Window Analysis:\")\n", " training_minutes = PT_BT_CONFIG['training_minutes']\n", " max_iterations = len(pair.market_data_) - training_minutes\n", " print(f\" Total data points: {len(pair.market_data_)}\")\n", " print(f\" Maximum iterations: {max_iterations}\")\n", - " print(f\" Analysis type: Dynamic sliding window\")\n", + " print(f\" Analysis type: Dynamic rolling window\")\n", "\n", " # Trading signals summary\n", " if pair_trades is not None and len(pair_trades) > 0:\n", @@ -1300,11 +1271,12 @@ " Data directory: /home/oleg/develop/pairs_trading/data/equity\n", " Database table: md_1min_bars\n", " Exchange: ALPACA\n", - " Training window: 150 minutes\n", + " Training window: 120 minutes\n", " Open threshold: 2\n", " Close threshold: 1\n", + "Fit Model: pt_trading.vecm_rolling_fit.VECMRollingFit\n", "Load configuration SUCCESS\n", - " Fit Method: SlidingFit\n", + " Fit Method: VECMRollingFit\n", "\n", "Data Configuration:\n", " Data File: 20250618.mktdata.ohlcv.db\n", @@ -1472,7 +1444,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Analysis for SlidingFit ...\n", + "Analysis for RollingFit ...\n", "\n", "=== SLIDING FIT FIT_MODEL ANALYSIS ===\n", "This strategy:\n", @@ -1480,15 +1452,15 @@ " - Adapts to changing market conditions\n", " - Dynamic parameter updates every minute\n", "\n", - "Sliding window analysis parameters:\n", - " Training window size: 150 minutes\n", - " Maximum iterations: 211\n", - " Total analysis time: ~211 minutes\n", + "Rolling window analysis parameters:\n", + " Training window size: 120 minutes\n", + " Maximum iterations: 241\n", + " Total analysis time: ~241 minutes\n", "\n", "Strategy Configuration:\n", " Open threshold: 2\n", " Close threshold: 1\n", - " Training minutes: 150\n", + " Training minutes: 120\n", " Funding per pair: $2000\n" ] }, @@ -1522,31 +1494,22 @@ " MSTR: Mean=$370.87, Std=$1.52\n", " Price Ratio: Mean=0.75, Std=0.04\n", " Correlation: -0.0929\n", - "Running SlidingFit analysis...\n", + "Running RollingFit analysis...\n", "\n", "=== SLIDING FIT ANALYSIS ===\n", "Processing first 200 iterations for demonstration...\n", "***COIN & MSTR*** STARTING....\n", - "OPEN_TRADES: 2025-06-18 16:00:00 open_scaled_disequilibrium=np.float64(2.8605299147981675)\n", + "OPEN_TRADES: 2025-06-18 15:30:00 open_scaled_disequilibrium=np.float64(4.435087777514003)\n", "OPEN TRADES:\n", " time action symbol price disequilibrium scaled_disequilibrium pair status\n", - "0 2025-06-18 16:00:00 SELL COIN 278.5026 26.172385 2.86053 COIN & MSTR OPEN\n", - "1 2025-06-18 16:00:00 BUY MSTR 372.2725 26.172385 2.86053 COIN & MSTR OPEN\n", - "CLOSE TRADES:\n", - " time action symbol price disequilibrium scaled_disequilibrium pair status\n", - "0 2025-06-18 16:59:00 BUY COIN 285.70 -153123.39816 0.369634 COIN & MSTR CLOSE\n", - "1 2025-06-18 16:59:00 SELL MSTR 371.35 -153123.39816 0.369634 COIN & MSTR CLOSE\n", - "OPEN_TRADES: 2025-06-18 17:07:00 open_scaled_disequilibrium=np.float64(2.1726379189346643)\n", - "OPEN TRADES:\n", - " time action symbol price disequilibrium scaled_disequilibrium pair status\n", - "0 2025-06-18 17:07:00 BUY COIN 288.749 -885.043609 2.172638 COIN & MSTR OPEN\n", - "1 2025-06-18 17:07:00 SELL MSTR 370.660 -885.043609 2.172638 COIN & MSTR OPEN\n", - "CLOSE TRADES:\n", - " time action symbol price disequilibrium scaled_disequilibrium pair status\n", - "0 2025-06-18 17:45:00 SELL COIN 288.3 -1.436136 0.950544 COIN & MSTR CLOSE\n", - "1 2025-06-18 17:45:00 BUY MSTR 369.6 -1.436136 0.950544 COIN & MSTR CLOSE\n", - "***COIN & MSTR*** FINISHED ... 8\n", - "Generated 8 trading signals\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", + "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", + "***COIN & MSTR*** FINISHED *** Num Trades:4\n", + "Generated 4 trading signals\n", "\n", "Strategy execution completed!\n", "\n", @@ -1574,23 +1537,19 @@ "output_type": "stream", "text": [ "=== SLIDING FIT INTERACTIVE VISUALIZATION ===\n", - "Note: Sliding strategy visualization with interactive plotly charts\n", + "Note: Rolling Fit strategy visualization with interactive plotly charts\n", "Using consistent timeline with 361 timestamps\n", "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 16:00:00 SELL COIN 278.5026 26.172385 2.860530 COIN & MSTR OPEN\n", - "2 2025-06-18 16:59:00 BUY COIN 285.7000 -153123.398160 0.369634 COIN & MSTR CLOSE\n", - "4 2025-06-18 17:07:00 BUY COIN 288.7490 -885.043609 2.172638 COIN & MSTR OPEN\n", - "6 2025-06-18 17:45:00 SELL COIN 288.3000 -1.436136 0.950544 COIN & MSTR CLOSE\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", "\n", "Symbol_B trades:\n", - " time action symbol price disequilibrium scaled_disequilibrium pair status\n", - "1 2025-06-18 16:00:00 BUY MSTR 372.2725 26.172385 2.860530 COIN & MSTR OPEN\n", - "3 2025-06-18 16:59:00 SELL MSTR 371.3500 -153123.398160 0.369634 COIN & MSTR CLOSE\n", - "5 2025-06-18 17:07:00 SELL MSTR 370.6600 -885.043609 2.172638 COIN & MSTR OPEN\n", - "7 2025-06-18 17:45:00 BUY MSTR 369.6000 -1.436136 0.950544 COIN & MSTR CLOSE\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" ] }, { @@ -1975,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+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/dD99poe9EUBMd6nJ+ncNQLqCt1dKJA5Au6YUM5IqEEAeqweAmG4SQFC2XznZCxNAceGpp0ArEUAYVEwlVRUSQP6sAT3YGQtA9v3p1Y3yCkBt2DPiD4YKQCN43oLPDwpAvGF+gEewCEAHZY4s+bMJQMyovMji3glA9E1quqyECUD+Codkt5wIQDjG4hRFjApAQBHnL9hzCkB9v7R8GDYLQEzabBUPUQpALLO28lqlCUC/dRazzZkIQLbrxAfaJwpAbbpfLymEC0A5geAu68oHQPMEs1nJZAlArFPR1hC3B0AIZcyQBh4GQBm5NsN7+gRAz+zSBYvNBEC7nj/mYBsFQM77+sI4tgRARxIgqXFYA0BWTaYsPxsEQHLSsToJKQJAxM0Ie2qvAUCgdmbbpmUCQBdxl4mpFAJAalVTMi7PAkB1xbNt8dwCQJGq40CUvgJApmkYQgOKAkD48NRTIgACQLXaCehw7QFA69mhJC3lAUBtGKnRjTsBQF1F/OVcRf4/3xOATuqj/T/jV9XSIrT8P5dXUQucS/0/NBV5xrYJ/j+AR6apBln7P5HA3K81vvo/uBBJvhu++T/35RjRqo74PwOr7OApqvY/LfyXi76s9j8p0MIxSZ71P9LKndUmn/M/3PhREJoH9D9ktJOzsEvzP5CrGlQq6/I/SW58Tiys9D+RLU/vkkb1P6FqmC6fA/c/cmWMmsXt9T8bNBMH5mj1PxkI/tklBvU/pmJhPs1R9z9eGJN1eCb1P15vYw+l4PU/ChaG/VeL9z8YM4qUii35P7Ym3jUntPg/Acmctx+Q9j+jHdv0XZH3P6J/Q8VdO/Y/nJDEYfvN9T/FK4mb5Tv0P8MJAtZL0/Q/Rje8t/w39T/fOsmZLyb0P7s7H47ZN/U/WEGI395C9D/UfIp3rUb0P7CHQ2uPifQ/mpXLrzep9D8BDI5bKXXzP19fJa5gYPM/pgzYYNjT8z+Iri8djanzP6x5xFu4afE/3eMBDgYI8z8oi8rNFVLzPwY+fI9gKvM/886SQqEH9T89VUor+Qr3P5bVVOGb6fY/OKxdPfsA+D+2FjCKlm32P6jIM8tI8PY/ejI2ZmAu9j+h7EUF5yT2P2Z+5ag2Xvc/44FEDoiK+T9fhhiTwY/6PymkOYJZzvg/3RjTPe6d+T+QqN3RFK/5P7q9cogGq/k/po+19s1J+D8fHsEEgMX3PwIZ7n4xxPc/tEs2GdZx9z/tB6ohxbL2PyUPr5I3QPc/AsXRJHBU9T/QDWL9uZH1Pyi+2mnO0fM/0lCheKJu9T8AAAAAAAD4fwAAAAAAAPh/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+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8=", "dtype": "f8" }, "yaxis": "y" @@ -1990,17 +1949,11 @@ "name": "OPEN", "type": "scatter", "x": [ - "2025-06-18T16:00:00.000000000", - "2025-06-18T16:00:00.000000000", - "2025-06-18T17:07:00.000000000", - "2025-06-18T17:07:00.000000000" + "2025-06-18T15:30:00.000000000", + "2025-06-18T15:30:00.000000000" ], "xaxis": "x2", "y": [ - 0, - 0, - 0, - 0, 0, 0, 0, @@ -2008,34 +1961,6 @@ ], "yaxis": "y2" }, - { - "marker": { - "color": "red", - "size": 10, - "symbol": "triangle-down" - }, - "mode": "markers", - "name": "CLOSE", - "type": "scatter", - "x": [ - "2025-06-18T16:59:00.000000000", - "2025-06-18T16:59:00.000000000", - "2025-06-18T17:45:00.000000000", - "2025-06-18T17:45:00.000000000" - ], - "xaxis": "x2", - "y": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ], - "yaxis": "y2" - }, { "line": { "color": "blue", @@ -2414,26 +2339,6 @@ }, "yaxis": "y3" }, - { - "marker": { - "color": "green", - "size": 12, - "symbol": "triangle-up" - }, - "mode": "markers", - "name": "COIN BUY OPEN", - "showlegend": true, - "type": "scatter", - "x": [ - "2025-06-18T17:07:00.000000000" - ], - "xaxis": "x3", - "y": { - "bdata": "RIts5/sLckA=", - "dtype": "f8" - }, - "yaxis": "y3" - }, { "marker": { "color": "green", @@ -2445,11 +2350,11 @@ "showlegend": true, "type": "scatter", "x": [ - "2025-06-18T16:59:00.000000000" + "2025-06-18T15:34:00.000000000" ], "xaxis": "x3", "y": { - "bdata": "MzMzMzPbcUA=", + "bdata": "7FG4HoXHcEA=", "dtype": "f8" }, "yaxis": "y3" @@ -2465,31 +2370,11 @@ "showlegend": true, "type": "scatter", "x": [ - "2025-06-18T16:00:00.000000000" + "2025-06-18T15:30:00.000000000" ], "xaxis": "x3", "y": { - "bdata": "gy9MpgpocUA=", - "dtype": "f8" - }, - "yaxis": "y3" - }, - { - "marker": { - "color": "red", - "size": 12, - "symbol": "triangle-down" - }, - "mode": "markers", - "name": "COIN SELL CLOSE", - "showlegend": true, - "type": "scatter", - "x": [ - "2025-06-18T17:45:00.000000000" - ], - "xaxis": "x3", - "y": { - "bdata": "zczMzMwEckA=", + "bdata": "cT0K16OccEA=", "dtype": "f8" }, "yaxis": "y3" @@ -2883,51 +2768,11 @@ "showlegend": true, "type": "scatter", "x": [ - "2025-06-18T16:00:00.000000000" + "2025-06-18T15:30:00.000000000" ], "xaxis": "x4", "y": { - "bdata": "j8L1KFxEd0A=", - "dtype": "f8" - }, - "yaxis": "y4" - }, - { - "marker": { - "color": "darkgreen", - "size": 12, - "symbol": "triangle-up" - }, - "mode": "markers", - "name": "MSTR BUY CLOSE", - "showlegend": true, - "type": "scatter", - "x": [ - "2025-06-18T17:45:00.000000000" - ], - "xaxis": "x4", - "y": { - "bdata": "mpmZmZkZd0A=", - "dtype": "f8" - }, - "yaxis": "y4" - }, - { - "marker": { - "color": "darkred", - "size": 12, - "symbol": "triangle-down" - }, - "mode": "markers", - "name": "MSTR SELL OPEN", - "showlegend": true, - "type": "scatter", - "x": [ - "2025-06-18T17:07:00.000000000" - ], - "xaxis": "x4", - "y": { - "bdata": "w/UoXI8qd0A=", + "bdata": "fdCzWfVOd0A=", "dtype": "f8" }, "yaxis": "y4" @@ -2943,11 +2788,11 @@ "showlegend": true, "type": "scatter", "x": [ - "2025-06-18T16:59:00.000000000" + "2025-06-18T15:34:00.000000000" ], "xaxis": "x4", "y": { - "bdata": "mpmZmZk1d0A=", + "bdata": "4XoUrkdRd0A=", "dtype": "f8" }, "yaxis": "y4" @@ -3865,7 +3710,7 @@ } }, "title": { - "text": "Sliding Fit Strategy Analysis - COIN & MSTR (2025-06-18)" + "text": "Strategy Analysis - COIN & MSTR (2025-06-18)" }, "xaxis": { "anchor": "y", @@ -3958,9 +3803,9 @@ }, "text/html": [ "
\n", - "
\n", - "