Article-24649-GrangerMTF-Ca.../GrangerMTF_Causality.ipynb

1018 lines
153 KiB
Text

2026-09-22 21:31:29 +02:00
{
"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
}