From e30b0df4dbc0d168864c3e4aeecb1e57169becde Mon Sep 17 00:00:00 2001 From: Oleg Sheynin Date: Thu, 24 Jul 2025 06:51:46 +0000 Subject: [PATCH] progress and result.py fixes --- configuration/zscore.cfg | 10 +- lib/pt_trading/__DELETE__/static_fit.py | 212 ------- lib/pt_trading/results.py | 595 ++++++++---------- lib/tools/data_loader.py | 132 ++-- research/cointegration_test.py | 2 +- research/notebooks/single_pair_test.ipynb | 312 ++++----- research/pt_backtest.py | 112 ++-- research/research_tools.py | 56 +- .../equity/20250714_003409.equity_results.db | Bin 28672 -> 0 bytes strategy/pair_strategy.py | 11 +- 10 files changed, 587 insertions(+), 855 deletions(-) delete mode 100644 lib/pt_trading/__DELETE__/static_fit.py delete mode 100644 researchresults/equity/20250714_003409.equity_results.db diff --git a/configuration/zscore.cfg b/configuration/zscore.cfg index b05cefe..65615d9 100644 --- a/configuration/zscore.cfg +++ b/configuration/zscore.cfg @@ -1,21 +1,13 @@ { - "instrument_type_specifics": { + "market_data_loading": { "CRYPTO": { "data_directory": "./data/crypto", - "datafiles": [ - "20250602.mktdata.ohlcv.db" - ], "db_table_name": "md_1min_bars", - "exchange_id": "BNBSPOT", "instrument_id_pfx": "PAIR-", }, "EQUITY": { "data_directory": "./data/equity", - "datafiles": [ - "20250602.mktdata.ohlcv.db" - ], "db_table_name": "md_1min_bars", - "exchange_id": "BNBSPOT", "instrument_id_pfx": "STOCK-", } }, diff --git a/lib/pt_trading/__DELETE__/static_fit.py b/lib/pt_trading/__DELETE__/static_fit.py deleted file mode 100644 index 12bf3a7..0000000 --- a/lib/pt_trading/__DELETE__/static_fit.py +++ /dev/null @@ -1,212 +0,0 @@ -from abc import ABC, abstractmethod -from enum import Enum -from typing import Dict, Optional, cast - -import pandas as pd # type: ignore[import] -from pt_trading.results import BacktestResult -from pt_trading.trading_pair import TradingPair -from pt_trading.fit_method import PairsTradingFitMethod - -NanoPerMin = 1e9 - - - -class StaticFit(PairsTradingFitMethod): - - def run_pair( - self, pair: TradingPair, bt_result: BacktestResult - ) -> Optional[pd.DataFrame]: # abstractmethod - config = pair.config_ - pair.get_datasets(training_minutes=config["training_minutes"]) - - try: - pair.predict() - except Exception as e: - print(f"{pair}: Prediction failed: {str(e)}") - return None - - pair_trades = self.create_trading_signals( - pair=pair, config=config, result=bt_result - ) - - return pair_trades - - def create_trading_signals( - self, pair: TradingPair, config: Dict, result: BacktestResult - ) -> pd.DataFrame: - beta = pair.vecm_fit_.beta # type: ignore - colname_a, colname_b = pair.colnames() - - predicted_df = pair.predicted_df_ - if predicted_df is None: - # Return empty DataFrame with correct columns and dtypes - return pd.DataFrame(columns=self.TRADES_COLUMNS).astype({ - "time": "datetime64[ns]", - "action": "string", - "symbol": "string", - "price": "float64", - "disequilibrium": "float64", - "scaled_disequilibrium": "float64", - "pair": "object" - }) - - open_threshold = config["dis-equilibrium_open_trshld"] - close_threshold = config["dis-equilibrium_close_trshld"] - - # Iterate through the testing dataset to find the first trading opportunity - open_row_index = None - for row_idx in range(len(predicted_df)): - curr_disequilibrium = predicted_df["scaled_disequilibrium"][row_idx] - - # Check if current row has sufficient disequilibrium (not near-zero) - if curr_disequilibrium >= open_threshold: - open_row_index = row_idx - break - - # If no row with sufficient disequilibrium found, skip this pair - if open_row_index is None: - print(f"{pair}: Insufficient disequilibrium in testing dataset. Skipping.") - return pd.DataFrame() - - # Look for close signal starting from the open position - trading_signals_df = ( - predicted_df["scaled_disequilibrium"][open_row_index:] < close_threshold - ) - - # Adjust indices to account for the offset from open_row_index - close_row_index = None - for idx, value in trading_signals_df.items(): - if value: - close_row_index = idx - break - - open_row = predicted_df.loc[open_row_index] - open_px_a = predicted_df.at[open_row_index, f"{colname_a}"] - open_px_b = predicted_df.at[open_row_index, f"{colname_b}"] - open_tstamp = predicted_df.at[open_row_index, "tstamp"] - open_disequilibrium = open_row["disequilibrium"] - open_scaled_disequilibrium = open_row["scaled_disequilibrium"] - - abs_beta = abs(beta[1]) - pred_px_b = predicted_df.loc[open_row_index][f"{colname_b}_pred"] - pred_px_a = predicted_df.loc[open_row_index][f"{colname_a}_pred"] - - if pred_px_b * abs_beta - pred_px_a > 0: - open_side_a = "BUY" - open_side_b = "SELL" - close_side_a = "SELL" - close_side_b = "BUY" - else: - open_side_b = "BUY" - open_side_a = "SELL" - close_side_b = "SELL" - close_side_a = "BUY" - - # If no close signal found, print position and unrealized PnL - if close_row_index is None: - - last_row_index = len(predicted_df) - 1 - - # Use the new method from BacktestResult to handle outstanding positions - result.handle_outstanding_position( - pair=pair, - pair_result_df=predicted_df, - last_row_index=last_row_index, - open_side_a=open_side_a, - open_side_b=open_side_b, - open_px_a=float(open_px_a), - open_px_b=float(open_px_b), - open_tstamp=pd.Timestamp(open_tstamp), - ) - - # Return only open trades (no close trades) - trd_signal_tuples = [ - ( - open_tstamp, - open_side_a, - pair.symbol_a_, - open_px_a, - open_disequilibrium, - open_scaled_disequilibrium, - pair, - ), - ( - open_tstamp, - open_side_b, - pair.symbol_b_, - open_px_b, - open_disequilibrium, - open_scaled_disequilibrium, - pair, - ), - ] - else: - # Close signal found - create complete trade - close_row = predicted_df.loc[close_row_index] - close_tstamp = close_row["tstamp"] - close_disequilibrium = close_row["disequilibrium"] - close_scaled_disequilibrium = close_row["scaled_disequilibrium"] - close_px_a = close_row[f"{colname_a}"] - close_px_b = close_row[f"{colname_b}"] - - print(f"{pair}: Close signal found at index {close_row_index}") - - trd_signal_tuples = [ - ( - open_tstamp, - open_side_a, - pair.symbol_a_, - open_px_a, - open_disequilibrium, - open_scaled_disequilibrium, - pair, - ), - ( - open_tstamp, - open_side_b, - pair.symbol_b_, - open_px_b, - open_disequilibrium, - open_scaled_disequilibrium, - pair, - ), - ( - close_tstamp, - close_side_a, - pair.symbol_a_, - close_px_a, - close_disequilibrium, - close_scaled_disequilibrium, - pair, - ), - ( - close_tstamp, - close_side_b, - pair.symbol_b_, - close_px_b, - close_disequilibrium, - close_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: - pass - - diff --git a/lib/pt_trading/results.py b/lib/pt_trading/results.py index 29c1a43..0f19263 100644 --- a/lib/pt_trading/results.py +++ b/lib/pt_trading/results.py @@ -46,7 +46,7 @@ def create_result_database(db_path: str) -> None: if db_dir and not os.path.exists(db_dir): os.makedirs(db_dir, exist_ok=True) print(f"Created directory: {db_dir}") - + conn = sqlite3.connect(db_path) cursor = conn.cursor() @@ -68,7 +68,8 @@ def create_result_database(db_path: str) -> None: close_quantity INTEGER, close_disequilibrium REAL, symbol_return REAL, - pair_return REAL + pair_return REAL, + close_condition TEXT ) """ ) @@ -121,7 +122,7 @@ def store_config_in_database( config: Dict, fit_method_class: str, datafiles: List[str], - instruments: List[str], + instruments: List[Dict[str, str]], ) -> None: """ Store configuration information in the database for reference. @@ -140,7 +141,12 @@ def store_config_in_database( # Convert lists to comma-separated strings for storage datafiles_str = ", ".join(datafiles) - instruments_str = ", ".join(instruments) + instruments_str = ", ".join( + [ + f"{inst['symbol']}:{inst['instrument_type']}:{inst['exchange_id']}" + for inst in instruments + ] + ) # Insert configuration record cursor.execute( @@ -170,6 +176,7 @@ def store_config_in_database( traceback.print_exc() + def convert_timestamp(timestamp: Any) -> Optional[datetime]: """Convert pandas Timestamp to Python datetime object for SQLite compatibility.""" if timestamp is None: @@ -187,244 +194,6 @@ def convert_timestamp(timestamp: Any) -> Optional[datetime]: else: raise ValueError(f"Unsupported timestamp type: {type(timestamp)}") - -def store_results_in_database( - db_path: str, datafile: str, bt_result: "BacktestResult" -) -> None: - """ - Store backtest results in the SQLite database. - """ - if db_path.upper() == "NONE": - return - - try: - # Extract date from datafile name (assuming format like 20250528.mktdata.ohlcv.db) - filename = os.path.basename(datafile) - date_str = filename.split(".")[0] # Extract date part - - # Convert to proper date format - try: - date_obj = datetime.strptime(date_str, "%Y%m%d").date() - except ValueError: - # If date parsing fails, use current date - date_obj = datetime.now().date() - - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - # Process each trade from bt_result - trades = bt_result.get_trades() - - for pair_name, symbols in trades.items(): - # Calculate pair return for this pair - pair_return = 0.0 - pair_trades = [] - - # First pass: collect all trades and calculate returns - for symbol, symbol_trades in symbols.items(): - if len(symbol_trades) == 0: # No trades for this symbol - print( - f"Warning: No trades found for symbol {symbol} in pair {pair_name}" - ) - continue - - elif len(symbol_trades) >= 2: # Completed trades (entry + exit) - # Handle both old and new tuple formats - if len(symbol_trades[0]) == 2: # Old format: (action, price) - entry_action, entry_price = symbol_trades[0] - exit_action, exit_price = symbol_trades[1] - open_disequilibrium = 0.0 # Fallback for old format - open_scaled_disequilibrium = 0.0 - close_disequilibrium = 0.0 - close_scaled_disequilibrium = 0.0 - open_time = datetime.now() - close_time = datetime.now() - else: # New format: (action, price, disequilibrium, scaled_disequilibrium, timestamp) - ( - entry_action, - entry_price, - open_disequilibrium, - open_scaled_disequilibrium, - open_time, - ) = symbol_trades[0] - ( - exit_action, - exit_price, - close_disequilibrium, - close_scaled_disequilibrium, - close_time, - ) = symbol_trades[1] - - # Handle None values - open_disequilibrium = ( - open_disequilibrium - if open_disequilibrium is not None - else 0.0 - ) - open_scaled_disequilibrium = ( - open_scaled_disequilibrium - if open_scaled_disequilibrium is not None - else 0.0 - ) - close_disequilibrium = ( - close_disequilibrium - if close_disequilibrium is not None - else 0.0 - ) - close_scaled_disequilibrium = ( - close_scaled_disequilibrium - if close_scaled_disequilibrium is not None - else 0.0 - ) - - # Convert pandas Timestamps to Python datetime objects - open_time = convert_timestamp(open_time) or datetime.now() - close_time = convert_timestamp(close_time) or datetime.now() - - # Calculate actual share quantities based on funding per pair - # Split funding equally between the two positions - funding_per_position = bt_result.config["funding_per_pair"] / 2 - shares = funding_per_position / entry_price - - # Calculate symbol return - symbol_return = 0.0 - if entry_action == "BUY" and exit_action == "SELL": - symbol_return = (exit_price - entry_price) / entry_price * 100 - elif entry_action == "SELL" and exit_action == "BUY": - symbol_return = (entry_price - exit_price) / entry_price * 100 - - pair_return += symbol_return - - pair_trades.append( - { - "symbol": symbol, - "entry_action": entry_action, - "entry_price": entry_price, - "exit_action": exit_action, - "exit_price": exit_price, - "symbol_return": symbol_return, - "open_disequilibrium": open_disequilibrium, - "open_scaled_disequilibrium": open_scaled_disequilibrium, - "close_disequilibrium": close_disequilibrium, - "close_scaled_disequilibrium": close_scaled_disequilibrium, - "open_time": open_time, - "close_time": close_time, - "shares": shares, - "is_completed": True, - } - ) - - # Skip one-sided trades - they will be handled by outstanding_positions table - elif len(symbol_trades) == 1: - print( - f"Skipping one-sided trade for {symbol} in pair {pair_name} - will be stored in outstanding_positions table" - ) - continue - - else: - # This should not happen, but handle unexpected cases - print( - f"Warning: Unexpected number of trades ({len(symbol_trades)}) for symbol {symbol} in pair {pair_name}" - ) - continue - - # Second pass: insert completed trade records into database - for trade in pair_trades: - # Only store completed trades in pt_bt_results table - cursor.execute( - """ - INSERT INTO pt_bt_results ( - date, pair, symbol, open_time, open_side, open_price, - open_quantity, open_disequilibrium, close_time, close_side, - close_price, close_quantity, close_disequilibrium, - symbol_return, pair_return - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - date_obj, - pair_name, - trade["symbol"], - trade["open_time"], - trade["entry_action"], - trade["entry_price"], - trade["shares"], - trade["open_scaled_disequilibrium"], - trade["close_time"], - trade["exit_action"], - trade["exit_price"], - trade["shares"], - trade["close_scaled_disequilibrium"], - trade["symbol_return"], - pair_return, - ), - ) - - # Store outstanding positions in separate table - outstanding_positions = bt_result.get_outstanding_positions() - for pos in outstanding_positions: - # Calculate position quantity (negative for SELL positions) - position_qty_a = ( - pos["shares_a"] if pos["side_a"] == "BUY" else -pos["shares_a"] - ) - position_qty_b = ( - pos["shares_b"] if pos["side_b"] == "BUY" else -pos["shares_b"] - ) - - # Calculate unrealized returns - # For symbol A: (current_price - open_price) / open_price * 100 * position_direction - unrealized_return_a = ( - (pos["current_px_a"] - pos["open_px_a"]) / pos["open_px_a"] * 100 - ) * (1 if pos["side_a"] == "BUY" else -1) - unrealized_return_b = ( - (pos["current_px_b"] - pos["open_px_b"]) / pos["open_px_b"] * 100 - ) * (1 if pos["side_b"] == "BUY" else -1) - - # Store outstanding position for symbol A - cursor.execute( - """ - INSERT INTO outstanding_positions ( - date, pair, symbol, position_quantity, last_price, unrealized_return, open_price, open_side - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - date_obj, - pos["pair"], - pos["symbol_a"], - position_qty_a, - pos["current_px_a"], - unrealized_return_a, - pos["open_px_a"], - pos["side_a"], - ), - ) - - # Store outstanding position for symbol B - cursor.execute( - """ - INSERT INTO outstanding_positions ( - date, pair, symbol, position_quantity, last_price, unrealized_return, open_price, open_side - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - date_obj, - pos["pair"], - pos["symbol_b"], - position_qty_b, - pos["current_px_b"], - unrealized_return_b, - pos["open_px_b"], - pos["side_b"], - ), - ) - - conn.commit() - conn.close() - - except Exception as e: - print(f"Error storing results in database: {str(e)}") - import traceback - - traceback.print_exc() class BacktestResult: @@ -437,7 +206,8 @@ class BacktestResult: self.trades: Dict[str, Dict[str, Any]] = {} self.total_realized_pnl = 0.0 self.outstanding_positions: List[Dict[str, Any]] = [] - + self.pairs_trades_: Dict[str, List[Dict[str, Any]]] = {} + def add_trade( self, pair_nm: str, @@ -458,15 +228,16 @@ class BacktestResult: if symbol not in self.trades[pair_nm]: self.trades[pair_nm][symbol] = [] self.trades[pair_nm][symbol].append( - {"symbol":symbol, - "side":side, - "action":action, - "price":price, - "disequilibrium":disequilibrium, - "scaled_disequilibrium":scaled_disequilibrium, - "timestamp":timestamp, - "status":status - } + { + "symbol": symbol, + "side": side, + "action": action, + "price": price, + "disequilibrium": disequilibrium, + "scaled_disequilibrium": scaled_disequilibrium, + "timestamp": timestamp, + "status": status, + } ) def add_outstanding_position(self, position: Dict[str, Any]) -> None: @@ -549,97 +320,126 @@ class BacktestResult: def calculate_returns(self, all_results: Dict[str, Dict[str, Any]]) -> None: """Calculate and print returns by day and pair.""" + def _symbol_return(trade1_side: str, trade1_px: float, trade2_side: str, trade2_px: float) -> float: + if trade1_side == "BUY" and trade2_side == "SELL": + return (trade2_px - trade1_px) / trade1_px * 100 + elif trade1_side == "SELL" and trade2_side == "BUY": + return (trade1_px - trade2_px) / trade1_px * 100 + else: + return 0 + print("\n====== Returns By Day and Pair ======") + trades = [] for filename, data in all_results.items(): - day_return = 0 + pairs = list(data["trades"].keys()) + for pair in pairs: + self.pairs_trades_[pair] = [] + trades_dict = data["trades"][pair] + for symbol in trades_dict.keys(): + trades.extend(trades_dict[symbol]) + trades = sorted(trades, key=lambda x: (x["timestamp"], x["symbol"])) + print(f"\n--- {filename} ---") self.outstanding_positions = data["outstanding_positions"] + + day_return = 0.0 + for idx in range(0, len(trades), 4): + symbol_a = trades[idx]["symbol"] + trade_a_1 = trades[idx] + trade_a_2 = trades[idx + 2] - # Process each pair - for pair, symbols in data["trades"].items(): - pair_return = 0 - pair_trades = [] + symbol_b = trades[idx + 1]["symbol"] + trade_b_1 = trades[idx + 1] + trade_b_2 = trades[idx + 3] - # Calculate individual symbol returns in the pair - for symbol, trades in symbols.items(): - if len(trades) == 0: - continue - symbol_return = 0 - symbol_trades = [trade for trade in trades if trade["symbol"] == symbol] + symbol_return = 0 + assert ( + trade_a_1["timestamp"] < trade_a_2["timestamp"] + ), f"Trade 1: {trade_a_1['timestamp']} is not less than Trade 2: {trade_a_2['timestamp']}" + assert ( + trade_a_1["action"] == "OPEN" and trade_a_2["action"] == "CLOSE" + ), f"Trade 1: {trade_a_1['action']} and Trade 2: {trade_a_2['action']} are the same" - # Calculate returns for all trade combinations - for idx in range(0, len(symbol_trades), 2): - trade1 = trades[idx] - trade2 = trades[idx + 1] - - assert trade1["timestamp"] < trade2["timestamp"], f"Trade 1: {trade1['timestamp']} is not less than Trade 2: {trade2['timestamp']}" - assert trade1["action"] == "OPEN" and trade2["action"] == "CLOSE", f"Trade 1: {trade1['action']} and Trade 2: {trade2['action']} are the same" - - # Calculate return based on action combination - trade_return = 0 - if trade1["side"] == "BUY" and trade2["side"] == "SELL": - # Long position - trade_return = (trade2["price"] - trade1["price"]) / trade1["price"] * 100 - elif trade1["side"] == "SELL" and trade2["side"] == "BUY": - # Short position - trade_return = (trade1["price"] - trade2["price"]) / trade1["price"] * 100 - - symbol_return += trade_return - - # Store trade details for reporting - pair_trades.append( - ( - symbol, - trade1["timestamp"], - trade2["timestamp"], - trade1["side"], - trade1["price"], - trade2["side"], - trade2["price"], - trade_return, - trade1["scaled_disequilibrium"], - trade2["scaled_disequilibrium"], - f"{idx + 1}", # Trade sequence number - ) - ) - - pair_return += symbol_return + # Calculate return based on action combination + trade_return = 0 + symbol_a_return = _symbol_return(trade_a_1["side"], trade_a_1["price"], trade_a_2["side"], trade_a_2["price"]) + symbol_b_return = _symbol_return(trade_b_1["side"], trade_b_1["price"], trade_b_2["side"], trade_b_2["price"]) - # Print pair returns with disequilibrium information - if pair_trades: - print(f" {pair}:") - for ( - symbol, - trade1["timestamp"], - trade2["timestamp"], - trade1["side"], - trade1["price"], - trade2["side"], - trade2["price"], - trade_return, - trade1["scaled_disequilibrium"], - trade2["scaled_disequilibrium"], - trade_num, - ) in pair_trades: - disequil_info = "" - if ( - trade1["scaled_disequilibrium"] is not None - and trade2["scaled_disequilibrium"] is not None - ): - disequil_info = f" | Open Dis-eq: {trade1["scaled_disequilibrium"]:.2f}," - f" Close Dis-eq: {trade2["scaled_disequilibrium"]:.2f}" + pair_return = symbol_a_return + symbol_b_return - print( - f" {trade2['timestamp'].time()} {symbol} (Trade #{trade_num}):" - f" {trade1["side"]} @ ${trade1["price"]:.2f}," - f" {trade2["side"]} @ ${trade2["price"]:.2f}," - f" Return: {trade_return:.2f}%{disequil_info}" - ) - print(f" Pair Total Return: {pair_return:.2f}%") - day_return += pair_return - + self.pairs_trades_[pair].append( + { + "symbol": symbol_a, + "open_side": trade_a_1["side"], + "open_action": trade_a_1["action"], + "open_price": trade_a_1["price"], + "close_side": trade_a_2["side"], + "close_action": trade_a_2["action"], + "close_price": trade_a_2["price"], + "symbol_return": symbol_a_return, + "open_disequilibrium": trade_a_1["disequilibrium"], + "open_scaled_disequilibrium": trade_a_1["scaled_disequilibrium"], + "close_disequilibrium": trade_a_2["disequilibrium"], + "close_scaled_disequilibrium": trade_a_2["scaled_disequilibrium"], + "open_time": trade_a_1["timestamp"], + "close_time": trade_a_2["timestamp"], + "shares": self.config["funding_per_pair"] / 2 / trade_a_1["price"], + "is_completed": True, + "close_condition": trade_a_2["status"], + "pair_return": pair_return + } + ) + self.pairs_trades_[pair].append( + { + "symbol": symbol_b, + "open_side": trade_b_1["side"], + "open_action": trade_b_1["action"], + "open_price": trade_b_1["price"], + "close_side": trade_b_2["side"], + "close_action": trade_b_2["action"], + "close_price": trade_b_2["price"], + "symbol_return": symbol_b_return, + "open_disequilibrium": trade_b_1["disequilibrium"], + "open_scaled_disequilibrium": trade_b_1["scaled_disequilibrium"], + "close_disequilibrium": trade_b_2["disequilibrium"], + "close_scaled_disequilibrium": trade_b_2["scaled_disequilibrium"], + "open_time": trade_b_1["timestamp"], + "close_time": trade_b_2["timestamp"], + "shares": self.config["funding_per_pair"] / 2 / trade_b_1["price"], + "is_completed": True, + "close_condition": trade_b_2["status"], + "pair_return": pair_return + } + ) + + + # Print pair returns with disequilibrium information + day_return = 0.0 + if self.pairs_trades_[pair]: + + print(f"{pair}:") + pair_return = 0.0 + for trd in self.pairs_trades_[pair]: + disequil_info = "" + if ( + 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}" + + print( + f" {trd['open_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"] + + print(f" Pair Total Return: {pair_return:.2f}%") + day_return += pair_return # Print day total return and add to global realized PnL if day_return != 0: @@ -716,7 +516,7 @@ class BacktestResult: print("-" * 100) - total_value += pos["total_current_value"] + total_value += pos["total_current_value"] print(f"{'TOTAL OUTSTANDING VALUE':<80} ${total_value:<12.2f}") @@ -811,3 +611,132 @@ class BacktestResult: ) return current_value_a, current_value_b, total_current_value + + def store_results_in_database( + self, db_path: str, datafile: str + ) -> None: + """ + Store backtest results in the SQLite database. + """ + if db_path.upper() == "NONE": + return + + try: + # Extract date from datafile name (assuming format like 20250528.mktdata.ohlcv.db) + filename = os.path.basename(datafile) + date_str = filename.split(".")[0] # Extract date part + + # Convert to proper date format + try: + date_obj = datetime.strptime(date_str, "%Y%m%d").date() + except ValueError: + # If date parsing fails, use current date + date_obj = datetime.now().date() + + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Process each trade from bt_result + trades = self.get_trades() + + for pair_name, _ in trades.items(): + + # Second pass: insert completed trade records into database + for trade_pair in sorted(self.pairs_trades_[pair_name], key=lambda x: x["open_time"]): + # Only store completed trades in pt_bt_results table + cursor.execute( + """ + INSERT INTO pt_bt_results ( + date, pair, symbol, open_time, open_side, open_price, + open_quantity, open_disequilibrium, close_time, close_side, + close_price, close_quantity, close_disequilibrium, + symbol_return, pair_return, close_condition + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + date_obj, + pair_name, + trade_pair["symbol"], + trade_pair["open_time"], + trade_pair["open_side"], + trade_pair["open_price"], + trade_pair["shares"], + trade_pair["open_scaled_disequilibrium"], + trade_pair["close_time"], + trade_pair["close_side"], + trade_pair["close_price"], + trade_pair["shares"], + trade_pair["close_scaled_disequilibrium"], + trade_pair["symbol_return"], + trade_pair["pair_return"], + trade_pair["close_condition"] + ), + ) + + # Store outstanding positions in separate table + outstanding_positions = self.get_outstanding_positions() + for pos in outstanding_positions: + # Calculate position quantity (negative for SELL positions) + position_qty_a = ( + pos["shares_a"] if pos["side_a"] == "BUY" else -pos["shares_a"] + ) + position_qty_b = ( + pos["shares_b"] if pos["side_b"] == "BUY" else -pos["shares_b"] + ) + + # Calculate unrealized returns + # For symbol A: (current_price - open_price) / open_price * 100 * position_direction + unrealized_return_a = ( + (pos["current_px_a"] - pos["open_px_a"]) / pos["open_px_a"] * 100 + ) * (1 if pos["side_a"] == "BUY" else -1) + unrealized_return_b = ( + (pos["current_px_b"] - pos["open_px_b"]) / pos["open_px_b"] * 100 + ) * (1 if pos["side_b"] == "BUY" else -1) + + # Store outstanding position for symbol A + cursor.execute( + """ + INSERT INTO outstanding_positions ( + date, pair, symbol, position_quantity, last_price, unrealized_return, open_price, open_side + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + date_obj, + pos["pair"], + pos["symbol_a"], + position_qty_a, + pos["current_px_a"], + unrealized_return_a, + pos["open_px_a"], + pos["side_a"], + ), + ) + + # Store outstanding position for symbol B + cursor.execute( + """ + INSERT INTO outstanding_positions ( + date, pair, symbol, position_quantity, last_price, unrealized_return, open_price, open_side + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + date_obj, + pos["pair"], + pos["symbol_b"], + position_qty_b, + pos["current_px_b"], + unrealized_return_b, + pos["open_px_b"], + pos["side_b"], + ), + ) + + conn.commit() + conn.close() + + except Exception as e: + print(f"Error storing results in database: {str(e)}") + import traceback + + traceback.print_exc() + diff --git a/lib/tools/data_loader.py b/lib/tools/data_loader.py index 7149ad0..4d2920d 100644 --- a/lib/tools/data_loader.py +++ b/lib/tools/data_loader.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import sqlite3 from typing import Dict, List, cast import pandas as pd - def load_sqlite_to_dataframe(db_path, query): try: conn = sqlite3.connect(db_path) @@ -35,20 +36,27 @@ def convert_time_to_UTC(value: str, timezone: str) -> str: return result.strftime("%Y-%m-%d %H:%M:%S") -def load_market_data(datafile: str, config: Dict) -> pd.DataFrame: - from tools.data_loader import load_sqlite_to_dataframe +def load_market_data( + datafile: str, + instruments: List[Dict[str, str]], + db_table_name: str, + trading_hours: Dict = {}, +) -> pd.DataFrame: - instrument_ids = [ - '"' + config["instrument_id_pfx"] + instrument + '"' - for instrument in config["instruments"] + insts = [ + '"' + instrument["instrument_id_pfx"] + instrument["symbol"] + '"' + for instrument in instruments ] - exchange_id = config["exchange_id"] + instrument_ids = list(set(insts)) + exchange_ids = list( + set(['"' + instrument["exchange_id"] + '"' for instrument in instruments]) + ) query = "select" query += " tstamp" query += ", tstamp_ns as time_ns" - query += f", substr(instrument_id, {len(config['instrument_id_pfx']) + 1}) as symbol" + query += f", substr(instrument_id, instr(instrument_id, '-') + 1) as symbol" query += ", open" query += ", high" query += ", low" @@ -57,74 +65,76 @@ def load_market_data(datafile: str, config: Dict) -> pd.DataFrame: query += ", num_trades" query += ", vwap" - query += f" from {config['db_table_name']}" - query += f" where exchange_id ='{exchange_id}'" + query += f" from {db_table_name}" + query += f" where exchange_id in ({','.join(exchange_ids)})" query += f" and instrument_id in ({','.join(instrument_ids)})" df = load_sqlite_to_dataframe(db_path=datafile, query=query) # Trading Hours - date_str = df["tstamp"][0][0:10] - trading_hours = config["trading_hours"] + if len(df) > 0 and len(trading_hours) > 0: + date_str = df["tstamp"][0][0:10] - start_time = convert_time_to_UTC( - f"{date_str} {trading_hours['begin_session']}", trading_hours["timezone"] - ) - end_time = convert_time_to_UTC( - f"{date_str} {trading_hours['end_session']}", trading_hours["timezone"] - ) + start_time = convert_time_to_UTC( + f"{date_str} {trading_hours['begin_session']}", trading_hours["timezone"] + ) + end_time = convert_time_to_UTC( + f"{date_str} {trading_hours['end_session']}", trading_hours["timezone"] + ) - # Perform boolean selection - df = df[(df["tstamp"] >= start_time) & (df["tstamp"] <= end_time)] - df["tstamp"] = pd.to_datetime(df["tstamp"]) + # Perform boolean selection + df = df[(df["tstamp"] >= start_time) & (df["tstamp"] <= end_time)] + df["tstamp"] = pd.to_datetime(df["tstamp"]) return cast(pd.DataFrame, df) -def get_available_instruments_from_db(datafile: str, config: Dict) -> List[str]: - """ - Auto-detect available instruments from the database by querying distinct instrument_id values. - Returns instruments without the configured prefix. - """ - try: - conn = sqlite3.connect(datafile) +# def get_available_instruments_from_db(datafile: str, config: Dict) -> List[str]: +# """ +# Auto-detect available instruments from the database by querying distinct instrument_id values. +# Returns instruments without the configured prefix. +# """ +# try: +# conn = sqlite3.connect(datafile) - # Build exclusion list with full instrument_ids - exclude_instruments = config.get("exclude_instruments", []) - prefix = config.get("instrument_id_pfx", "") - exclude_instrument_ids = [f"{prefix}{inst}" for inst in exclude_instruments] - - # Query to get distinct instrument_ids - query = f""" - SELECT DISTINCT instrument_id - FROM {config['db_table_name']} - WHERE exchange_id = ? - """ - - # Add exclusion clause if there are instruments to exclude - if exclude_instrument_ids: - placeholders = ','.join(['?' for _ in exclude_instrument_ids]) - query += f" AND instrument_id NOT IN ({placeholders})" - cursor = conn.execute(query, (config["exchange_id"],) + tuple(exclude_instrument_ids)) - else: - cursor = conn.execute(query, (config["exchange_id"],)) - instrument_ids = [row[0] for row in cursor.fetchall()] - conn.close() +# # Build exclusion list with full instrument_ids +# exclude_instruments = config.get("exclude_instruments", []) +# prefix = config.get("instrument_id_pfx", "") +# exclude_instrument_ids = [f"{prefix}{inst}" for inst in exclude_instruments] - # Remove the configured prefix to get instrument symbols - instruments = [] - for instrument_id in instrument_ids: - if instrument_id.startswith(prefix): - symbol = instrument_id[len(prefix) :] - instruments.append(symbol) - else: - instruments.append(instrument_id) +# # Query to get distinct instrument_ids +# query = f""" +# SELECT DISTINCT instrument_id +# FROM {config['db_table_name']} +# WHERE exchange_id = ? +# """ - return sorted(instruments) +# # Add exclusion clause if there are instruments to exclude +# if exclude_instrument_ids: +# placeholders = ",".join(["?" for _ in exclude_instrument_ids]) +# query += f" AND instrument_id NOT IN ({placeholders})" +# cursor = conn.execute( +# query, (config["exchange_id"],) + tuple(exclude_instrument_ids) +# ) +# else: +# cursor = conn.execute(query, (config["exchange_id"],)) +# instrument_ids = [row[0] for row in cursor.fetchall()] +# conn.close() - except Exception as e: - print(f"Error auto-detecting instruments from {datafile}: {str(e)}") - return [] +# # Remove the configured prefix to get instrument symbols +# instruments = [] +# for instrument_id in instrument_ids: +# if instrument_id.startswith(prefix): +# symbol = instrument_id[len(prefix) :] +# instruments.append(symbol) +# else: +# instruments.append(instrument_id) + +# return sorted(instruments) + +# except Exception as e: +# print(f"Error auto-detecting instruments from {datafile}: {str(e)}") +# return [] # if __name__ == "__main__": diff --git a/research/cointegration_test.py b/research/cointegration_test.py index 98c0f14..675fe61 100644 --- a/research/cointegration_test.py +++ b/research/cointegration_test.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional import pandas as pd from tools.config import expand_filename, load_config -from tools.data_loader import get_available_instruments_from_db, load_market_data +from tools.data_loader import get_available_instruments_from_db from pt_trading.results import ( BacktestResult, create_result_database, diff --git a/research/notebooks/single_pair_test.ipynb b/research/notebooks/single_pair_test.ipynb index c780311..84de433 100644 --- a/research/notebooks/single_pair_test.ipynb +++ b/research/notebooks/single_pair_test.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 97, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -81,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": 98, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -122,7 +122,7 @@ }, { "cell_type": "code", - "execution_count": 99, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -201,7 +201,7 @@ }, { "cell_type": "code", - "execution_count": 100, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -287,11 +287,11 @@ }, { "cell_type": "code", - "execution_count": 101, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ - "def prepare_market_data() -> None: # Load market data\n", + "def prepare_market_data() -> None: # Load market data\n", " global PT_BT_CONFIG\n", " global DATA_FILE\n", " global SYMBOL_A\n", @@ -302,15 +302,23 @@ " from tools.data_loader import load_market_data\n", " from pt_trading.trading_pair import TradingPair\n", "\n", - "\n", " datafile_path = f\"{PT_BT_CONFIG['data_directory']}/{DATA_FILE}\"\n", " print(f\"Loading data from: {datafile_path}\")\n", "\n", - " market_data_df = load_market_data(datafile_path, config=PT_BT_CONFIG)\n", + " market_data_df = load_market_data(\n", + " datafile=datafile_path,\n", + " exchange_id=PT_BT_CONFIG[\"exchange_id\"],\n", + " instruments=PT_BT_CONFIG[\"instruments\"],\n", + " instrument_id_pfx=PT_BT_CONFIG[\"instrument_id_pfx\"],\n", + " db_table_name=PT_BT_CONFIG[\"db_table_name\"],\n", + " trading_hours=PT_BT_CONFIG[\"trading_hours\"],\n", + " )\n", "\n", " print(f\"Loaded {len(market_data_df)} rows of market data\")\n", " print(f\"Symbols in data: {market_data_df['symbol'].unique()}\")\n", - " print(f\"Time range: {market_data_df['tstamp'].min()} to {market_data_df['tstamp'].max()}\")\n", + " print(\n", + " f\"Time range: {market_data_df['tstamp'].min()} to {market_data_df['tstamp'].max()}\"\n", + " )\n", "\n", " # Create trading pair\n", " pair = FIT_MODEL.create_trading_pair(\n", @@ -318,7 +326,7 @@ " market_data=market_data_df,\n", " symbol_a=SYMBOL_A,\n", " symbol_b=SYMBOL_B,\n", - " price_column=PT_BT_CONFIG[\"price_column\"]\n", + " price_column=PT_BT_CONFIG[\"price_column\"],\n", " )\n", "\n", " print(f\"\\nCreated trading pair: {pair}\")\n", @@ -332,6 +340,8 @@ " display(pair.market_data_.head())\n", "\n", " display(pair.market_data_.tail())\n", + "\n", + "\n", "# prepare_market_data()" ] }, @@ -348,7 +358,7 @@ }, { "cell_type": "code", - "execution_count": 102, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -393,7 +403,7 @@ }, { "cell_type": "code", - "execution_count": 103, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -485,7 +495,7 @@ }, { "cell_type": "code", - "execution_count": 104, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -556,7 +566,7 @@ }, { "cell_type": "code", - "execution_count": 105, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -1060,7 +1070,7 @@ }, { "cell_type": "code", - "execution_count": 106, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -1130,7 +1140,7 @@ }, { "cell_type": "code", - "execution_count": 107, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -1211,7 +1221,7 @@ }, { "cell_type": "code", - "execution_count": 108, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -1658,71 +1668,6 @@ "opacity": 0.8, "type": "scatter", "x": [ - "2025-06-02T13:31:00.000000000", - "2025-06-02T13:32:00.000000000", - "2025-06-02T13:33:00.000000000", - "2025-06-02T13:34:00.000000000", - "2025-06-02T13:35:00.000000000", - "2025-06-02T13:37:00.000000000", - "2025-06-02T13:38:00.000000000", - "2025-06-02T13:40:00.000000000", - "2025-06-02T13:41:00.000000000", - "2025-06-02T13:42:00.000000000", - "2025-06-02T13:44:00.000000000", - "2025-06-02T13:45:00.000000000", - "2025-06-02T13:49:00.000000000", - "2025-06-02T13:50:00.000000000", - "2025-06-02T13:51:00.000000000", - "2025-06-02T13:55:00.000000000", - "2025-06-02T13:59:00.000000000", - "2025-06-02T14:04:00.000000000", - "2025-06-02T14:05:00.000000000", - "2025-06-02T14:11:00.000000000", - "2025-06-02T14:15:00.000000000", - "2025-06-02T14:18:00.000000000", - "2025-06-02T14:23:00.000000000", - "2025-06-02T14:27:00.000000000", - "2025-06-02T14:28:00.000000000", - "2025-06-02T14:30:00.000000000", - "2025-06-02T14:34:00.000000000", - "2025-06-02T14:35:00.000000000", - "2025-06-02T14:36:00.000000000", - "2025-06-02T14:37:00.000000000", - "2025-06-02T14:38:00.000000000", - "2025-06-02T14:39:00.000000000", - "2025-06-02T14:41:00.000000000", - "2025-06-02T14:43:00.000000000", - "2025-06-02T14:45:00.000000000", - "2025-06-02T14:58:00.000000000", - "2025-06-02T15:08:00.000000000", - "2025-06-02T15:14:00.000000000", - "2025-06-02T15:19:00.000000000", - "2025-06-02T15:20:00.000000000", - "2025-06-02T15:21:00.000000000", - "2025-06-02T15:23:00.000000000", - "2025-06-02T15:24:00.000000000", - "2025-06-02T15:29:00.000000000", - "2025-06-02T15:30:00.000000000", - "2025-06-02T15:31:00.000000000", - "2025-06-02T15:33:00.000000000", - "2025-06-02T15:34:00.000000000", - "2025-06-02T15:35:00.000000000", - "2025-06-02T15:38:00.000000000", - "2025-06-02T15:48:00.000000000", - "2025-06-02T15:49:00.000000000", - "2025-06-02T15:53:00.000000000", - "2025-06-02T15:55:00.000000000", - "2025-06-02T15:57:00.000000000", - "2025-06-02T16:04:00.000000000", - "2025-06-02T16:06:00.000000000", - "2025-06-02T16:07:00.000000000", - "2025-06-02T16:09:00.000000000", - "2025-06-02T16:12:00.000000000", - "2025-06-02T16:16:00.000000000", - "2025-06-02T16:17:00.000000000", - "2025-06-02T16:18:00.000000000", - "2025-06-02T16:19:00.000000000", - "2025-06-02T16:20:00.000000000", "NaT", "NaT", "NaT", @@ -1844,46 +1789,81 @@ "NaT", "NaT", "2025-06-02T13:30:00.000000000", + "2025-06-02T13:31:00.000000000", + "2025-06-02T13:32:00.000000000", + "2025-06-02T13:33:00.000000000", + "2025-06-02T13:34:00.000000000", + "2025-06-02T13:35:00.000000000", "2025-06-02T13:36:00.000000000", + "2025-06-02T13:37:00.000000000", + "2025-06-02T13:38:00.000000000", "2025-06-02T13:39:00.000000000", + "2025-06-02T13:40:00.000000000", + "2025-06-02T13:41:00.000000000", + "2025-06-02T13:42:00.000000000", "2025-06-02T13:43:00.000000000", + "2025-06-02T13:44:00.000000000", + "2025-06-02T13:45:00.000000000", "2025-06-02T13:46:00.000000000", "2025-06-02T13:47:00.000000000", "2025-06-02T13:48:00.000000000", + "2025-06-02T13:49:00.000000000", + "2025-06-02T13:50:00.000000000", + "2025-06-02T13:51:00.000000000", "2025-06-02T13:52:00.000000000", "2025-06-02T13:53:00.000000000", "2025-06-02T13:54:00.000000000", + "2025-06-02T13:55:00.000000000", "2025-06-02T13:56:00.000000000", "2025-06-02T13:57:00.000000000", "2025-06-02T13:58:00.000000000", + "2025-06-02T13:59:00.000000000", "2025-06-02T14:00:00.000000000", "2025-06-02T14:01:00.000000000", "2025-06-02T14:02:00.000000000", "2025-06-02T14:03:00.000000000", + "2025-06-02T14:04:00.000000000", + "2025-06-02T14:05:00.000000000", "2025-06-02T14:06:00.000000000", "2025-06-02T14:07:00.000000000", "2025-06-02T14:08:00.000000000", "2025-06-02T14:09:00.000000000", "2025-06-02T14:10:00.000000000", + "2025-06-02T14:11:00.000000000", "2025-06-02T14:12:00.000000000", "2025-06-02T14:13:00.000000000", "2025-06-02T14:14:00.000000000", + "2025-06-02T14:15:00.000000000", "2025-06-02T14:16:00.000000000", "2025-06-02T14:17:00.000000000", + "2025-06-02T14:18:00.000000000", "2025-06-02T14:19:00.000000000", "2025-06-02T14:20:00.000000000", "2025-06-02T14:21:00.000000000", "2025-06-02T14:22:00.000000000", + "2025-06-02T14:23:00.000000000", "2025-06-02T14:24:00.000000000", "2025-06-02T14:25:00.000000000", "2025-06-02T14:26:00.000000000", + "2025-06-02T14:27:00.000000000", + "2025-06-02T14:28:00.000000000", "2025-06-02T14:29:00.000000000", + "2025-06-02T14:30:00.000000000", "2025-06-02T14:31:00.000000000", "2025-06-02T14:32:00.000000000", "2025-06-02T14:33:00.000000000", + "2025-06-02T14:34:00.000000000", + "2025-06-02T14:35:00.000000000", + "2025-06-02T14:36:00.000000000", + "2025-06-02T14:37:00.000000000", + "2025-06-02T14:38:00.000000000", + "2025-06-02T14:39:00.000000000", "2025-06-02T14:40:00.000000000", + "2025-06-02T14:41:00.000000000", "2025-06-02T14:42:00.000000000", + "2025-06-02T14:43:00.000000000", "2025-06-02T14:44:00.000000000", + "2025-06-02T14:45:00.000000000", "2025-06-02T14:46:00.000000000", "2025-06-02T14:47:00.000000000", "2025-06-02T14:48:00.000000000", @@ -1895,6 +1875,7 @@ "2025-06-02T14:55:00.000000000", "2025-06-02T14:56:00.000000000", "2025-06-02T14:57:00.000000000", + "2025-06-02T14:58:00.000000000", "2025-06-02T14:59:00.000000000", "2025-06-02T15:00:00.000000000", "2025-06-02T15:01:00.000000000", @@ -1904,23 +1885,37 @@ "2025-06-02T15:05:00.000000000", "2025-06-02T15:06:00.000000000", "2025-06-02T15:07:00.000000000", + "2025-06-02T15:08:00.000000000", "2025-06-02T15:09:00.000000000", "2025-06-02T15:10:00.000000000", "2025-06-02T15:11:00.000000000", "2025-06-02T15:12:00.000000000", "2025-06-02T15:13:00.000000000", + "2025-06-02T15:14:00.000000000", "2025-06-02T15:15:00.000000000", "2025-06-02T15:16:00.000000000", "2025-06-02T15:17:00.000000000", "2025-06-02T15:18:00.000000000", + "2025-06-02T15:19:00.000000000", + "2025-06-02T15:20:00.000000000", + "2025-06-02T15:21:00.000000000", "2025-06-02T15:22:00.000000000", + "2025-06-02T15:23:00.000000000", + "2025-06-02T15:24:00.000000000", "2025-06-02T15:25:00.000000000", "2025-06-02T15:26:00.000000000", "2025-06-02T15:27:00.000000000", "2025-06-02T15:28:00.000000000", + "2025-06-02T15:29:00.000000000", + "2025-06-02T15:30:00.000000000", + "2025-06-02T15:31:00.000000000", "2025-06-02T15:32:00.000000000", + "2025-06-02T15:33:00.000000000", + "2025-06-02T15:34:00.000000000", + "2025-06-02T15:35:00.000000000", "2025-06-02T15:36:00.000000000", "2025-06-02T15:37:00.000000000", + "2025-06-02T15:38:00.000000000", "2025-06-02T15:39:00.000000000", "2025-06-02T15:40:00.000000000", "2025-06-02T15:41:00.000000000", @@ -1930,24 +1925,39 @@ "2025-06-02T15:45:00.000000000", "2025-06-02T15:46:00.000000000", "2025-06-02T15:47:00.000000000", + "2025-06-02T15:48:00.000000000", + "2025-06-02T15:49:00.000000000", "2025-06-02T15:50:00.000000000", "2025-06-02T15:51:00.000000000", "2025-06-02T15:52:00.000000000", + "2025-06-02T15:53:00.000000000", "2025-06-02T15:54:00.000000000", + "2025-06-02T15:55:00.000000000", "2025-06-02T15:56:00.000000000", + "2025-06-02T15:57:00.000000000", "2025-06-02T15:58:00.000000000", "2025-06-02T15:59:00.000000000", "2025-06-02T16:00:00.000000000", "2025-06-02T16:01:00.000000000", "2025-06-02T16:02:00.000000000", "2025-06-02T16:03:00.000000000", + "2025-06-02T16:04:00.000000000", "2025-06-02T16:05:00.000000000", + "2025-06-02T16:06:00.000000000", + "2025-06-02T16:07:00.000000000", "2025-06-02T16:08:00.000000000", + "2025-06-02T16:09:00.000000000", "2025-06-02T16:10:00.000000000", "2025-06-02T16:11:00.000000000", + "2025-06-02T16:12:00.000000000", "2025-06-02T16:13:00.000000000", "2025-06-02T16:14:00.000000000", "2025-06-02T16:15:00.000000000", + "2025-06-02T16:16:00.000000000", + "2025-06-02T16:17:00.000000000", + "2025-06-02T16:18:00.000000000", + "2025-06-02T16:19:00.000000000", + "2025-06-02T16:20:00.000000000", "2025-06-02T16:21:00.000000000", "2025-06-02T16:22:00.000000000", "2025-06-02T16:23:00.000000000", @@ -2410,7 +2420,7 @@ ], "xaxis": "x", "y": { - "bdata": "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", + "bdata": "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", "dtype": "f8" }, "yaxis": "y" @@ -2424,71 +2434,6 @@ "opacity": 0.8, "type": "scatter", "x": [ - "2025-06-02T13:31:00.000000000", - "2025-06-02T13:32:00.000000000", - "2025-06-02T13:33:00.000000000", - "2025-06-02T13:34:00.000000000", - "2025-06-02T13:35:00.000000000", - "2025-06-02T13:37:00.000000000", - "2025-06-02T13:38:00.000000000", - "2025-06-02T13:40:00.000000000", - "2025-06-02T13:41:00.000000000", - "2025-06-02T13:42:00.000000000", - "2025-06-02T13:44:00.000000000", - "2025-06-02T13:45:00.000000000", - "2025-06-02T13:49:00.000000000", - "2025-06-02T13:50:00.000000000", - "2025-06-02T13:51:00.000000000", - "2025-06-02T13:55:00.000000000", - "2025-06-02T13:59:00.000000000", - "2025-06-02T14:04:00.000000000", - "2025-06-02T14:05:00.000000000", - "2025-06-02T14:11:00.000000000", - "2025-06-02T14:15:00.000000000", - "2025-06-02T14:18:00.000000000", - "2025-06-02T14:23:00.000000000", - "2025-06-02T14:27:00.000000000", - "2025-06-02T14:28:00.000000000", - "2025-06-02T14:30:00.000000000", - "2025-06-02T14:34:00.000000000", - "2025-06-02T14:35:00.000000000", - "2025-06-02T14:36:00.000000000", - "2025-06-02T14:37:00.000000000", - "2025-06-02T14:38:00.000000000", - "2025-06-02T14:39:00.000000000", - "2025-06-02T14:41:00.000000000", - "2025-06-02T14:43:00.000000000", - "2025-06-02T14:45:00.000000000", - "2025-06-02T14:58:00.000000000", - "2025-06-02T15:08:00.000000000", - "2025-06-02T15:14:00.000000000", - "2025-06-02T15:19:00.000000000", - "2025-06-02T15:20:00.000000000", - "2025-06-02T15:21:00.000000000", - "2025-06-02T15:23:00.000000000", - "2025-06-02T15:24:00.000000000", - "2025-06-02T15:29:00.000000000", - "2025-06-02T15:30:00.000000000", - "2025-06-02T15:31:00.000000000", - "2025-06-02T15:33:00.000000000", - "2025-06-02T15:34:00.000000000", - "2025-06-02T15:35:00.000000000", - "2025-06-02T15:38:00.000000000", - "2025-06-02T15:48:00.000000000", - "2025-06-02T15:49:00.000000000", - "2025-06-02T15:53:00.000000000", - "2025-06-02T15:55:00.000000000", - "2025-06-02T15:57:00.000000000", - "2025-06-02T16:04:00.000000000", - "2025-06-02T16:06:00.000000000", - "2025-06-02T16:07:00.000000000", - "2025-06-02T16:09:00.000000000", - "2025-06-02T16:12:00.000000000", - "2025-06-02T16:16:00.000000000", - "2025-06-02T16:17:00.000000000", - "2025-06-02T16:18:00.000000000", - "2025-06-02T16:19:00.000000000", - "2025-06-02T16:20:00.000000000", "NaT", "NaT", "NaT", @@ -2610,46 +2555,81 @@ "NaT", "NaT", "2025-06-02T13:30:00.000000000", + "2025-06-02T13:31:00.000000000", + "2025-06-02T13:32:00.000000000", + "2025-06-02T13:33:00.000000000", + "2025-06-02T13:34:00.000000000", + "2025-06-02T13:35:00.000000000", "2025-06-02T13:36:00.000000000", + "2025-06-02T13:37:00.000000000", + "2025-06-02T13:38:00.000000000", "2025-06-02T13:39:00.000000000", + "2025-06-02T13:40:00.000000000", + "2025-06-02T13:41:00.000000000", + "2025-06-02T13:42:00.000000000", "2025-06-02T13:43:00.000000000", + "2025-06-02T13:44:00.000000000", + "2025-06-02T13:45:00.000000000", "2025-06-02T13:46:00.000000000", "2025-06-02T13:47:00.000000000", "2025-06-02T13:48:00.000000000", + "2025-06-02T13:49:00.000000000", + "2025-06-02T13:50:00.000000000", + "2025-06-02T13:51:00.000000000", "2025-06-02T13:52:00.000000000", "2025-06-02T13:53:00.000000000", "2025-06-02T13:54:00.000000000", + "2025-06-02T13:55:00.000000000", "2025-06-02T13:56:00.000000000", "2025-06-02T13:57:00.000000000", "2025-06-02T13:58:00.000000000", + "2025-06-02T13:59:00.000000000", "2025-06-02T14:00:00.000000000", "2025-06-02T14:01:00.000000000", "2025-06-02T14:02:00.000000000", "2025-06-02T14:03:00.000000000", + "2025-06-02T14:04:00.000000000", + "2025-06-02T14:05:00.000000000", "2025-06-02T14:06:00.000000000", "2025-06-02T14:07:00.000000000", "2025-06-02T14:08:00.000000000", "2025-06-02T14:09:00.000000000", "2025-06-02T14:10:00.000000000", + "2025-06-02T14:11:00.000000000", "2025-06-02T14:12:00.000000000", "2025-06-02T14:13:00.000000000", "2025-06-02T14:14:00.000000000", + "2025-06-02T14:15:00.000000000", "2025-06-02T14:16:00.000000000", "2025-06-02T14:17:00.000000000", + "2025-06-02T14:18:00.000000000", "2025-06-02T14:19:00.000000000", "2025-06-02T14:20:00.000000000", "2025-06-02T14:21:00.000000000", "2025-06-02T14:22:00.000000000", + "2025-06-02T14:23:00.000000000", "2025-06-02T14:24:00.000000000", "2025-06-02T14:25:00.000000000", "2025-06-02T14:26:00.000000000", + "2025-06-02T14:27:00.000000000", + "2025-06-02T14:28:00.000000000", "2025-06-02T14:29:00.000000000", + "2025-06-02T14:30:00.000000000", "2025-06-02T14:31:00.000000000", "2025-06-02T14:32:00.000000000", "2025-06-02T14:33:00.000000000", + "2025-06-02T14:34:00.000000000", + "2025-06-02T14:35:00.000000000", + "2025-06-02T14:36:00.000000000", + "2025-06-02T14:37:00.000000000", + "2025-06-02T14:38:00.000000000", + "2025-06-02T14:39:00.000000000", "2025-06-02T14:40:00.000000000", + "2025-06-02T14:41:00.000000000", "2025-06-02T14:42:00.000000000", + "2025-06-02T14:43:00.000000000", "2025-06-02T14:44:00.000000000", + "2025-06-02T14:45:00.000000000", "2025-06-02T14:46:00.000000000", "2025-06-02T14:47:00.000000000", "2025-06-02T14:48:00.000000000", @@ -2661,6 +2641,7 @@ "2025-06-02T14:55:00.000000000", "2025-06-02T14:56:00.000000000", "2025-06-02T14:57:00.000000000", + "2025-06-02T14:58:00.000000000", "2025-06-02T14:59:00.000000000", "2025-06-02T15:00:00.000000000", "2025-06-02T15:01:00.000000000", @@ -2670,23 +2651,37 @@ "2025-06-02T15:05:00.000000000", "2025-06-02T15:06:00.000000000", "2025-06-02T15:07:00.000000000", + "2025-06-02T15:08:00.000000000", "2025-06-02T15:09:00.000000000", "2025-06-02T15:10:00.000000000", "2025-06-02T15:11:00.000000000", "2025-06-02T15:12:00.000000000", "2025-06-02T15:13:00.000000000", + "2025-06-02T15:14:00.000000000", "2025-06-02T15:15:00.000000000", "2025-06-02T15:16:00.000000000", "2025-06-02T15:17:00.000000000", "2025-06-02T15:18:00.000000000", + "2025-06-02T15:19:00.000000000", + "2025-06-02T15:20:00.000000000", + "2025-06-02T15:21:00.000000000", "2025-06-02T15:22:00.000000000", + "2025-06-02T15:23:00.000000000", + "2025-06-02T15:24:00.000000000", "2025-06-02T15:25:00.000000000", "2025-06-02T15:26:00.000000000", "2025-06-02T15:27:00.000000000", "2025-06-02T15:28:00.000000000", + "2025-06-02T15:29:00.000000000", + "2025-06-02T15:30:00.000000000", + "2025-06-02T15:31:00.000000000", "2025-06-02T15:32:00.000000000", + "2025-06-02T15:33:00.000000000", + "2025-06-02T15:34:00.000000000", + "2025-06-02T15:35:00.000000000", "2025-06-02T15:36:00.000000000", "2025-06-02T15:37:00.000000000", + "2025-06-02T15:38:00.000000000", "2025-06-02T15:39:00.000000000", "2025-06-02T15:40:00.000000000", "2025-06-02T15:41:00.000000000", @@ -2696,24 +2691,39 @@ "2025-06-02T15:45:00.000000000", "2025-06-02T15:46:00.000000000", "2025-06-02T15:47:00.000000000", + "2025-06-02T15:48:00.000000000", + "2025-06-02T15:49:00.000000000", "2025-06-02T15:50:00.000000000", "2025-06-02T15:51:00.000000000", "2025-06-02T15:52:00.000000000", + "2025-06-02T15:53:00.000000000", "2025-06-02T15:54:00.000000000", + "2025-06-02T15:55:00.000000000", "2025-06-02T15:56:00.000000000", + "2025-06-02T15:57:00.000000000", "2025-06-02T15:58:00.000000000", "2025-06-02T15:59:00.000000000", "2025-06-02T16:00:00.000000000", "2025-06-02T16:01:00.000000000", "2025-06-02T16:02:00.000000000", "2025-06-02T16:03:00.000000000", + "2025-06-02T16:04:00.000000000", "2025-06-02T16:05:00.000000000", + "2025-06-02T16:06:00.000000000", + "2025-06-02T16:07:00.000000000", "2025-06-02T16:08:00.000000000", + "2025-06-02T16:09:00.000000000", "2025-06-02T16:10:00.000000000", "2025-06-02T16:11:00.000000000", + "2025-06-02T16:12:00.000000000", "2025-06-02T16:13:00.000000000", "2025-06-02T16:14:00.000000000", "2025-06-02T16:15:00.000000000", + "2025-06-02T16:16:00.000000000", + "2025-06-02T16:17:00.000000000", + "2025-06-02T16:18:00.000000000", + "2025-06-02T16:19:00.000000000", + "2025-06-02T16:20:00.000000000", "2025-06-02T16:21:00.000000000", "2025-06-02T16:22:00.000000000", "2025-06-02T16:23:00.000000000", @@ -3176,7 +3186,7 @@ ], "xaxis": "x", "y": { - "bdata": "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", + "bdata": "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", "dtype": "f8" }, "yaxis": "y" @@ -7179,9 +7189,9 @@ }, "text/html": [ "
\n", - "