a979dcba4b
Add panel_verdict, panel_confidence, panel_rationale, panel_takes_json columns to the signals table (additive, idempotent ALTER TABLE migration for existing DBs). _report_row now reads the panel fields attach_panel writes onto the SignalReport, so LLM panel verdicts stop evaporating on process exit. Also documents the Oanda re-pin (round_trip_cost stays $0.30 for cross-book comparability and the live TP gate; $0.65 is the measured headline) and adds the feed-divergence / parity / shadow-scan study tooling referenced by CLAUDE.md.
117 lines
4.1 KiB
Python
117 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Scratchpad — Oanda vs Tiingo candle parity check
|
|
=================================================
|
|
Fetches the last N closed XAU/USD candles from Oanda and Tiingo for
|
|
M5/M15/H1 via the normal provider stack (make_data_provider →
|
|
fetch_ohlcv: forming-candle drop + validate/clean included, i.e. exactly
|
|
what the signal pipeline consumes) and reports how well they line up:
|
|
|
|
* last-bar timestamp per provider (feed-lag check)
|
|
* timestamp mismatches (bars present in one feed only)
|
|
* per-bar OHLC diff table for the common tail
|
|
* max/mean |diff| per column, with the Close diff also in % of price
|
|
|
|
Both feeds are mid-price (Oanda mid of MBA candles; Tiingo resampled
|
|
mid), so small diffs are expected — this measures how small.
|
|
|
|
Usage:
|
|
venv/bin/python scripts/compare_oanda_tiingo.py [--n 10]
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from config import PlatformConfig, Timeframe, XAUUSD
|
|
from providers import make_data_provider
|
|
|
|
TIMEFRAMES = [Timeframe.M5, Timeframe.M15, Timeframe.H1]
|
|
OHLC = ["Open", "High", "Low", "Close"]
|
|
|
|
|
|
def fetch_tail(provider, tf, n):
|
|
"""Last n closed bars from one provider, or None on failure."""
|
|
try:
|
|
df = provider.fetch_ohlcv(symbol=XAUUSD, timeframe=tf)
|
|
except Exception as e: # scratchpad: report, don't crash
|
|
print(f" !! {provider.name} {tf.value} fetch failed: {e}")
|
|
return None
|
|
return df.tail(n)
|
|
|
|
|
|
def compare_tf(tf, oanda_df, tiingo_df, n):
|
|
print(f"\n{'=' * 78}\n{tf.value} — last {n} closed bars\n{'=' * 78}")
|
|
if oanda_df is None or tiingo_df is None:
|
|
print(" skipped (a provider failed)")
|
|
return
|
|
|
|
print(f" Oanda last bar: {oanda_df.index[-1]} "
|
|
f"Tiingo last bar: {tiingo_df.index[-1]}")
|
|
|
|
common = oanda_df.index.intersection(tiingo_df.index)
|
|
only_o = oanda_df.index.difference(tiingo_df.index)
|
|
only_t = tiingo_df.index.difference(oanda_df.index)
|
|
if len(only_o):
|
|
print(f" bars only in Oanda : {[str(t) for t in only_o]}")
|
|
if len(only_t):
|
|
print(f" bars only in Tiingo: {[str(t) for t in only_t]}")
|
|
if not len(common):
|
|
print(" NO common timestamps — feeds are on different bar grids!")
|
|
return
|
|
|
|
o = oanda_df.loc[common, OHLC]
|
|
t = tiingo_df.loc[common, OHLC]
|
|
diff = o - t
|
|
|
|
print(f"\n {len(common)}/{n} bars share timestamps "
|
|
f"(window {common[0]} → {common[-1]})\n")
|
|
|
|
# Per-bar Close table — the number that matters most.
|
|
hdr = f" {'bar close (UTC)':<20}{'Oanda C':>12}{'Tiingo C':>12}{'ΔC ($)':>10}"
|
|
print(hdr + f"{'ΔO':>8}{'ΔH':>8}{'ΔL':>8}")
|
|
for ts in common:
|
|
row = (f" {str(ts):<20}{o.loc[ts, 'Close']:>12.2f}"
|
|
f"{t.loc[ts, 'Close']:>12.2f}{diff.loc[ts, 'Close']:>10.3f}")
|
|
row += (f"{diff.loc[ts, 'Open']:>8.3f}{diff.loc[ts, 'High']:>8.3f}"
|
|
f"{diff.loc[ts, 'Low']:>8.3f}")
|
|
print(row)
|
|
|
|
print("\n summary over common bars:")
|
|
for col in OHLC:
|
|
d = diff[col].abs()
|
|
print(f" {col:<6} max |Δ| = {d.max():8.4f} mean |Δ| = {d.mean():8.4f}")
|
|
px = o["Close"].iloc[-1]
|
|
dc = diff["Close"].abs()
|
|
print(f" Close Δ as % of price: max {dc.max() / px * 100:.4f}% "
|
|
f"mean {dc.mean() / px * 100:.4f}%")
|
|
exact = (diff[OHLC].abs() < 1e-9).all(axis=1).sum()
|
|
print(f" bars with identical OHLC: {exact}/{len(common)}")
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--n", type=int, default=10,
|
|
help="bars per timeframe to compare (default 10)")
|
|
args = ap.parse_args()
|
|
|
|
cfg = PlatformConfig()
|
|
oanda = make_data_provider(cfg, "oanda")
|
|
tiingo = make_data_provider(cfg, "tiingo")
|
|
|
|
for tf in TIMEFRAMES:
|
|
o_df = fetch_tail(oanda, tf, args.n)
|
|
t_df = fetch_tail(tiingo, tf, args.n)
|
|
compare_tf(tf, o_df, t_df, args.n)
|
|
|
|
print("\nDone. Note: Oanda=mid of MBA candles, Tiingo=resampled mid — "
|
|
"sub-$1 diffs on gold are normal feed divergence, not a bug.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|