diff --git a/.gitignore b/.gitignore index ee63380..c1436db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ # SpecStory explanation file +__pycache__/ .specstory/ .history/ .cursorindexingignore data .vscode/ cvttpy +# SpecStory explanation file +.specstory/.what-is-this.md diff --git a/src/direct_solution.py b/src/direct_solution.py deleted file mode 100644 index 260a22b..0000000 --- a/src/direct_solution.py +++ /dev/null @@ -1,43 +0,0 @@ -import pandas as pd - -# This example shows how to transform your dataframe to have columns like close-COIN, close-PYPL, etc. - -# Assuming df is your input dataframe with the structure shown in your question -def transform_dataframe(df): - # Select only the columns we need - selected_columns = ["tstamp", "symbol", "close"] - df_selected = df[selected_columns] - - # Start with unique timestamps - result_df = df_selected["tstamp"].drop_duplicates().reset_index(drop=True) - - # For each unique symbol, add a corresponding close price column - for symbol in df_selected["symbol"].unique(): - # Filter rows for this symbol - df_symbol = df_selected[df_selected["symbol"] == symbol].reset_index(drop=True) - - # Create column name like "close-COIN" - price_column = f"close-{symbol}" - - # Create temporary dataframe with timestamp and price - temp_df = pd.DataFrame({ - "tstamp": df_symbol["tstamp"], - price_column: df_symbol["close"] - }) - - # Join with our result dataframe - result_df = pd.merge(result_df, temp_df, on="tstamp", how="left") - - return result_df - -# Example usage (assuming df is your input dataframe): -# result_df = transform_dataframe(df) -# print(result_df.head()) - -""" -The resulting dataframe will look like: - tstamp close-COIN close-GBTC close-HOOD close-MSTR close-PYPL -0 2025-05-20 14:30:00 262.3650 45.1234 21.567 935.42 72.1611 -1 2025-05-20 14:31:00 262.5850 45.2100 21.589 935.67 72.1611 -... -""" \ No newline at end of file diff --git a/src/pivot_example.py b/src/pivot_example.py deleted file mode 100644 index 5554d47..0000000 --- a/src/pivot_example.py +++ /dev/null @@ -1,77 +0,0 @@ -import pandas as pd - -# Assuming your dataframe is named 'df' -def pivot_dataframe(df): - # Convert timestamp to datetime if it's not already - df['tstamp'] = pd.to_datetime(df['tstamp']) - - # Create a pivot table with 'tstamp' as index and 'symbol' as columns - # You can choose any aggregation function (mean, first, last, etc.) - # if there are multiple values per timestamp/symbol - pivoted_df = pd.pivot_table( - df, - values=['close', 'open', 'high', 'low', 'volume'], - index='tstamp', - columns='symbol', - aggfunc='first' # Use 'first' if there's only one value per timestamp/symbol - ) - - # Flatten the multi-level column names - pivoted_df.columns = [f"{col[0]}-{col[1]}" for col in pivoted_df.columns] - - # Reset index to make tstamp a column - pivoted_df = pivoted_df.reset_index() - - return pivoted_df - -# Example usage: -# pivoted_df = pivot_dataframe(your_dataframe) -# print(pivoted_df.head()) - -# Alternative approach (similar to your code): -def pivot_alternative(df): - # Create an empty dataframe with just the timestamp - result_df = df['tstamp'].drop_duplicates().reset_index(drop=True) - - # For each symbol, create a separate column for each metric - for symbol in df['symbol'].unique(): - # Filter dataframe for this symbol - df_symbol = df[df['symbol'] == symbol].reset_index(drop=True) - - # Create columns for each price/volume metric - for metric in ['close', 'open', 'high', 'low', 'volume']: - column_name = f"{metric}-{symbol}" - # Create a temporary dataframe with tstamp and the new column - temp_df = pd.DataFrame({ - 'tstamp': df_symbol['tstamp'], - column_name: df_symbol[metric] - }) - - # Merge with the result dataframe - result_df = pd.merge(result_df, temp_df, on='tstamp', how='left') - - return result_df - -# Example using the code similar to what's in your main code: -""" -# Selected columns from original dataframe -selected_columns = ["tstamp", "symbol", "close"] -df_selected = df[selected_columns] - -# Create result dataframe starting with timestamps -result_df = df_selected["tstamp"].drop_duplicates().reset_index(drop=True) - -# For each symbol, add a column with its close price -for symbol in df_selected["symbol"].unique(): - df_symbol = df_selected[df_selected["symbol"] == symbol].reset_index(drop=True) - price_column = f"close-{symbol}" - - # Create temp dataframe with tstamp and the price column - temp_df = pd.DataFrame({ - "tstamp": df_symbol["tstamp"], - price_column: df_symbol["close"] - }) - - # Merge with result dataframe - result_df = pd.merge(result_df, temp_df, on="tstamp", how="left") -""" \ No newline at end of file diff --git a/src/pt_backtest_slide.py b/src/pt_backtest_slide.py index 3b60723..bc3a8e0 100644 --- a/src/pt_backtest_slide.py +++ b/src/pt_backtest_slide.py @@ -41,9 +41,11 @@ CONFIG: Dict = { "price_column": "close", "min_required_points": 30, "zero_threshold": 1e-10, - "equilibrium_threshold": 10.0, - # "training_minutes": 120, + "equilibrium_threshold_open": 5.0, + "equilibrium_threshold_close": 1.0, "training_minutes": 120, + + "funding_per_pair": 2000.0, } # ====== later =================== @@ -66,6 +68,9 @@ CONFIG: Dict = { # ------------------------ Settings ------------------------ TRADES = {} +TOTAL_UNREALIZED_PNL = 0.0 # Global variable to track total unrealized PnL +TOTAL_REALIZED_PNL = 0.0 # Global variable to track total realized PnL +OUTSTANDING_POSITIONS = [] # Global list to track outstanding positions with share quantities class Pair: symbol_a_: str symbol_b_: str @@ -76,11 +81,9 @@ class Pair: self.symbol_b_ = symbol_b self.price_column_ = price_column - def colname_a(self) -> str: - return f"{self.price_column_}_{self.symbol_a_}" - def colname_b(self) -> str: - return f"{self.price_column_}_{self.symbol_b_}" + def colnames(self) -> List[str]: + return [f"{self.price_column_}_{self.symbol_a_}", f"{self.price_column_}_{self.symbol_b_}"] def __repr__(self) ->str: return f"{self.symbol_a_} & {self.symbol_b_}" @@ -148,8 +151,7 @@ def transform_dataframe(df: pd.DataFrame, price_column: str): def get_datasets(df: pd.DataFrame, training_minutes: int, pair: Pair) -> Tuple[pd.DataFrame, pd.DataFrame]: # Training dataset - colname_a = pair.colname_a() - colname_b = pair.colname_b() + colname_a, colname_b = pair.colnames() df = df[["tstamp", colname_a, colname_b]] df = df.dropna() @@ -163,7 +165,7 @@ def get_datasets(df: pd.DataFrame, training_minutes: int, pair: Pair) -> Tuple[p return (training_df, testing_df) def fit_VECM(training_pair_df, pair: Pair): - vecm_model = VECM(training_pair_df[[pair.colname_a(), pair.colname_b()]].reset_index(drop=True), coint_rank=1) + vecm_model = VECM(training_pair_df[pair.colnames()].reset_index(drop=True), coint_rank=1) vecm_fit = vecm_model.fit() # Check if the model converged properly @@ -172,17 +174,18 @@ def fit_VECM(training_pair_df, pair: Pair): return vecm_fit -def create_trading_signals(vecm_fit, testing_pair_df, pair: Pair, colname_a, colname_b) -> pd.DataFrame: +def create_trading_signals(vecm_fit, testing_pair_df, pair: Pair) -> pd.DataFrame: result_columns = [ "time", "action", "symbol", "price", - "divergence", + "equilibrium", "pair", ] next_values = vecm_fit.predict(steps=len(testing_pair_df)) + colname_a, colname_b = pair.colnames() # Convert prediction to a DataFrame for readability predicted_df = pd.DataFrame(next_values, columns=[colname_a, colname_b]) @@ -198,52 +201,57 @@ def create_trading_signals(vecm_fit, testing_pair_df, pair: Pair, colname_a, col testing_pair_df.reset_index(drop=True), predicted_df, left_index=True, right_index=True, suffixes=('', '_pred') ).dropna() - pair_result_df["testing_eqlbrm_term"] = ( + pair_result_df["equilibrium"] = ( beta[0] * pair_result_df[colname_a] + beta[1] * pair_result_df[colname_b] ) - pair_result_df["abs_testing_eqlbrm_term"] = np.abs(pair_result_df["testing_eqlbrm_term"]) + pair_result_df["abs_equilibrium"] = np.abs(pair_result_df["equilibrium"]) - # Check if the first value is non-zero to avoid division by zero + # Reset index to ensure proper indexing pair_result_df = pair_result_df.reset_index() - initial_abs_term = pair_result_df["abs_testing_eqlbrm_term"][0] - if ( - initial_abs_term < CONFIG["zero_threshold"] - ): # Small threshold to avoid division by very small numbers - print( - f"{pair}: Skipping pair due to near-zero initial equilibrium: {initial_abs_term}" - ) + + # Iterate through the testing dataset to find the first trading opportunity + open_row_index = None + initial_abs_term = None + + for row_idx in range(len(pair_result_df)): + current_abs_term = pair_result_df["abs_equilibrium"][row_idx] + + # Check if current row has sufficient equilibrium (not near-zero) + if current_abs_term >= CONFIG["equilibrium_threshold_open"]: + open_row_index = row_idx + initial_abs_term = current_abs_term + break + + # If no row with sufficient equilibrium found, skip this pair + if open_row_index is None: + print(f"{pair}: Skipping pair - no rows with sufficient equilibrium found in testing dataset") return pd.DataFrame() + # Look for close signal starting from the open position trading_signals_df = ( - pair_result_df["abs_testing_eqlbrm_term"] - < initial_abs_term / CONFIG["equilibrium_threshold"] - ) - close_row_index = next( - (index for index, value in trading_signals_df.items() if value), None + pair_result_df["abs_equilibrium"][open_row_index:] + # < initial_abs_term / CONFIG["equilibrium_threshold_close"] + < CONFIG["equilibrium_threshold_close"] ) - if close_row_index is None: - print(f"{pair}: NO SIGNAL FOUND") - return pd.DataFrame() - - open_row = pair_result_df.loc[0] - close_row = pair_result_df.loc[close_row_index] + # 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 = pair_result_df.loc[open_row_index] open_tstamp = open_row["tstamp"] - open_eqlbrm = open_row["testing_eqlbrm_term"] + open_eqlbrm = open_row["equilibrium"] open_px_a = open_row[f"{colname_a}"] open_px_b = open_row[f"{colname_b}"] - close_tstamp = close_row["tstamp"] - close_eqlbrm = close_row["testing_eqlbrm_term"] - close_px_a = close_row[f"{colname_a}"] - close_px_b = close_row[f"{colname_b}"] - abs_beta = abs(beta[1]) - pred_px_b = pair_result_df.loc[0][f"{colname_b}_pred"] - pred_px_a = pair_result_df.loc[0][f"{colname_a}_pred"] + pred_px_b = pair_result_df.loc[open_row_index][f"{colname_b}_pred"] + pred_px_a = pair_result_df.loc[open_row_index][f"{colname_a}_pred"] if pred_px_b * abs_beta - pred_px_a > 0: open_side_a = "BUY" @@ -256,40 +264,136 @@ def create_trading_signals(vecm_fit, testing_pair_df, pair: Pair, colname_a, col close_side_b = "SELL" close_side_a = "BUY" - trd_signal_tuples = [ - ( - open_tstamp, - open_side_a, - pair.symbol_a_, - open_px_a, - open_eqlbrm, - pair, - ), - ( - open_tstamp, - open_side_b, - pair.symbol_b_, - open_px_b, - open_eqlbrm, - pair, - ), - ( - close_tstamp, - close_side_a, - pair.symbol_a_, - close_px_a, - close_eqlbrm, - pair, - ), - ( - close_tstamp, - close_side_b, - pair.symbol_b_, - close_px_b, - close_eqlbrm, - pair, - ), - ] + # If no close signal found, print position and unrealized PnL + if close_row_index is None: + global TOTAL_UNREALIZED_PNL, OUTSTANDING_POSITIONS + + last_row_index = len(pair_result_df) - 1 + last_row = pair_result_df.loc[last_row_index] + last_tstamp = last_row["tstamp"] + last_px_a = last_row[f"{colname_a}"] + last_px_b = last_row[f"{colname_b}"] + + # Calculate share quantities based on $1000 funding per pair + # Split $1000 equally between the two positions ($500 each) + funding_per_position = CONFIG["funding_per_pair"] / 2 + shares_a = funding_per_position / open_px_a + shares_b = funding_per_position / open_px_b + + # Calculate unrealized PnL for each position + if open_side_a == "BUY": + unrealized_pnl_a = (last_px_a - open_px_a) / open_px_a * 100 + unrealized_dollar_a = shares_a * (last_px_a - open_px_a) + else: # SELL + unrealized_pnl_a = (open_px_a - last_px_a) / open_px_a * 100 + unrealized_dollar_a = shares_a * (open_px_a - last_px_a) + + if open_side_b == "BUY": + unrealized_pnl_b = (last_px_b - open_px_b) / open_px_b * 100 + unrealized_dollar_b = shares_b * (last_px_b - open_px_b) + else: # SELL + unrealized_pnl_b = (open_px_b - last_px_b) / open_px_b * 100 + unrealized_dollar_b = shares_b * (open_px_b - last_px_b) + + total_unrealized_pnl = unrealized_pnl_a + unrealized_pnl_b + total_unrealized_dollar = unrealized_dollar_a + unrealized_dollar_b + + # Add to global total + TOTAL_UNREALIZED_PNL += total_unrealized_pnl + + # Store outstanding positions + OUTSTANDING_POSITIONS.append({ + 'pair': str(pair), + 'symbol_a': pair.symbol_a_, + 'symbol_b': pair.symbol_b_, + 'side_a': open_side_a, + 'side_b': open_side_b, + 'shares_a': shares_a, + 'shares_b': shares_b, + 'open_px_a': open_px_a, + 'open_px_b': open_px_b, + 'current_px_a': last_px_a, + 'current_px_b': last_px_b, + 'unrealized_dollar_a': unrealized_dollar_a, + 'unrealized_dollar_b': unrealized_dollar_b, + 'total_unrealized_dollar': total_unrealized_dollar, + 'open_time': open_tstamp, + 'last_time': last_tstamp, + 'initial_abs_term': initial_abs_term, + 'current_abs_term': pair_result_df.loc[last_row_index, "abs_equilibrium"], + 'closing_threshold': initial_abs_term / CONFIG["equilibrium_threshold_close"], + 'equilibrium_ratio': pair_result_df.loc[last_row_index, "abs_equilibrium"] / (initial_abs_term / CONFIG["equilibrium_threshold_close"]) + }) + + print(f"{pair}: NO CLOSE SIGNAL FOUND - Position held until end of session") + print(f" Open: {open_tstamp} | Last: {last_tstamp}") + print(f" {pair.symbol_a_}: {open_side_a} {shares_a:.2f} shares @ ${open_px_a:.2f} -> ${last_px_a:.2f} | Unrealized: ${unrealized_dollar_a:.2f} ({unrealized_pnl_a:.2f}%)") + print(f" {pair.symbol_b_}: {open_side_b} {shares_b:.2f} shares @ ${open_px_b:.2f} -> ${last_px_b:.2f} | Unrealized: ${unrealized_dollar_b:.2f} ({unrealized_pnl_b:.2f}%)") + print(f" Total Unrealized: ${total_unrealized_dollar:.2f} ({total_unrealized_pnl:.2f}%)") + + # Return only open trades (no close trades) + trd_signal_tuples = [ + ( + open_tstamp, + open_side_a, + pair.symbol_a_, + open_px_a, + open_eqlbrm, + pair, + ), + ( + open_tstamp, + open_side_b, + pair.symbol_b_, + open_px_b, + open_eqlbrm, + pair, + ), + ] + else: + # Close signal found - create complete trade + close_row = pair_result_df.loc[close_row_index] + close_tstamp = close_row["tstamp"] + close_eqlbrm = close_row["equilibrium"] + 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_eqlbrm, + pair, + ), + ( + open_tstamp, + open_side_b, + pair.symbol_b_, + open_px_b, + open_eqlbrm, + pair, + ), + ( + close_tstamp, + close_side_a, + pair.symbol_a_, + close_px_a, + close_eqlbrm, + pair, + ), + ( + close_tstamp, + close_side_b, + pair.symbol_b_, + close_px_b, + close_eqlbrm, + pair, + ), + ] # Add tuples to data frame return pd.DataFrame( @@ -297,7 +401,6 @@ def create_trading_signals(vecm_fit, testing_pair_df, pair: Pair, colname_a, col columns=result_columns, ) - def run_single_pair(market_data: pd.DataFrame, price_column:str, pair: Pair) -> Optional[pd.DataFrame]: colname_a = f"{price_column}_{pair.symbol_a_}" colname_b = f"{price_column}_{pair.symbol_b_}" @@ -337,8 +440,6 @@ def run_single_pair(market_data: pd.DataFrame, price_column:str, pair: Pair) -> vecm_fit=vecm_fit, testing_pair_df=testing_pair_df, pair=pair, - colname_a=colname_a, - colname_b=colname_b ) except Exception as e: print(f"{pair}: Prediction failed: {str(e)}") @@ -346,49 +447,6 @@ def run_single_pair(market_data: pd.DataFrame, price_column:str, pair: Pair) -> return pair_trades - -def run_pairs(summaries_df: pd.DataFrame, price_column: str) -> None: - - result_df = transform_dataframe(df=summaries_df, price_column=price_column) - - stock_price_columns = [ - column - for column in result_df.columns - if column.startswith(f"{price_column}_") - ] - - # Find the starting indices for A and B - all_indexes = range(len(stock_price_columns)) - unique_index_pairs = [(i, j) for i in all_indexes for j in all_indexes if i < j] - - pairs_trades = [] - for a_index, b_index in unique_index_pairs: - # Get the actual variable names - colname_a = stock_price_columns[a_index] - colname_b = stock_price_columns[b_index] - - symbol_a = colname_a[len(f"{price_column}-") :] - symbol_b = colname_b[len(f"{price_column}-") :].replace( - "STOCK-", "" - ) - pair = Pair(symbol_a, symbol_b, price_column) - - single_pair_trades = run_single_pair(market_data=result_df, price_column=price_column, pair=pair) - if single_pair_trades is not None: - pairs_trades.append(single_pair_trades) - # Check if result_list has any data before concatenating - if not pairs_trades: - print("No trading signals found for any pairs") - return None - - result = pd.concat(pairs_trades, ignore_index=True) - result["time"] = pd.to_datetime(result["time"]) - result = result.set_index("time").sort_index() - - collect_single_day_results(result) - # print_single_day_results(result) - - def add_trade(pair, symbol, action, price): # Ensure we always use clean names without STOCK- prefix pair = str(pair).replace("STOCK-", "") @@ -431,7 +489,9 @@ def print_results_suummary(all_results): ) print(f"{filename}: {trade_count} trades") + def calculate_returns(all_results: Dict): + global TOTAL_REALIZED_PNL print("\n====== Returns By Day and Pair ======") for filename, data in all_results.items(): @@ -488,21 +548,101 @@ def calculate_returns(all_results: Dict): print(f" Pair Total Return: {pair_return:.2f}%") day_return += pair_return - # Print day total return + # Print day total return and add to global realized PnL if day_return != 0: print(f" Day Total Return: {day_return:.2f}%") + TOTAL_REALIZED_PNL += day_return + +def run_pairs(summaries_df: pd.DataFrame, price_column: str) -> None: + + result_df = transform_dataframe(df=summaries_df, price_column=price_column) + + stock_price_columns = [ + column + for column in result_df.columns + if column.startswith(f"{price_column}_") + ] + + # Find the starting indices for A and B + all_indexes = range(len(stock_price_columns)) + unique_index_pairs = [(i, j) for i in all_indexes for j in all_indexes if i < j] + + pairs_trades = [] + for a_index, b_index in unique_index_pairs: + # Get the actual variable names + colname_a = stock_price_columns[a_index] + colname_b = stock_price_columns[b_index] + + symbol_a = colname_a[len(f"{price_column}-") :] + symbol_b = colname_b[len(f"{price_column}-") :].replace( + "STOCK-", "" + ) + pair = Pair(symbol_a, symbol_b, price_column) + + single_pair_trades = run_single_pair(market_data=result_df, price_column=price_column, pair=pair) + if len(single_pair_trades) > 0: + pairs_trades.append(single_pair_trades) + # Check if result_list has any data before concatenating + if len(pairs_trades) == 0: + print("No trading signals found for any pairs") + return None + + result = pd.concat(pairs_trades, ignore_index=True) + result["time"] = pd.to_datetime(result["time"]) + result = result.set_index("time").sort_index() + + collect_single_day_results(result) + # print_single_day_results(result) + +def print_outstanding_positions(): + """Print all outstanding positions with share quantities and unrealized PnL""" + if not OUTSTANDING_POSITIONS: + print("\n====== NO OUTSTANDING POSITIONS ======") + return + + print(f"\n====== OUTSTANDING POSITIONS ======") + print(f"{'Pair':<15} {'Symbol':<6} {'Side':<4} {'Shares':<10} {'Open $':<8} {'Current $':<10} {'Unrealized $':<12} {'%':<8} {'Close Eq':<10}") + print("-" * 105) + + total_unrealized_dollar = 0.0 + + for pos in OUTSTANDING_POSITIONS: + # Print position A + print(f"{pos['pair']:<15} {pos['symbol_a']:<6} {pos['side_a']:<4} {pos['shares_a']:<10.2f} {pos['open_px_a']:<8.2f} {pos['current_px_a']:<10.2f} {pos['unrealized_dollar_a']:<12.2f} {pos['unrealized_dollar_a']/500*100:<8.2f} {'':<10}") + + # Print position B + print(f"{'':<15} {pos['symbol_b']:<6} {pos['side_b']:<4} {pos['shares_b']:<10.2f} {pos['open_px_b']:<8.2f} {pos['current_px_b']:<10.2f} {pos['unrealized_dollar_b']:<12.2f} {pos['unrealized_dollar_b']/500*100:<8.2f} {'':<10}") + + # Print pair totals with equilibrium info + equilibrium_status = "CLOSE" if pos['current_abs_term'] < pos['closing_threshold'] else f"{pos['equilibrium_ratio']:.2f}x" + print(f"{'':<15} {'PAIR':<6} {'TOT':<4} {'':<10} {'':<8} {'':<10} {pos['total_unrealized_dollar']:<12.2f} {pos['total_unrealized_dollar']/1000*100:<8.2f} {equilibrium_status:<10}") + + # Print equilibrium details + print(f"{'':<15} {'EQ':<6} {'INFO':<4} {'':<10} {'':<8} {'':<10} {'Curr:':<6}{pos['current_abs_term']:<6.4f} {'Thresh:':<7}{pos['closing_threshold']:<6.4f} {'':<10}") + print("-" * 105) + + total_unrealized_dollar += pos['total_unrealized_dollar'] + + print(f"{'TOTAL OUTSTANDING':<80} ${total_unrealized_dollar:<12.2f}") if __name__ == "__main__": # Initialize a dictionary to store all trade results all_results = {} + # Initialize global PnL tracking variables + TOTAL_REALIZED_PNL = 0.0 + TOTAL_UNREALIZED_PNL = 0.0 + OUTSTANDING_POSITIONS = [] + # Process each data file price_column = CONFIG["price_column"] for datafile in CONFIG["datafiles"]: print(f"\n====== Processing {datafile} ======") - # Clear the TRADES global dictionary for the new file + # Clear the TRADES global dictionary and reset unrealized PnL for the new file TRADES.clear() + TOTAL_UNREALIZED_PNL = 0.0 + TOTAL_REALIZED_PNL = 0.0 # Process data for this file try: @@ -516,8 +656,23 @@ if __name__ == "__main__": all_results[filename] = {"trades": TRADES.copy()} print(f"Successfully processed {filename}") + + # Print total unrealized PnL for this file + if TOTAL_UNREALIZED_PNL != 0: + print(f"\n====== TOTAL UNREALIZED PnL for {filename}: {TOTAL_UNREALIZED_PNL:.2f}% ======") + else: + print(f"\n====== No unrealized positions for {filename} ======") + except Exception as e: print(f"Error processing {datafile}: {str(e)}") # print_results_suummary(all_results) calculate_returns(all_results) + + # Print grand totals + print(f"\n====== GRAND TOTALS ACROSS ALL PAIRS ======") + print(f"Total Realized PnL: {TOTAL_REALIZED_PNL:.2f}%") + print(f"Total Unrealized PnL: {TOTAL_UNREALIZED_PNL:.2f}%") + print(f"Combined Total PnL: {TOTAL_REALIZED_PNL + TOTAL_UNREALIZED_PNL:.2f}%") + + print_outstanding_positions()