1018 lines
153 KiB
Text
1018 lines
153 KiB
Text
{
| |||
"cells": [
| |||
{
| |||
"cell_type": "markdown",
| |||
"id": "c1f6c5db",
| |||
"metadata": {},
| |||
"source": [
| |||
"# Part 11 — Multi-Timeframe Causality Detection using Granger Analysis\n",
| |||
"\n",
| |||
"**Goal:** test whether returns of a *leading* (higher) timeframe Granger-cause returns of a *lagging* (lower) timeframe,\n",
| |||
"then export a calibrated causality table that the MQL5 **GrangerCausalityMonitor** EA reads and re-tests live.\n",
| |||
"\n",
| |||
"Pairs tested: `H1 → M15`, `M30 → M15`, `M15 → M5`, `H1 → M5`\n",
| |||
"\n",
| |||
"**Pipeline**\n",
| |||
"1. Load OHLC for every timeframe (MetaTrader5 package, CSV, or synthetic data)\n",
| |||
"2. Convert prices to log returns (bps) and confirm stationarity (ADF)\n",
| |||
"3. Align timeframes without look-ahead (only *completed* higher-TF bars are used)\n",
| |||
"4. Select lags by BIC, run the Granger F-test, cross-check with statsmodels + HC3 robust test\n",
| |||
"5. Rolling-window stability, out-of-sample check, Benjamini–Hochberg correction\n",
| |||
"6. Export `granger_mtf_causality.csv` to the MT5 **Common\\Files** folder"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": null,
| |||
"id": "c1b11aaf",
| |||
"metadata": {},
| |||
"outputs": [],
| |||
"source": [
| |||
"# Step 0 — install once (uncomment and run if needed)\n",
| |||
"# %pip install MetaTrader5 pandas numpy scipy statsmodels matplotlib"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 3,
| |||
"id": "261f2ab0-046f-4877-bb57-23a6e7bad76e",
| |||
"metadata": {},
| |||
"outputs": [
| |||
{
| |||
"name": "stdout",
| |||
"output_type": "stream",
| |||
"text": [
| |||
"Connected | Euro vs US Dollar | digits=5\n",
| |||
"Server: FBS MetaTrader 5\n",
| |||
"\n",
| |||
" M5: 30,524 bars 2026-04-24 → 2026-09-21 | close [1.13254, 1.17941] → ./data\\EURUSD.m_M5.csv\n",
| |||
" M15: 10,175 bars 2026-04-24 → 2026-09-21 | close [1.13254, 1.17885] → ./data\\EURUSD.m_M15.csv\n",
| |||
" M30: 5,087 bars 2026-04-24 → 2026-09-21 | close [1.13286, 1.17885] → ./data\\EURUSD.m_M30.csv\n",
| |||
" H1: 2,543 bars 2026-04-24 → 2026-09-21 | close [1.13286, 1.17885] → ./data\\EURUSD.m_H1.csv\n",
| |||
"\n",
| |||
"Done. 4/4 timeframes loaded.\n"
| |||
]
| |||
},
| |||
{
| |||
"data": {
| |||
"text/html": [
| |||
"<div>\n",
| |||
"<style scoped>\n",
| |||
" .dataframe tbody tr th:only-of-type {\n",
| |||
" vertical-align: middle;\n",
| |||
" }\n",
| |||
"\n",
| |||
" .dataframe tbody tr th {\n",
| |||
" vertical-align: top;\n",
| |||
" }\n",
| |||
"\n",
| |||
" .dataframe thead th {\n",
| |||
" text-align: right;\n",
| |||
" }\n",
| |||
"</style>\n",
| |||
"<table border=\"1\" class=\"dataframe\">\n",
| |||
" <thead>\n",
| |||
" <tr style=\"text-align: right;\">\n",
| |||
" <th></th>\n",
| |||
" <th>time</th>\n",
| |||
" <th>open</th>\n",
| |||
" <th>high</th>\n",
| |||
" <th>low</th>\n",
| |||
" <th>close</th>\n",
| |||
" <th>volume</th>\n",
| |||
" </tr>\n",
| |||
" </thead>\n",
| |||
" <tbody>\n",
| |||
" <tr>\n",
| |||
" <th>30521</th>\n",
| |||
" <td>2026-09-21 10:15:00+00:00</td>\n",
| |||
" <td>1.14723</td>\n",
| |||
" <td>1.14730</td>\n",
| |||
" <td>1.14715</td>\n",
| |||
" <td>1.14724</td>\n",
| |||
" <td>308</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>30522</th>\n",
| |||
" <td>2026-09-21 10:20:00+00:00</td>\n",
| |||
" <td>1.14724</td>\n",
| |||
" <td>1.14758</td>\n",
| |||
" <td>1.14723</td>\n",
| |||
" <td>1.14740</td>\n",
| |||
" <td>234</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>30523</th>\n",
| |||
" <td>2026-09-21 10:25:00+00:00</td>\n",
| |||
" <td>1.14740</td>\n",
| |||
" <td>1.14760</td>\n",
| |||
" <td>1.14740</td>\n",
| |||
" <td>1.14755</td>\n",
| |||
" <td>155</td>\n",
| |||
" </tr>\n",
| |||
" </tbody>\n",
| |||
"</table>\n",
| |||
"</div>"
| |||
],
| |||
"text/plain": [
| |||
" time open high low close volume\n",
| |||
"30521 2026-09-21 10:15:00+00:00 1.14723 1.14730 1.14715 1.14724 308\n",
| |||
"30522 2026-09-21 10:20:00+00:00 1.14724 1.14758 1.14723 1.14740 234\n",
| |||
"30523 2026-09-21 10:25:00+00:00 1.14740 1.14760 1.14740 1.14755 155"
| |||
]
| |||
},
| |||
"execution_count": 3,
| |||
"metadata": {},
| |||
"output_type": "execute_result"
| |||
}
| |||
],
| |||
"source": [
| |||
"# ---------------------------------------------------------------------\n",
| |||
"# CELL: Download EURUSD OHLC from MT5 for all required timeframes\n",
| |||
"# ---------------------------------------------------------------------\n",
| |||
"import MetaTrader5 as mt5\n",
| |||
"import pandas as pd\n",
| |||
"import os\n",
| |||
"from datetime import datetime, timedelta, timezone\n",
| |||
"\n",
| |||
"SYMBOL = \"EURUSD.m\"\n",
| |||
"DAYS = 150 # ← reduced so M5 stays under MT5's copy limit\n",
| |||
"SAVE = True\n",
| |||
"DATA_DIR = \"./data\"\n",
| |||
"\n",
| |||
"TF_MAP = {\n",
| |||
" \"M5\": mt5.TIMEFRAME_M5,\n",
| |||
" \"M15\": mt5.TIMEFRAME_M15,\n",
| |||
" \"M30\": mt5.TIMEFRAME_M30,\n",
| |||
" \"H1\": mt5.TIMEFRAME_H1,\n",
| |||
"}\n",
| |||
"\n",
| |||
"# --- connect \n",
| |||
"if not mt5.initialize():\n",
| |||
" raise RuntimeError(f\"MT5 initialize() failed: {mt5.last_error()}\")\n",
| |||
"\n",
| |||
"mt5.symbol_select(SYMBOL, True)\n",
| |||
"info = mt5.symbol_info(SYMBOL)\n",
| |||
"if info is None:\n",
| |||
" mt5.shutdown()\n",
| |||
" raise RuntimeError(f\"{SYMBOL} not found in Market Watch\")\n",
| |||
"\n",
| |||
"print(f\"Connected | {info.description} | digits={info.digits}\")\n",
| |||
"print(f\"Server: {mt5.terminal_info().name}\\n\")\n",
| |||
"\n",
| |||
"# --- date window (use copy_rates_range so MT5 handles chunking itself) ─\n",
| |||
"date_to = datetime.now(timezone.utc)\n",
| |||
"date_from = date_to - timedelta(days=DAYS)\n",
| |||
"\n",
| |||
"if SAVE:\n",
| |||
" os.makedirs(DATA_DIR, exist_ok=True)\n",
| |||
"\n",
| |||
"data = {}\n",
| |||
"for tf_name, tf_const in TF_MAP.items():\n",
| |||
" rates = mt5.copy_rates_range(SYMBOL, tf_const, date_from, date_to)\n",
| |||
"\n",
| |||
" if rates is None or len(rates) == 0:\n",
| |||
" err = mt5.last_error()\n",
| |||
" print(f\" {tf_name:>4}: NO DATA ({err})\")\n",
| |||
" continue\n",
| |||
"\n",
| |||
" df = pd.DataFrame(rates)\n",
| |||
" df[\"time\"] = pd.to_datetime(df[\"time\"], unit=\"s\", utc=True)\n",
| |||
" df = df.iloc[:-1].reset_index(drop=True) # drop forming bar\n",
| |||
" df = df[[\"time\", \"open\", \"high\", \"low\", \"close\", \"tick_volume\"]].copy()\n",
| |||
" df.rename(columns={\"tick_volume\": \"volume\"}, inplace=True)\n",
| |||
"\n",
| |||
" # sanity checks\n",
| |||
" assert (df[\"high\"] >= df[\"low\"]).all(), f\"{tf_name}: high < low\"\n",
| |||
" assert (df[\"close\"] > 0).all(), f\"{tf_name}: non-positive close\"\n",
| |||
" assert df[\"time\"].is_monotonic_increasing, f\"{tf_name}: timestamps not sorted\"\n",
| |||
"\n",
| |||
" data[tf_name] = df\n",
| |||
"\n",
| |||
" path = os.path.join(DATA_DIR, f\"{SYMBOL}_{tf_name}.csv\") if SAVE else None\n",
| |||
" if SAVE:\n",
| |||
" df.to_csv(path, index=False)\n",
| |||
"\n",
| |||
" print(f\" {tf_name:>4}: {len(df):>7,} bars \"\n",
| |||
" f\"{df['time'].iloc[0].strftime('%Y-%m-%d')} → \"\n",
| |||
" f\"{df['time'].iloc[-1].strftime('%Y-%m-%d')} \"\n",
| |||
" f\"| close [{df['close'].min():.5f}, {df['close'].max():.5f}]\"\n",
| |||
" + (f\" → {path}\" if SAVE else \"\"))\n",
| |||
"\n",
| |||
"mt5.shutdown()\n",
| |||
"print(f\"\\nDone. {len(data)}/{len(TF_MAP)} timeframes loaded.\")\n",
| |||
"\n",
| |||
"# --- verify all four are present before continuing \n",
| |||
"missing = [tf for tf in TF_MAP if tf not in data]\n",
| |||
"if missing:\n",
| |||
" raise RuntimeError(f\"Missing timeframes: {missing}. \"\n",
| |||
" \"Increase DAYS or check MT5 history depth.\")\n",
| |||
"\n",
| |||
"data[\"M5\"].tail(3)"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 14,
| |||
"id": "2e251ab4",
| |||
"metadata": {},
| |||
"outputs": [],
| |||
"source": [
| |||
"# Step 1 - Imports\n",
| |||
"import os, warnings\n",
| |||
"from datetime import datetime, timedelta, timezone\n",
| |||
"\n",
| |||
"import numpy as np\n",
| |||
"import pandas as pd\n",
| |||
"import matplotlib.pyplot as plt\n",
| |||
"from scipy import stats\n",
| |||
"import statsmodels.api as sm\n",
| |||
"from statsmodels.tsa.stattools import adfuller\n",
| |||
"from statsmodels.stats.multitest import multipletests\n",
| |||
"\n",
| |||
"warnings.filterwarnings(\"ignore\")\n",
| |||
"pd.set_option(\"display.width\", 180)\n",
| |||
"pd.set_option(\"display.max_columns\", 30)"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 15,
| |||
"id": "7e431d3b",
| |||
"metadata": {},
| |||
"outputs": [
| |||
{
| |||
"name": "stdout",
| |||
"output_type": "stream",
| |||
"text": [
| |||
"Timeframes needed: ['M5', 'M15', 'M30', 'H1']\n"
| |||
]
| |||
}
| |||
],
| |||
"source": [
| |||
"# Step 2 — Configuration\n",
| |||
"SYMBOL = \"EURUSD.m\"\n",
| |||
"DATA_SOURCE = \"mt5\" # \"mt5\", \"csv\" or \"synthetic\"\n",
| |||
"CSV_DIR = \"./data\" # used when DATA_SOURCE == \"csv\": files named SYMBOL_TF.csv (time,open,high,low,close)\n",
| |||
"DAYS_OF_HISTORY = 150\n",
| |||
"\n",
| |||
"PAIRS = [(\"H1\", \"M15\"), (\"M30\", \"M15\"), (\"M15\", \"M5\"), (\"H1\", \"M5\")] # (lead, lag)\n",
| |||
"\n",
| |||
"ALPHA = 0.05 # significance level (after BH correction)\n",
| |||
"MAX_P = 4 # max lags of the LEAD timeframe (in completed lead bars)\n",
| |||
"MAX_Q = 6 # max lags of the LAG timeframe (own returns)\n",
| |||
"WINDOW = 1500 # rolling window in lag-TF bars (the EA uses the same window live)\n",
| |||
"STEP = 250 # rolling step\n",
| |||
"STABILITY_MIN = 0.35 # share of rolling windows that must be significant\n",
| |||
"TRAIN_FRAC = 0.70 # chronological train/test split\n",
| |||
"REQUIRE_OOS_EDGE = False # if True, VALID also needs a positive out-of-sample directional edge\n",
| |||
"SCALE = 1e4 # returns expressed in basis points\n",
| |||
"OUTPUT_FILE = \"granger_mtf_causality.csv\"\n",
| |||
"\n",
| |||
"TF_MINUTES = {\"M1\": 1, \"M5\": 5, \"M15\": 15, \"M30\": 30, \"H1\": 60, \"H4\": 240, \"D1\": 1440}\n",
| |||
"TFS = sorted({tf for pair in PAIRS for tf in pair}, key=lambda t: TF_MINUTES[t])\n",
| |||
"print(\"Timeframes needed:\", TFS)"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 16,
| |||
"id": "eaf797c8",
| |||
"metadata": {},
| |||
"outputs": [
| |||
{
| |||
"name": "stdout",
| |||
"output_type": "stream",
| |||
"text": [
| |||
" M5: 30560 bars 2026-04-24 10:50:00 -> 2026-09-21 13:40:00\n",
| |||
" M15: 10187 bars 2026-04-24 11:00:00 -> 2026-09-21 13:30:00\n",
| |||
" M30: 5093 bars 2026-04-24 11:00:00 -> 2026-09-21 13:00:00\n",
| |||
" H1: 2546 bars 2026-04-24 11:00:00 -> 2026-09-21 12:00:00\n"
| |||
]
| |||
}
| |||
],
| |||
"source": [
| |||
"# Step 3 --- Data loaders\n",
| |||
"def load_mt5(symbol, tf, days):\n",
| |||
" import MetaTrader5 as mt5\n",
| |||
" tf_map = {\"M1\": mt5.TIMEFRAME_M1, \"M5\": mt5.TIMEFRAME_M5, \"M15\": mt5.TIMEFRAME_M15,\n",
| |||
" \"M30\": mt5.TIMEFRAME_M30, \"H1\": mt5.TIMEFRAME_H1, \"H4\": mt5.TIMEFRAME_H4,\n",
| |||
" \"D1\": mt5.TIMEFRAME_D1}\n",
| |||
" date_to = datetime.now(timezone.utc) + timedelta(days=1)\n",
| |||
" date_from = date_to - timedelta(days=days + 1)\n",
| |||
" rates = mt5.copy_rates_range(symbol, tf_map[tf], date_from, date_to)\n",
| |||
" if rates is None or len(rates) == 0:\n",
| |||
" raise RuntimeError(f\"No {tf} data for {symbol}: {mt5.last_error()}\")\n",
| |||
" df = pd.DataFrame(rates)\n",
| |||
" df[\"time\"] = pd.to_datetime(df[\"time\"], unit=\"s\")\n",
| |||
" # drop the last (still forming) bar so every row is a closed bar\n",
| |||
" return df[[\"time\", \"open\", \"high\", \"low\", \"close\"]].iloc[:-1].reset_index(drop=True)\n",
| |||
"\n",
| |||
"\n",
| |||
"def load_csv(symbol, tf):\n",
| |||
" df = pd.read_csv(os.path.join(CSV_DIR, f\"{symbol}_{tf}.csv\"), parse_dates=[\"time\"])\n",
| |||
" return df[[\"time\", \"open\", \"high\", \"low\", \"close\"]].sort_values(\"time\").reset_index(drop=True)\n",
| |||
"\n",
| |||
"\n",
| |||
"def make_synthetic(days, seed=11):\n",
| |||
" # M5 random walk with a slow persistent drift -> real multi-timeframe structure for testing\n",
| |||
" rng = np.random.default_rng(seed)\n",
| |||
" n = days * 24 * 12\n",
| |||
" t = pd.date_range(\"2025-01-01\", periods=n, freq=\"5min\")\n",
| |||
" slow = np.zeros(n)\n",
| |||
" shocks = rng.standard_normal(n)\n",
| |||
" for i in range(1, n):\n",
| |||
" slow[i] = 0.995 * slow[i - 1] + 0.1 * shocks[i]\n",
| |||
" r = 0.6e-4 * np.r_[0, slow[:-1]] + 4e-4 * rng.standard_normal(n)\n",
| |||
" close = 2000 * np.exp(np.cumsum(r))\n",
| |||
" m5 = pd.DataFrame({\"time\": t, \"open\": np.r_[close[0], close[:-1]], \"close\": close})\n",
| |||
" m5[\"high\"] = m5[[\"open\", \"close\"]].max(axis=1)\n",
| |||
" m5[\"low\"] = m5[[\"open\", \"close\"]].min(axis=1)\n",
| |||
" out = {}\n",
| |||
" for tf in TFS:\n",
| |||
" if tf == \"M5\":\n",
| |||
" out[tf] = m5[[\"time\", \"open\", \"high\", \"low\", \"close\"]].copy()\n",
| |||
" continue\n",
| |||
" agg = (m5.set_index(\"time\")\n",
| |||
" .resample(f\"{TF_MINUTES[tf]}min\", label=\"left\", closed=\"left\")\n",
| |||
" .agg({\"open\": \"first\", \"high\": \"max\", \"low\": \"min\", \"close\": \"last\"})\n",
| |||
" .dropna().reset_index())\n",
| |||
" out[tf] = agg\n",
| |||
" return out\n",
| |||
"\n",
| |||
"\n",
| |||
"if DATA_SOURCE == \"mt5\":\n",
| |||
" import MetaTrader5 as mt5\n",
| |||
" if not mt5.initialize():\n",
| |||
" raise RuntimeError(f\"MT5 initialize() failed: {mt5.last_error()}\")\n",
| |||
" mt5.symbol_select(SYMBOL, True)\n",
| |||
" data = {tf: load_mt5(SYMBOL, tf, DAYS_OF_HISTORY) for tf in TFS}\n",
| |||
"elif DATA_SOURCE == \"csv\":\n",
| |||
" data = {tf: load_csv(SYMBOL, tf) for tf in TFS}\n",
| |||
"else:\n",
| |||
" data = make_synthetic(DAYS_OF_HISTORY)\n",
| |||
"\n",
| |||
"for tf in TFS:\n",
| |||
" d = data[tf]\n",
| |||
" print(f\"{tf:>4}: {len(d):>7} bars {d['time'].iloc[0]} -> {d['time'].iloc[-1]}\")"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 17,
| |||
"id": "c7a4ea9b",
| |||
"metadata": {},
| |||
"outputs": [
| |||
{
| |||
"data": {
| |||
"text/html": [
| |||
"<div>\n",
| |||
"<style scoped>\n",
| |||
" .dataframe tbody tr th:only-of-type {\n",
| |||
" vertical-align: middle;\n",
| |||
" }\n",
| |||
"\n",
| |||
" .dataframe tbody tr th {\n",
| |||
" vertical-align: top;\n",
| |||
" }\n",
| |||
"\n",
| |||
" .dataframe thead th {\n",
| |||
" text-align: right;\n",
| |||
" }\n",
| |||
"</style>\n",
| |||
"<table border=\"1\" class=\"dataframe\">\n",
| |||
" <thead>\n",
| |||
" <tr style=\"text-align: right;\">\n",
| |||
" <th></th>\n",
| |||
" <th>ADF p (price)</th>\n",
| |||
" <th>ADF p (returns)</th>\n",
| |||
" <th>ret std (bps)</th>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>tf</th>\n",
| |||
" <th></th>\n",
| |||
" <th></th>\n",
| |||
" <th></th>\n",
| |||
" </tr>\n",
| |||
" </thead>\n",
| |||
" <tbody>\n",
| |||
" <tr>\n",
| |||
" <th>M5</th>\n",
| |||
" <td>0.814968</td>\n",
| |||
" <td>0.0</td>\n",
| |||
" <td>1.576934</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>M15</th>\n",
| |||
" <td>0.550736</td>\n",
| |||
" <td>0.0</td>\n",
| |||
" <td>2.895177</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>M30</th>\n",
| |||
" <td>0.381284</td>\n",
| |||
" <td>0.0</td>\n",
| |||
" <td>4.459110</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>H1</th>\n",
| |||
" <td>0.382926</td>\n",
| |||
" <td>0.0</td>\n",
| |||
" <td>6.354726</td>\n",
| |||
" </tr>\n",
| |||
" </tbody>\n",
| |||
"</table>\n",
| |||
"</div>"
| |||
],
| |||
"text/plain": [
| |||
" ADF p (price) ADF p (returns) ret std (bps)\n",
| |||
"tf \n",
| |||
"M5 0.814968 0.0 1.576934\n",
| |||
"M15 0.550736 0.0 2.895177\n",
| |||
"M30 0.381284 0.0 4.459110\n",
| |||
"H1 0.382926 0.0 6.354726"
| |||
]
| |||
},
| |||
"execution_count": 17,
| |||
"metadata": {},
| |||
"output_type": "execute_result"
| |||
}
| |||
],
| |||
"source": [
| |||
"# Step 4 — Log returns (bps) and stationarity check\n",
| |||
"def add_returns(df):\n",
| |||
" df = df.copy()\n",
| |||
" df[\"ret\"] = SCALE * np.log(df[\"close\"] / df[\"close\"].shift(1))\n",
| |||
" return df\n",
| |||
"\n",
| |||
"data = {tf: add_returns(df) for tf, df in data.items()}\n",
| |||
"\n",
| |||
"rows = []\n",
| |||
"for tf in TFS:\n",
| |||
" px = data[tf][\"close\"].tail(5000)\n",
| |||
" rt = data[tf][\"ret\"].dropna().tail(5000)\n",
| |||
" rows.append({\"tf\": tf,\n",
| |||
" \"ADF p (price)\": adfuller(px, autolag=\"AIC\")[1],\n",
| |||
" \"ADF p (returns)\": adfuller(rt, autolag=\"AIC\")[1],\n",
| |||
" \"ret std (bps)\": rt.std()})\n",
| |||
"adf_table = pd.DataFrame(rows).set_index(\"tf\")\n",
| |||
"adf_table"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 18,
| |||
"id": "9610bd95",
| |||
"metadata": {},
| |||
"outputs": [
| |||
{
| |||
"name": "stdout",
| |||
"output_type": "stream",
| |||
"text": [
| |||
" H1 -> M15 rows: 10167\n",
| |||
" M30 -> M15 rows: 10177\n",
| |||
" M15 -> M5 rows: 30543\n",
| |||
" H1 -> M5 rows: 30498\n"
| |||
]
| |||
},
| |||
{
| |||
"data": {
| |||
"text/html": [
| |||
"<div>\n",
| |||
"<style scoped>\n",
| |||
" .dataframe tbody tr th:only-of-type {\n",
| |||
" vertical-align: middle;\n",
| |||
" }\n",
| |||
"\n",
| |||
" .dataframe tbody tr th {\n",
| |||
" vertical-align: top;\n",
| |||
" }\n",
| |||
"\n",
| |||
" .dataframe thead th {\n",
| |||
" text-align: right;\n",
| |||
" }\n",
| |||
"</style>\n",
| |||
"<table border=\"1\" class=\"dataframe\">\n",
| |||
" <thead>\n",
| |||
" <tr style=\"text-align: right;\">\n",
| |||
" <th></th>\n",
| |||
" <th>y</th>\n",
| |||
" <th>ylag1</th>\n",
| |||
" <th>ylag2</th>\n",
| |||
" <th>ylag3</th>\n",
| |||
" <th>ylag4</th>\n",
| |||
" <th>ylag5</th>\n",
| |||
" <th>ylag6</th>\n",
| |||
" <th>xlag1</th>\n",
| |||
" <th>xlag2</th>\n",
| |||
" <th>xlag3</th>\n",
| |||
" <th>xlag4</th>\n",
| |||
" </tr>\n",
| |||
" </thead>\n",
| |||
" <tbody>\n",
| |||
" <tr>\n",
| |||
" <th>2026-04-24 16:00:00</th>\n",
| |||
" <td>-0.597884</td>\n",
| |||
" <td>1.110385</td>\n",
| |||
" <td>-5.294575</td>\n",
| |||
" <td>-1.536610</td>\n",
| |||
" <td>0.768275</td>\n",
| |||
" <td>-1.792551</td>\n",
| |||
" <td>-1.194855</td>\n",
| |||
" <td>-4.952524</td>\n",
| |||
" <td>9.651150</td>\n",
| |||
" <td>14.365853</td>\n",
| |||
" <td>-2.909041</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>2026-04-24 16:15:00</th>\n",
| |||
" <td>-4.955274</td>\n",
| |||
" <td>-0.597884</td>\n",
| |||
" <td>1.110385</td>\n",
| |||
" <td>-5.294575</td>\n",
| |||
" <td>-1.536610</td>\n",
| |||
" <td>0.768275</td>\n",
| |||
" <td>-1.792551</td>\n",
| |||
" <td>-4.952524</td>\n",
| |||
" <td>9.651150</td>\n",
| |||
" <td>14.365853</td>\n",
| |||
" <td>-2.909041</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>2026-04-24 16:30:00</th>\n",
| |||
" <td>1.367218</td>\n",
| |||
" <td>-4.955274</td>\n",
| |||
" <td>-0.597884</td>\n",
| |||
" <td>1.110385</td>\n",
| |||
" <td>-5.294575</td>\n",
| |||
" <td>-1.536610</td>\n",
| |||
" <td>0.768275</td>\n",
| |||
" <td>-4.952524</td>\n",
| |||
" <td>9.651150</td>\n",
| |||
" <td>14.365853</td>\n",
| |||
" <td>-2.909041</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>2026-04-24 16:45:00</th>\n",
| |||
" <td>2.563029</td>\n",
| |||
" <td>1.367218</td>\n",
| |||
" <td>-4.955274</td>\n",
| |||
" <td>-0.597884</td>\n",
| |||
" <td>1.110385</td>\n",
| |||
" <td>-5.294575</td>\n",
| |||
" <td>-1.536610</td>\n",
| |||
" <td>-4.952524</td>\n",
| |||
" <td>9.651150</td>\n",
| |||
" <td>14.365853</td>\n",
| |||
" <td>-2.909041</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>2026-04-24 17:00:00</th>\n",
| |||
" <td>3.416351</td>\n",
| |||
" <td>2.563029</td>\n",
| |||
" <td>1.367218</td>\n",
| |||
" <td>-4.955274</td>\n",
| |||
" <td>-0.597884</td>\n",
| |||
" <td>1.110385</td>\n",
| |||
" <td>-5.294575</td>\n",
| |||
" <td>-1.622912</td>\n",
| |||
" <td>-4.952524</td>\n",
| |||
" <td>9.651150</td>\n",
| |||
" <td>14.365853</td>\n",
| |||
" </tr>\n",
| |||
" </tbody>\n",
| |||
"</table>\n",
| |||
"</div>"
| |||
],
| |||
"text/plain": [
| |||
" y ylag1 ylag2 ylag3 ylag4 ylag5 ylag6 xlag1 xlag2 xlag3 xlag4\n",
| |||
"2026-04-24 16:00:00 -0.597884 1.110385 -5.294575 -1.536610 0.768275 -1.792551 -1.194855 -4.952524 9.651150 14.365853 -2.909041\n",
| |||
"2026-04-24 16:15:00 -4.955274 -0.597884 1.110385 -5.294575 -1.536610 0.768275 -1.792551 -4.952524 9.651150 14.365853 -2.909041\n",
| |||
"2026-04-24 16:30:00 1.367218 -4.955274 -0.597884 1.110385 -5.294575 -1.536610 0.768275 -4.952524 9.651150 14.365853 -2.909041\n",
| |||
"2026-04-24 16:45:00 2.563029 1.367218 -4.955274 -0.597884 1.110385 -5.294575 -1.536610 -4.952524 9.651150 14.365853 -2.909041\n",
| |||
"2026-04-24 17:00:00 3.416351 2.563029 1.367218 -4.955274 -0.597884 1.110385 -5.294575 -1.622912 -4.952524 9.651150 14.365853"
| |||
]
| |||
},
| |||
"execution_count": 18,
| |||
"metadata": {},
| |||
"output_type": "execute_result"
| |||
}
| |||
],
| |||
"source": [
| |||
"# Step 5 — Look-ahead-free alignment\n",
| |||
"# For every lag-TF bar opening at time t we only use lead-TF bars that CLOSED at or before t,\n",
| |||
"# i.e. lead bars whose open time <= t - lead_period.\n",
| |||
"def build_design(tf_lead, tf_lag, p_max=MAX_P, q_max=MAX_Q):\n",
| |||
" lo, hi = data[tf_lag], data[tf_lead]\n",
| |||
" per_h = pd.Timedelta(minutes=TF_MINUTES[tf_lead])\n",
| |||
" y = lo[\"ret\"].to_numpy()\n",
| |||
" hret = hi[\"ret\"].to_numpy()\n",
| |||
" hpos = np.searchsorted(hi[\"time\"].to_numpy(), (lo[\"time\"] - per_h).to_numpy(), side=\"right\") - 1\n",
| |||
" i = np.arange(len(lo))\n",
| |||
" ok = (i - q_max >= 1) & (hpos - (p_max - 1) >= 1)\n",
| |||
" idx, hp = i[ok], hpos[ok]\n",
| |||
" cols = {\"y\": y[idx]}\n",
| |||
" for j in range(1, q_max + 1):\n",
| |||
" cols[f\"ylag{j}\"] = y[idx - j] # own lags (lag-TF bars)\n",
| |||
" for k in range(1, p_max + 1):\n",
| |||
" cols[f\"xlag{k}\"] = hret[hp - (k - 1)] # last k completed lead-TF bars\n",
| |||
" return pd.DataFrame(cols, index=lo[\"time\"].to_numpy()[idx]).dropna()\n",
| |||
"\n",
| |||
"designs = {pair: build_design(*pair) for pair in PAIRS}\n",
| |||
"for pair, D in designs.items():\n",
| |||
" print(f\"{pair[0]:>4} -> {pair[1]:<4} rows: {len(D)}\")\n",
| |||
"designs[PAIRS[0]].head()"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 19,
| |||
"id": "f05cc7ff",
| |||
"metadata": {},
| |||
"outputs": [],
| |||
"source": [
| |||
"# Step 6 — Granger F-test (restricted vs unrestricted OLS) and BIC lag selection\n",
| |||
"def design_matrices(D, p, q):\n",
| |||
" Xr = np.column_stack([np.ones(len(D))] + [D[f\"ylag{j}\"].to_numpy() for j in range(1, q + 1)])\n",
| |||
" Xu = np.column_stack([Xr] + [D[f\"xlag{k}\"].to_numpy() for k in range(1, p + 1)])\n",
| |||
" return Xr, Xu, D[\"y\"].to_numpy()\n",
| |||
"\n",
| |||
"\n",
| |||
"def ols(X, y):\n",
| |||
" beta, *_ = np.linalg.lstsq(X, y, rcond=None)\n",
| |||
" e = y - X @ beta\n",
| |||
" return beta, float(e @ e)\n",
| |||
"\n",
| |||
"\n",
| |||
"def granger_test(D, p, q):\n",
| |||
" Xr, Xu, y = design_matrices(D, p, q)\n",
| |||
" beta_u, rss_u = ols(Xu, y)\n",
| |||
" beta_r, rss_r = ols(Xr, y)\n",
| |||
" n, k = Xu.shape\n",
| |||
" df2 = n - k\n",
| |||
" F = max(((rss_r - rss_u) / p) / (rss_u / df2), 0.0)\n",
| |||
" return {\"F\": F, \"p_value\": float(stats.f.sf(F, p, df2)), \"df2\": df2,\n",
| |||
" \"beta_u\": beta_u, \"beta_r\": beta_r, \"sigma_y\": float(np.std(y))}\n",
| |||
"\n",
| |||
"\n",
| |||
"def select_lags(D):\n",
| |||
" best = None\n",
| |||
" for q in range(1, MAX_Q + 1):\n",
| |||
" for p in range(1, MAX_P + 1):\n",
| |||
" _, Xu, y = design_matrices(D, p, q)\n",
| |||
" _, rss = ols(Xu, y)\n",
| |||
" n, k = Xu.shape\n",
| |||
" bic = n * np.log(rss / n) + k * np.log(n)\n",
| |||
" if best is None or bic < best[0]:\n",
| |||
" best = (bic, p, q)\n",
| |||
" return best[1], best[2]"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 20,
| |||
"id": "4b8d984e",
| |||
"metadata": {},
| |||
"outputs": [
| |||
{
| |||
"data": {
| |||
"text/html": [
| |||
"<div>\n",
| |||
"<style scoped>\n",
| |||
" .dataframe tbody tr th:only-of-type {\n",
| |||
" vertical-align: middle;\n",
| |||
" }\n",
| |||
"\n",
| |||
" .dataframe tbody tr th {\n",
| |||
" vertical-align: top;\n",
| |||
" }\n",
| |||
"\n",
| |||
" .dataframe thead th {\n",
| |||
" text-align: right;\n",
| |||
" }\n",
| |||
"</style>\n",
| |||
"<table border=\"1\" class=\"dataframe\">\n",
| |||
" <thead>\n",
| |||
" <tr style=\"text-align: right;\">\n",
| |||
" <th></th>\n",
| |||
" <th>lead_tf</th>\n",
| |||
" <th>lag_tf</th>\n",
| |||
" <th>p</th>\n",
| |||
" <th>q</th>\n",
| |||
" <th>window</th>\n",
| |||
" <th>alpha</th>\n",
| |||
" <th>f_stat</th>\n",
| |||
" <th>p_value</th>\n",
| |||
" <th>p_statsmodels</th>\n",
| |||
" <th>p_hc3</th>\n",
| |||
" <th>stability</th>\n",
| |||
" <th>oos_hit</th>\n",
| |||
" <th>oos_hit_base</th>\n",
| |||
" <th>oos_edge_bps</th>\n",
| |||
" <th>oos_r2_gain</th>\n",
| |||
" <th>n_train</th>\n",
| |||
" <th>p_adj</th>\n",
| |||
" <th>status</th>\n",
| |||
" </tr>\n",
| |||
" </thead>\n",
| |||
" <tbody>\n",
| |||
" <tr>\n",
| |||
" <th>0</th>\n",
| |||
" <td>H1</td>\n",
| |||
" <td>M15</td>\n",
| |||
" <td>1</td>\n",
| |||
" <td>1</td>\n",
| |||
" <td>1500</td>\n",
| |||
" <td>0.05</td>\n",
| |||
" <td>0.00119</td>\n",
| |||
" <td>0.97252</td>\n",
| |||
" <td>0.97252</td>\n",
| |||
" <td>0.98082</td>\n",
| |||
" <td>0.08571</td>\n",
| |||
" <td>0.52521</td>\n",
| |||
" <td>0.52254</td>\n",
| |||
" <td>0.14268</td>\n",
| |||
" <td>-0.00000</td>\n",
| |||
" <td>7116</td>\n",
| |||
" <td>0.97252</td>\n",
| |||
" <td>INVALID</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>1</th>\n",
| |||
" <td>M30</td>\n",
| |||
" <td>M15</td>\n",
| |||
" <td>1</td>\n",
| |||
" <td>2</td>\n",
| |||
" <td>1500</td>\n",
| |||
" <td>0.05</td>\n",
| |||
" <td>9.64243</td>\n",
| |||
" <td>0.00191</td>\n",
| |||
" <td>0.00191</td>\n",
| |||
" <td>0.11002</td>\n",
| |||
" <td>0.42857</td>\n",
| |||
" <td>0.49967</td>\n",
| |||
" <td>0.51901</td>\n",
| |||
" <td>-0.03522</td>\n",
| |||
" <td>-0.00575</td>\n",
| |||
" <td>7123</td>\n",
| |||
" <td>0.00764</td>\n",
| |||
" <td>VALID</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>2</th>\n",
| |||
" <td>M15</td>\n",
| |||
" <td>M5</td>\n",
| |||
" <td>1</td>\n",
| |||
" <td>1</td>\n",
| |||
" <td>1500</td>\n",
| |||
" <td>0.05</td>\n",
| |||
" <td>4.84478</td>\n",
| |||
" <td>0.02774</td>\n",
| |||
" <td>0.02774</td>\n",
| |||
" <td>0.21685</td>\n",
| |||
" <td>0.19658</td>\n",
| |||
" <td>0.50703</td>\n",
| |||
" <td>0.50657</td>\n",
| |||
" <td>0.04416</td>\n",
| |||
" <td>0.00066</td>\n",
| |||
" <td>21380</td>\n",
| |||
" <td>0.05548</td>\n",
| |||
" <td>INVALID</td>\n",
| |||
" </tr>\n",
| |||
" <tr>\n",
| |||
" <th>3</th>\n",
| |||
" <td>H1</td>\n",
| |||
" <td>M5</td>\n",
| |||
" <td>1</td>\n",
| |||
" <td>1</td>\n",
| |||
" <td>1500</td>\n",
| |||
" <td>0.05</td>\n",
| |||
" <td>0.09290</td>\n",
| |||
" <td>0.76053</td>\n",
| |||
" <td>0.76053</td>\n",
| |||
" <td>0.83955</td>\n",
| |||
" <td>0.06034</td>\n",
| |||
" <td>0.50492</td>\n",
| |||
" <td>0.50412</td>\n",
| |||
" <td>0.00523</td>\n",
| |||
" <td>-0.00000</td>\n",
| |||
" <td>21348</td>\n",
| |||
" <td>0.97252</td>\n",
| |||
" <td>INVALID</td>\n",
| |||
" </tr>\n",
| |||
" </tbody>\n",
| |||
"</table>\n",
| |||
"</div>"
| |||
],
| |||
"text/plain": [
| |||
" lead_tf lag_tf p q window alpha f_stat p_value p_statsmodels p_hc3 stability oos_hit oos_hit_base oos_edge_bps oos_r2_gain n_train p_adj status\n",
| |||
"0 H1 M15 1 1 1500 0.05 0.00119 0.97252 0.97252 0.98082 0.08571 0.52521 0.52254 0.14268 -0.00000 7116 0.97252 INVALID\n",
| |||
"1 M30 M15 1 2 1500 0.05 9.64243 0.00191 0.00191 0.11002 0.42857 0.49967 0.51901 -0.03522 -0.00575 7123 0.00764 VALID\n",
| |||
"2 M15 M5 1 1 1500 0.05 4.84478 0.02774 0.02774 0.21685 0.19658 0.50703 0.50657 0.04416 0.00066 21380 0.05548 INVALID\n",
| |||
"3 H1 M5 1 1 1500 0.05 0.09290 0.76053 0.76053 0.83955 0.06034 0.50492 0.50412 0.00523 -0.00000 21348 0.97252 INVALID"
| |||
]
| |||
},
| |||
"execution_count": 20,
| |||
"metadata": {},
| |||
"output_type": "execute_result"
| |||
}
| |||
],
| |||
"source": [
| |||
"# Step 7 — Full evaluation per pair: train test, cross-checks, rolling stability, out-of-sample\n",
| |||
"results, rolling = [], {}\n",
| |||
"\n",
| |||
"for (lead, lag), D in designs.items():\n",
| |||
" cut = int(len(D) * TRAIN_FRAC)\n",
| |||
" D_tr, D_te = D.iloc[:cut], D.iloc[cut:]\n",
| |||
" p, q = select_lags(D_tr)\n",
| |||
" g = granger_test(D_tr, p, q)\n",
| |||
"\n",
| |||
" # cross-check 1: statsmodels nested F-test must match ours\n",
| |||
" Xr, Xu, y = design_matrices(D_tr, p, q)\n",
| |||
" fit_u, fit_r = sm.OLS(y, Xu).fit(), sm.OLS(y, Xr).fit()\n",
| |||
" F_sm, p_sm, _ = fit_u.compare_f_test(fit_r)\n",
| |||
"\n",
| |||
" # cross-check 2: heteroskedasticity-robust (HC3) Wald test on the lead-TF coefficients\n",
| |||
" R = np.zeros((p, Xu.shape[1]))\n",
| |||
" R[:, 1 + q:] = np.eye(p)\n",
| |||
" p_hc3 = float(sm.OLS(y, Xu).fit(cov_type=\"HC3\").f_test(R).pvalue)\n",
| |||
"\n",
| |||
" # rolling stability over the whole sample, same window the EA uses live\n",
| |||
" pv, ts = [], []\n",
| |||
" for s in range(0, len(D) - WINDOW + 1, STEP):\n",
| |||
" w = D.iloc[s:s + WINDOW]\n",
| |||
" pv.append(granger_test(w, p, q)[\"p_value\"])\n",
| |||
" ts.append(w.index[-1])\n",
| |||
" rolling[(lead, lag)] = pd.Series(pv, index=pd.to_datetime(ts))\n",
| |||
" stability = float(np.mean(np.array(pv) < ALPHA)) if pv else 0.0\n",
| |||
"\n",
| |||
" # out-of-sample: train coefficients applied to unseen data\n",
| |||
" Xr_te, Xu_te, y_te = design_matrices(D_te, p, q)\n",
| |||
" pred_u, pred_r = Xu_te @ g[\"beta_u\"], Xr_te @ g[\"beta_r\"]\n",
| |||
" nz = y_te != 0\n",
| |||
" oos_hit = float(np.mean(np.sign(pred_u[nz]) == np.sign(y_te[nz])))\n",
| |||
" oos_hit_base = float(np.mean(np.sign(pred_r[nz]) == np.sign(y_te[nz])))\n",
| |||
" oos_edge = float(np.mean(np.sign(pred_u) * y_te))\n",
| |||
" oos_r2_gain = 1 - np.sum((y_te - pred_u) ** 2) / np.sum((y_te - pred_r) ** 2)\n",
| |||
"\n",
| |||
" results.append({\"lead_tf\": lead, \"lag_tf\": lag, \"p\": p, \"q\": q, \"window\": WINDOW, \"alpha\": ALPHA,\n",
| |||
" \"f_stat\": g[\"F\"], \"p_value\": g[\"p_value\"], \"p_statsmodels\": p_sm, \"p_hc3\": p_hc3,\n",
| |||
" \"stability\": stability, \"oos_hit\": oos_hit, \"oos_hit_base\": oos_hit_base,\n",
| |||
" \"oos_edge_bps\": oos_edge, \"oos_r2_gain\": oos_r2_gain, \"n_train\": len(D_tr)})\n",
| |||
"\n",
| |||
"res = pd.DataFrame(results)\n",
| |||
"res[\"p_adj\"] = multipletests(res[\"p_value\"], alpha=ALPHA, method=\"fdr_bh\")[1]\n",
| |||
"ok = (res[\"p_adj\"] < ALPHA) & (res[\"stability\"] >= STABILITY_MIN)\n",
| |||
"if REQUIRE_OOS_EDGE:\n",
| |||
" ok &= res[\"oos_edge_bps\"] > 0\n",
| |||
"res[\"status\"] = np.where(ok, \"VALID\", \"INVALID\")\n",
| |||
"\n",
| |||
"assert np.allclose(res[\"p_value\"], res[\"p_statsmodels\"], atol=1e-8), \"F-test mismatch with statsmodels\"\n",
| |||
"res.round(5)"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 21,
| |||
"id": "73f27e8c",
| |||
"metadata": {},
| |||
"outputs": [
| |||
{
| |||
"name": "stdout",
| |||
"output_type": "stream",
| |||
"text": [
| |||
"MULTI-TIMEFRAME CAUSALITY\n",
| |||
" H1 → M15 INVALID p_adj=0.9725 F= 0.00 stab=9% lags(p=1, q=1) OOS hit=52.5% vs 52.3%\n",
| |||
" M30 → M15 VALID p_adj=0.0076 F= 9.64 stab=43% lags(p=1, q=2) OOS hit=50.0% vs 51.9%\n",
| |||
" M15 → M5 INVALID p_adj=0.0555 F= 4.84 stab=20% lags(p=1, q=1) OOS hit=50.7% vs 50.7%\n",
| |||
" H1 → M5 INVALID p_adj=0.9725 F= 0.09 stab=6% lags(p=1, q=1) OOS hit=50.5% vs 50.4%\n"
| |||
]
| |||
}
| |||
],
| |||
"source": [
| |||
"# Step 8 — Monitor-style summary (this is exactly what the EA panel will show)\n",
| |||
"print(\"MULTI-TIMEFRAME CAUSALITY\")\n",
| |||
"for _, r in res.iterrows():\n",
| |||
" print(f\"{r.lead_tf:>4} \\u2192 {r.lag_tf:<4} {r.status:<8} p_adj={r.p_adj:.4f} F={r.f_stat:6.2f} \"\n",
| |||
" f\"stab={r.stability:.0%} lags(p={r.p}, q={r.q}) OOS hit={r.oos_hit:.1%} vs {r.oos_hit_base:.1%}\")"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 22,
| |||
"id": "94b5a898",
| |||
"metadata": {},
| |||
"outputs": [
| |||
{
| |||
"data": {
| |||
"image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAG4CAYAAACq4a6LAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Qd8FHX6x/FvegGS0HsHQZBmw3qiotjLqWc9Fcudnp79PLn/2U5P9Iq9V+z1LHeeYkcOGyCiIIh0Qi9CQoD0/b+e3zLrbrIJSUh2s7uf9+s1LJmZnZ2dmd39zTPPPL8kn8/nEwAAAAAAAAAAqCa5+igAAAAAAAAAAEAQHQAAAAAAAACAWpCJDgAAAAAAAABADQiiAwAAAAAAAABQA4LoAAAAAAAAAADUgCA6AAAAAAAAAAA1IIgOAAAAAAAAAEANCKIDAAAAAAAAAFADgugAAAAAAAAAANSAIDqAuDVhwgQlJSVpyZIlTfo6o0aNckOwNWvW6OSTT1bbtm3dOtx9992aNGmS+789JrL8/HxlZmbqs88+a9C2TVR2HNvxY8e156abbnLjYpGtt61/YznttNP0q1/9qtGWBwCIrzZg1TZFuN9V1N25556rXr16NelvO34+lqdPn75Tm+N3v/udDjvssJ3epE3R9ozl9mw88vbH+vXr1RzMmTNHqampmj17drRXBSCIDjRnixcv1qWXXqpddtlF2dnZbhg0aJAuueQSfffdd9FePdTiyiuv1Hvvvadx48bp2Wef1RFHHMH22u4vf/mLRo4cqf33379ZbJNVq1bpuuuu08EHH6xWrVrt8ELH559/rgMOOMB9Hjt16qTLLrtMRUVFikcvvPCCuwAUC/74xz/qX//6l7799ttorwoAoAFBQm+wYEnXrl1dkHbFihVsyxhmbSYLyG3atCmir7t161b3urGUuPLggw822QUdO6d8/PHH9ac//alJlo/qpk6d6i5c7LHHHkpLS6v1IkHw91/wcPvtt1eb174TLWkkLy9POTk5Ov7447Vo0aKwy33iiSe06667uuSl/v3767777ovJXWXxj6OPPlo33HBDtFcFUCrbAGie3n77bZ166qnuROLMM8/UsGHDlJycrB9++EGvv/66HnroIdcg6tmzZ7RXtdn69a9/7bJTMzIymvR13n///WrjPv74Y9eoueaaawLj7GLItm3blJ6erkS1bt06Pf30025oLubNm6c77rjDNS6HDBmiL774osZ5Z86cqUMPPdQ1SO+8804tX75c//jHPzR//ny9++67irQ///nP7gJAUwbRLevjiiuuUHM3YsQI7bnnnvrnP/+pZ555JtqrAwBowEX23r17q7i4WF9++aULKE6ZMsX9DlkQqLFZG9raZRbgQuOw7WnnLsFB9JtvvtldELGgXySD6Pa6JlbuaLQgert27dy2amz33HOP+2xZwkhzb3vGi3feecdduBg6dKj69OmjH3/8sdb57S6Bs88+u1rbNpgl7dg+LCgocBdE7Lvrrrvu0kEHHeTOUewOaM8jjzyiiy66SCeddJKuuuoq/e9//3OJP/bZsMSTWGPv5aijjtLChQvVt2/faK8OEhhBdKAZsh8HC/5a4/6jjz5S586dQ6ZbwM8aWhZUr82WLVvUokULxaqdXf+UlBQ3NLVwQfG1a9dWO1mw/dUUJ4Gx5LnnnnMnV8cee6yaC8sQ2bBhg9q0aaPXXntNp5xySo3zWoO1devWLrPJsj+M3cp84YUXuosphx9+eATXXG5bBp+sJjrLzLnxxhvd92PLli2jvToAgHo48sgj3cVQc8EFF7iAorV5//3vfzdJuS7L9Ez0dllji9XtGevnTLUpKyvT888/74KQjYG2Z91cfPHFLlidlZXl7izfURDdkq3OOuusWuex9q0l7liW+1577RX43txtt91cEsltt90WuJj1f//3fy57285tjJ2rVFZW6pZbbtFvfvMbdz4TTT6fz10wte1TF6NHj3brbIlYdsEViBZqogPN0N/+9jfXmHvqqaeqBdC9xotdSe7evXtgnGUtWNDIAvB2ldbKUlgGu7ErzxYY7NGjh8vKtudZuRH7gQ3mLcNuEzvhhBPc/9u3b++yqSsqKkLmtaCjZXpbINGCxeecc44roxCutqRlz1t9cAtSWuPaTpDshCjcrbyffvqpu/WtQ4cO6tatW63byW5JGzx4sCurYT+qtlzLnK2tHqY1Huz2zi5durjn2dV8q7NmgdDgzA/vuVa3267e23awxvWJJ57osqmDBdfY9J5nDYMHHnggcDueqakm+ldffeX2mb0Hew3LWLCMEY+V7rF1sywG235WQuS8885z+yBc/boFCxYEMn5yc3M1duxYl3UQLqC99957B7bfL37xi2pZ9ZZdfeCBB7r1smPKGmPff/+9GurNN990pVx2JsBpFyjOP/98dezY0W0Pu0sjXGZ7XY9Re192bO5IYWGhPvjgA9fA9QLoxrJG7P288sortT6/tLTU3YZoQXvbL7ZNbdt+8skn1ea1255tH9p83rqHuxW6ag3J2uq7Vq1TunnzZpdhbse+fS/YZ86yYGbMmOGm2zH93//+V0uXLg0cx8G1T0tKSlzAul+/foHvlWuvvdaND2Z/2/eNfYZsWx933HEug78uvM/Myy+/7C5g2LFv282WYbX1q7L1t+9O208AgNhmv5HG2rZV7/bz2ib2G2l3/s2dO7feyw/3m9lUbeFw7Hfdfh+932Fr91qbwqtDXNd2Q03ty3Dvb/Xq1a5daK9lr2nnGbb9gtvKb731lmvvWVvZ5rGsTwu8VX3/4QS3NezxD3/4g/u/ZUF7bQl7LcuctfZbOAMGDNCYMWNqfR2rD27z2IUWC8LZ8q1t7L1v22fGstG91/XWq7ZzJjtPsDJ2dn5hbUxra/72t7/Vxo0bQ17f9tkxxxzj7pSwtrTNa+30cHfCWTve3q+tp233W2+91Z3jBZ+j2PKsfW3nQd76Vs2gt/bUjs5JwrF1tGPKgpAeO0+xbWfL89h7t+PYEpCC25x2IcvOPb3SheHql9vfFii2dr4FdO24sW04ceLEsOtjAWDbZnZsWcZ0OOXl5e64s3lsebaNrC0Y3M609bfsa3s/nt///vdufe69996QvqpsnN3JHSl27NQ1QOyxc3MLLNfEAuK27bwAuhk4cKC7Szb4PMS+I+z7yc6pg1lJWGsnW/u+Luy4sQuY9h1n2/nyyy+vtn52LB9yyCHuPML2k5VeCbedvc+MlTu1c3bbNt6+t3a7lcq0488+m/YdULX0kGXd22fCvp+AaCJ9DWimpVwsMGXBxvqwxoY1KO1HyEpMWHDUvPrqqy6IalfE7QfQrl5bANoCWTYtmDWQbRn22raMDz/80F3ZtgaMPd9rZFkmsS3HxtmPt/2g2clDVdYgtNrXVtvSbv2zRp/9yNuJidUvtgZgMPuxt8ahnTTYj3xNHnvsMXchwYLz3g+6NVItIH3GGWfU+DyrUW4XKWz97X3ayY491tRgsYaYBZgtWGgNXWtYWyPRgnrhWCDaaqDbSVW42/KqskaDNSjsJMbehwUJ7UTQjgH725vHat3ZSY9Nt2366KOPuke73blqQ9YaO3YyMX78eBcQtVsJrWFjjWCPnVRYI3i//fZzV/Mtm962nZ2YetnU9j5sn9r2sefaMWSNIju+vvnmm2qdSdUlE2batGmB46ghrHFpDSi7UGD7wd6nHcN2QmQNfm+b1ecYratZs2a5z5iXJeexbTd8+HC3TXYUhLd9cfrpp7tsEAtiW61C2762nrYMYycCdkJrJxmWNWSlY954442dWvdwbNnWGLftaA1ea2zba9rxt/vuu7sMFrtd1L4n7FZR4138sO1rgWyb37JZbB1t+9h8lmljJ1Eeyya0Czb2ubTjzY4xOzmvj7/+9a/uOLeMHruIYp9DOxm0W1eDT1DsfdjfdvGr6ncLACC2eAHG4IxJa5da5qUFLK0dY+0Ca9NaW9PaPPVtm4TT2G3hcCwgaQFx+8214K/97lrAypJM7HfXApx1bTfUh5V2sPajtW9tW9lvqrUzly1bFth2FnS333sLUNq
| |||
"text/plain": [
| |||
"<Figure size 1500x450 with 2 Axes>"
| |||
]
| |||
},
| |||
"metadata": {},
| |||
"output_type": "display_data"
| |||
}
| |||
],
| |||
"source": [
| |||
"# Step 9 — Visuals for the article\n",
| |||
"fig, axes = plt.subplots(1, 2, figsize=(15, 4.5))\n",
| |||
"labels = [f\"{a}\\u2192{b}\" for a, b in zip(res.lead_tf, res.lag_tf)]\n",
| |||
"colors = [\"tab:green\" if s == \"VALID\" else \"tab:red\" for s in res.status]\n",
| |||
"axes[0].bar(labels, -np.log10(res[\"p_adj\"].clip(lower=1e-300)), color=colors)\n",
| |||
"axes[0].axhline(-np.log10(ALPHA), ls=\"--\", c=\"k\", lw=1, label=f\"alpha = {ALPHA}\")\n",
| |||
"axes[0].set_title(\"Granger significance (-log10 adjusted p)\")\n",
| |||
"axes[0].legend()\n",
| |||
"\n",
| |||
"for (lead, lag), s in rolling.items():\n",
| |||
" axes[1].plot(s.index, -np.log10(s.clip(lower=1e-300)), lw=1.2, label=f\"{lead}\\u2192{lag}\")\n",
| |||
"axes[1].axhline(-np.log10(ALPHA), ls=\"--\", c=\"k\", lw=1)\n",
| |||
"axes[1].set_title(f\"Rolling causality strength (window = {WINDOW} bars)\")\n",
| |||
"axes[1].legend()\n",
| |||
"plt.tight_layout()\n",
| |||
"plt.show()"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": 23,
| |||
"id": "1bd4ace7",
| |||
"metadata": {},
| |||
"outputs": [
| |||
{
| |||
"name": "stdout",
| |||
"output_type": "stream",
| |||
"text": [
| |||
"Exported -> C:\\Users\\wyt_coal\\AppData\\Roaming\\MetaQuotes\\Terminal\\Common\\Files\\granger_mtf_causality.csv\n",
| |||
"lead_tf,lag_tf,p,q,window,alpha,f_stat,p_value,p_adj,stability,oos_hit,oos_edge_bps,status\n",
| |||
"H1,M15,1,1,1500,0.0500,0.001187,0.97251713,0.97251713,0.0857,0.5252,0.142677,INVALID\n",
| |||
"M30,M15,1,2,1500,0.0500,9.642431,0.00190880,0.00763521,0.4286,0.4997,-0.035219,VALID\n",
| |||
"M15,M5,1,1,1500,0.0500,4.844780,0.02774058,0.05548117,0.1966,0.5070,0.044158,INVALID\n",
| |||
"H1,M5,1,1,1500,0.0500,0.092896,0.76053027,0.97251713,0.0603,0.5049,0.005228,INVALID\n",
| |||
"\n"
| |||
]
| |||
}
| |||
],
| |||
"source": [
| |||
"# Step 10 — Export to MT5 Common\\\\Files (the EA reads it with FILE_COMMON)\n",
| |||
"def common_files_dir():\n",
| |||
" if DATA_SOURCE == \"mt5\":\n",
| |||
" info = mt5.terminal_info()\n",
| |||
" if info is not None:\n",
| |||
" return os.path.join(info.commondata_path, \"Files\")\n",
| |||
" return os.path.abspath(\"./outputs\")\n",
| |||
"\n",
| |||
"export_cols = [\"lead_tf\", \"lag_tf\", \"p\", \"q\", \"window\", \"alpha\", \"f_stat\", \"p_value\", \"p_adj\",\n",
| |||
" \"stability\", \"oos_hit\", \"oos_edge_bps\", \"status\"]\n",
| |||
"\n",
| |||
"def write_table(path):\n",
| |||
" os.makedirs(os.path.dirname(path), exist_ok=True)\n",
| |||
" with open(path, \"w\", newline=\"\", encoding=\"ascii\") as f:\n",
| |||
" f.write(\",\".join(export_cols) + \"\\r\\n\")\n",
| |||
" for _, r in res[export_cols].iterrows():\n",
| |||
" f.write(\",\".join([r.lead_tf, r.lag_tf, str(int(r.p)), str(int(r.q)), str(int(r.window)),\n",
| |||
" f\"{r.alpha:.4f}\", f\"{r.f_stat:.6f}\", f\"{r.p_value:.8f}\", f\"{r.p_adj:.8f}\",\n",
| |||
" f\"{r.stability:.4f}\", f\"{r.oos_hit:.4f}\", f\"{r.oos_edge_bps:.6f}\",\n",
| |||
" r.status]) + \"\\r\\n\")\n",
| |||
"\n",
| |||
"target = os.path.join(common_files_dir(), OUTPUT_FILE)\n",
| |||
"write_table(target)\n",
| |||
"write_table(os.path.abspath(os.path.join(\"./outputs\", OUTPUT_FILE)))\n",
| |||
"print(\"Exported ->\", target)\n",
| |||
"print(open(target).read())\n",
| |||
"\n",
| |||
"if DATA_SOURCE == \"mt5\":\n",
| |||
" mt5.shutdown()"
| |||
]
| |||
},
| |||
{
| |||
"cell_type": "code",
| |||
"execution_count": null,
| |||
"id": "d5e26503-5f32-48a0-9712-f871f6f01414",
| |||
"metadata": {},
| |||
"outputs": [],
| |||
"source": []
| |||
}
| |||
],
| |||
"metadata": {
| |||
"kernelspec": {
| |||
"display_name": "Python 3 (ipykernel)",
| |||
"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.14.3"
| |||
}
| |||
},
| |||
"nbformat": 4,
| |||
"nbformat_minor": 5
| |||
}
|