{ "cells": [ { "cell_type": "code", "execution_count": 18, "id": "c95e8240", "metadata": {}, "outputs": [], "source": [ "import os\n", "import pandas as pd\n", "import numpy as np\n", "\n", "# ============================================================\n", "# Backtesting Pair Trading Strategy (Version 2)\n", "# ============================================================\n", "# This version builds trading signals (based on rolling mean/std of spread)\n", "# and runs a backtest comparing the strategy against benchmarks:\n", "# - Buy & Hold BTC\n", "# - Buy & Hold ETH\n", "# - 50/50 Portfolio\n", "#\n", "# Signals are cached in CSV files under \"rolling_mean_cache/\" so that\n", "# they do not need to be recomputed every time.\n", "# ============================================================\n", "\n", "\n", "def build_signals(btc, eth, k=1, window_days=10, cache_dir=\"rolling_mean_cache\"):\n", " \"\"\"\n", " Build trading signals based on the spread between BTC and ETH.\n", "\n", " Parameters\n", " ----------\n", " btc : DataFrame\n", " BTC OHLCV data (must include 'open_time' and 'close').\n", " eth : DataFrame\n", " ETH OHLCV data (must include 'open_time' and 'close').\n", " k : int, optional\n", " Standard deviation multiplier for signal thresholds.\n", " window_days : int, optional\n", " Rolling window size in days.\n", " cache_dir : str, optional\n", " Directory to save signal CSV files.\n", "\n", " Returns\n", " -------\n", " str\n", " Path to the saved signal file.\n", " \"\"\"\n", " os.makedirs(cache_dir, exist_ok=True)\n", " output_path = os.path.join(cache_dir, f\"signals_k{k}_w{window_days}.csv\")\n", "\n", " df = pd.DataFrame({\n", " \"time\": pd.to_datetime(btc[\"open_time\"]),\n", " \"btc_close\": btc[\"close\"],\n", " \"eth_close\": eth[\"close\"]\n", " })\n", "\n", " # Spread\n", " df[\"spread\"] = df[\"btc_close\"] - df[\"eth_close\"]\n", "\n", " # Rolling mean & std\n", " window_size = 60 * 24 * window_days\n", " df[\"mean\"] = df[\"spread\"].rolling(window_size).mean()\n", " df[\"std\"] = df[\"spread\"].rolling(window_size).std()\n", "\n", " # Upper / lower bounds\n", " df[\"upper\"] = df[\"mean\"] + k * df[\"std\"]\n", " df[\"lower\"] = df[\"mean\"] - k * df[\"std\"]\n", "\n", " # Generate trading signals\n", " conditions = [\n", " df[\"spread\"] > df[\"upper\"],\n", " df[\"spread\"] < df[\"lower\"]\n", " ]\n", " choices = [\"sell_btc_buy_eth\", \"buy_btc_sell_eth\"]\n", " df[\"signal\"] = np.select(conditions, choices, default=\"hold\")\n", "\n", " df.to_csv(output_path, index=False)\n", " print(f\"✅ Signals saved to {output_path}\")\n", " return output_path\n", "\n", "\n", "def run_backtest(btc, eth, k=1, window_days=10,\n", " start_date=None, end_date=None,\n", " initial_capital=1000, trade_size=200,\n", " fee_rate=0.001, cache_dir=\"rolling_mean_cache\"):\n", " \"\"\"\n", " Run backtest for pair trading strategy and compare with benchmarks.\n", "\n", " Parameters\n", " ----------\n", " btc : DataFrame\n", " BTC OHLCV data.\n", " eth : DataFrame\n", " ETH OHLCV data.\n", " k : int\n", " Standard deviation multiplier for signal thresholds.\n", " window_days : int\n", " Rolling window size in days.\n", " start_date : str or None\n", " Backtest start date (inclusive).\n", " end_date : str or None\n", " Backtest end date (inclusive).\n", " initial_capital : float\n", " Starting portfolio value in USD.\n", " trade_size : float\n", " Trade size per signal in USD.\n", " fee_rate : float\n", " Transaction fee rate.\n", " cache_dir : str\n", " Directory where signals are stored.\n", "\n", " Returns\n", " -------\n", " dict\n", " Final portfolio values for benchmarks and strategy.\n", " Series\n", " Strategy equity curve.\n", " list\n", " Log of executed trades.\n", " DataFrame\n", " DataFrame with signals and spread data.\n", " \"\"\"\n", " signal_file = os.path.join(cache_dir, f\"signals_k{k}_w{window_days}.csv\")\n", "\n", " # Build signals if not cached\n", " if not os.path.exists(signal_file):\n", " print(f\"⚠️ Signal file {signal_file} not found → building...\")\n", " build_signals(btc, eth, k, window_days, cache_dir)\n", "\n", " # Load signals\n", " df = pd.read_csv(signal_file, parse_dates=[\"time\"])\n", "\n", " # Filter by date range\n", " if start_date:\n", " df = df[df[\"time\"] >= pd.to_datetime(start_date)]\n", " if end_date:\n", " df = df[df[\"time\"] <= pd.to_datetime(end_date)]\n", " df = df.reset_index(drop=True)\n", "\n", " # ------------------------------\n", " # Benchmarks\n", " # ------------------------------\n", " # 100% BTC\n", " bh_btc_units = initial_capital / df[\"btc_close\"].iloc[0]\n", " bh_btc_value = bh_btc_units * df[\"btc_close\"]\n", "\n", " # 100% ETH\n", " bh_eth_units = initial_capital / df[\"eth_close\"].iloc[0]\n", " bh_eth_value = bh_eth_units * df[\"eth_close\"]\n", "\n", " # 50/50 portfolio\n", " bh_half_btc = (initial_capital / 2) / df[\"btc_close\"].iloc[0]\n", " bh_half_eth = (initial_capital / 2) / df[\"eth_close\"].iloc[0]\n", " bh_equal_value = bh_half_btc * df[\"btc_close\"] + bh_half_eth * df[\"eth_close\"]\n", "\n", " # ------------------------------\n", " # Strategy\n", " # ------------------------------\n", " btc_units = (initial_capital / 2) / df[\"btc_close\"].iloc[0]\n", " eth_units = (initial_capital / 2) / df[\"eth_close\"].iloc[0]\n", " strat_values = []\n", " trade_log = []\n", "\n", " for _, row in df.iterrows():\n", " btc_price, eth_price, signal = row[\"btc_close\"], row[\"eth_close\"], row[\"signal\"]\n", " total_value = btc_units * btc_price + eth_units * eth_price\n", "\n", " if signal == \"sell_btc_buy_eth\":\n", " btc_value = btc_units * btc_price\n", " if btc_value > 0:\n", " sell_value = min(trade_size, btc_value)\n", " fee = sell_value * fee_rate\n", " btc_units -= sell_value / btc_price\n", " eth_units += (sell_value - fee) / eth_price\n", " trade_log.append((row[\"time\"], signal, total_value))\n", "\n", " elif signal == \"buy_btc_sell_eth\":\n", " eth_value = eth_units * eth_price\n", " if eth_value > 0:\n", " sell_value = min(trade_size, eth_value)\n", " fee = sell_value * fee_rate\n", " eth_units -= sell_value / eth_price\n", " btc_units += (sell_value - fee) / btc_price\n", " trade_log.append((row[\"time\"], signal, total_value))\n", "\n", " strat_values.append(btc_units * btc_price + eth_units * eth_price)\n", "\n", " strat_values = pd.Series(strat_values, index=df[\"time\"])\n", "\n", " # ------------------------------\n", " # Results summary\n", " # ------------------------------\n", " results = {\n", " \"BTC Buy&Hold\": bh_btc_value.iloc[-1],\n", " \"ETH Buy&Hold\": bh_eth_value.iloc[-1],\n", " \"50/50 Portfolio\": bh_equal_value.iloc[-1],\n", " \"Strategy\": strat_values.iloc[-1]\n", " }\n", "\n", " print(\"========== 📊 Backtest ==========\")\n", " print(f\"File: {signal_file}\")\n", " print(f\"Period: {start_date} → {end_date}\")\n", " print(f\"Initial Capital: ${initial_capital}\")\n", " for name, val in results.items():\n", " ret = (val / initial_capital - 1) * 100\n", " print(f\"{name:15s}: ${val:.2f} | Return: {ret:.2f}%\")\n", " print(f\"Total Trades: {len(trade_log)}\")\n", "\n", " return results, strat_values, trade_log, df\n" ] }, { "cell_type": "code", "execution_count": 21, "id": "3d92e6b8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "========== 📊 Backtest ==========\n", "File: rolling_mean_cache\\signals_k2.5_w7.csv\n", "Period: 2025-01-01 → 2025-09-01\n", "Initial Capital: $1000\n", "BTC Buy&Hold : $1156.21 | Return: 15.62%\n", "ETH Buy&Hold : $1311.57 | Return: 31.16%\n", "50/50 Portfolio: $1233.89 | Return: 23.39%\n", "Strategy : $2210.08 | Return: 121.01%\n", "Total Trades: 105\n" ] } ], "source": [ "# load data\n", "# btc = pd.read_csv(r\"C:\\Users\\amirs\\OneDrive\\Desktop\\myAlgoCode\\detector\\BTCUSDT_1m_1000d.csv\")\n", "# eth = pd.read_csv(r\"C:\\Users\\amirs\\OneDrive\\Desktop\\myAlgoCode\\detector\\ETHUSDT_1m_1000d.csv\")\n", "results, strat_values, trade_log, df = run_backtest(\n", " btc, eth,\n", " k=2.5, window_days=7,\n", " start_date=\"2025-01-01\", end_date=\"2025-09-01\",\n", " initial_capital=1000, fee_rate=0.001\n", ")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "aef6e2f7", "metadata": {}, "outputs": [], "source": [ "========== 📊 Backtest ==========\n", "File: rolling_mean_cache\\signals_k1.5_w7.csv\n", "Period: 2025-01-01 → 2025-09-01\n", "Initial Capital: $1000\n", "BTC Buy&Hold : $1156.21 | Return: 15.62%\n", "ETH Buy&Hold : $1311.57 | Return: 31.16%\n", "50/50 Portfolio: $1233.89 | Return: 23.39%\n", "Strategy : $1927.83 | Return: 92.78%\n", "Total Trades: 229" ] } ], "metadata": { "kernelspec": { "display_name": "base", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.7" } }, "nbformat": 4, "nbformat_minor": 5 }