c00ed4471b
Removed (all recoverable from git history):
- llm_provider_switch_plan.txt: 2026-08-08 DeepSeek/Anthropic multi-provider plan that
was never implemented (llm_client.py is DeepSeek-only, no `anthropic` anywhere)
- docs/superpowers/specs/2026-07-21-centralized-config-design.md: spec of the
config.json migration that merged the same day; its plan sibling was already
deleted in b936d44. app_config's docstring pointer goes with it.
- scripts/compare_oanda_tiingo.py: Phase 0 recent-tail smoke test, superseded by the
Phase 1/2 study scripts (result recorded in the test plan)
- backtest/research/fetch_oanda_m15.py + oanda_td_divergence.py + its by-hour CSV:
the 2026-07-24 Oanda-vs-Twelve-Data study, superseded by the 2026-08-14 feed
divergence report (which keeps a one-line citation)
- backtest/research/burst_profile.py and hour_analysis.py: superseded by burst_map.py
and book_report.py
- backtest/research/panel_payload_drytest_2026-08-15.py: one-off live DeepSeek probe;
its .jsonl evidence stays
Added to git: the three 2026-08-14 research reports (feed divergence, re-pin
verification, signal parity), their CSV, and docs/data_source_change_test_plan.md -
CLAUDE.md cites them as standing evidence and their driver scripts were already
committed. The test plan no longer refers to auto-execution.
Hygiene:
- .gitignore: drop oanda_state.json (daemon removed 2026-09-03) and the vanished
backtest/trailgrid dirs; ignore .coverage
- docs/architecture_diagram.py only writes the PNG when run as a script (it used
to plot on import); test box refreshed to 461 tests / 31 modules; PNG regenerated
(the committed one still drew the removed daemon)
- backtest/instructions_for_backtest.txt: manual-journal head replaced by the
actual reporting checklist; the 2026-09-06 addendum is unchanged
- doc drift: README/publisher list all six published files; news_summary.php says
4 h (not 24 h) and drops the pre-id sha1 fallback whose window closed;
analyze_signal_log lists six lenses and --scan-outcomes; agent_panel cost model
says 4 requests by default; tiingo comment cites the NY-anchored weekend;
Module 21 title no longer says "execution lifecycle"
Suite: 461 passed, 0 failed.
Claude-Session: https://claude.ai/code/session_0156YhogLytzj9P2iH1skGbL
445 lines
19 KiB
Python
445 lines
19 KiB
Python
"""Where and when gold moves violently — burst map of the raw bars.
|
||
|
||
Usage:
|
||
python backtest/research/burst_map.py --parquet PATH --bar-minutes N \
|
||
[--k 3,4,5] [--book BOOK.csv] [--within-bars 12] \
|
||
[--start YYYY-MM-DD --end YYYY-MM-DD] [--out DIR]
|
||
|
||
A "burst" is one bar whose range is >= K x the PRIOR bar's Wilder ATR(14) —
|
||
the same definition every burst study in this folder has used (spelled
|
||
out here bar-for-bar: TR = max(H-L, |H-Cprev|, |L-Cprev|), ATR =
|
||
TR.ewm(alpha=1/14, min_periods=14, adjust=False).mean(), ratio =
|
||
(H-L) / ATR.shift(1)).
|
||
|
||
Reports:
|
||
* burst frequency by (weekday, UTC bar-CLOSE hour), plus mean |1 h| and
|
||
|4 h| absolute moves in $ and in ATR units on the same grid;
|
||
* post-burst drift in the burst's own direction over 6/12/24/48 bars,
|
||
overall and by session bucket — does a violent bar continue or snap back;
|
||
* the largest trailing-60-minute close-to-close moves, de-overlapped;
|
||
* if a trade book is given: how trades that were decided just after a burst,
|
||
or that were OPEN when one printed, performed against the rest.
|
||
|
||
Every ATR-unit number here is normalised by the PRIOR bar's ATR — the same
|
||
denominator as the burst test, so a "4x burst that ran another 4 ATR" reads
|
||
directly. All hours are UTC and are the bar's CLOSE hour (label + one bar),
|
||
matching the clock the live gates evaluate.
|
||
|
||
Analysis tooling only — never imported by the live code.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import math
|
||
import os
|
||
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
try: # package import (backtest.research.burst_map)
|
||
from backtest.research.book_report import base_metrics, load_book, session_of
|
||
except ImportError: # run directly as a script from anywhere
|
||
from book_report import base_metrics, load_book, session_of
|
||
|
||
SLOT_UNKNOWN = ""
|
||
|
||
|
||
# ─────────────────────────── bar primitives ─────────────────────────
|
||
|
||
def load_bars(parquet: str, start: str | None = None,
|
||
end: str | None = None) -> pd.DataFrame:
|
||
"""Read a cached OHLCV parquet, de-duplicated and oldest-first."""
|
||
df = pd.read_parquet(parquet).sort_index()
|
||
df = df[~df.index.duplicated(keep="last")]
|
||
if start:
|
||
df = df[df.index >= pd.Timestamp(start, tz="UTC")]
|
||
if end:
|
||
df = df[df.index <= pd.Timestamp(end, tz="UTC")]
|
||
return df
|
||
|
||
|
||
def wilder_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
|
||
"""Wilder ATR (the definition every burst study in this folder uses)."""
|
||
h, l, c = df["High"], df["Low"], df["Close"]
|
||
tr = pd.concat([h - l, (h - c.shift()).abs(), (l - c.shift()).abs()],
|
||
axis=1).max(axis=1)
|
||
return tr.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
|
||
|
||
|
||
def burst_ratio(df: pd.DataFrame, period: int = 14) -> pd.Series:
|
||
"""Bar range expressed in PRIOR-bar ATRs."""
|
||
return (df["High"] - df["Low"]) / wilder_atr(df, period).shift(1)
|
||
|
||
|
||
def burst_flags(df: pd.DataFrame, k: float, period: int = 14) -> pd.Series:
|
||
"""True where the bar's range is >= k x the prior bar's Wilder ATR(14)."""
|
||
return (burst_ratio(df, period) >= k).fillna(False)
|
||
|
||
|
||
def burst_dir(df: pd.DataFrame) -> pd.Series:
|
||
"""+1 up bar, −1 down bar, 0 doji (sign of Close − Open)."""
|
||
return np.sign(df["Close"] - df["Open"]).astype(float)
|
||
|
||
|
||
def close_times(df: pd.DataFrame, bar_minutes: int) -> pd.DatetimeIndex:
|
||
"""Bar CLOSE timestamps (the parquet index labels bar OPEN time)."""
|
||
return df.index + pd.Timedelta(minutes=bar_minutes)
|
||
|
||
|
||
# ───────────────────── when does gold move? tables ──────────────────
|
||
|
||
def hour_weekday_table(df: pd.DataFrame, k: float, bar_minutes: int = 5,
|
||
ks: tuple[float, ...] | None = None) -> dict:
|
||
"""Burst frequency and absolute-move size on a (weekday, close-hour) grid.
|
||
|
||
Returns a dict of DataFrames (several tables are wanted, one frame each):
|
||
burst_pct : weekday rows x close-hour cols, % of bars that are >= k x
|
||
bursts (one frame; per-k frames live in `burst_pct_by_k`)
|
||
absmove : (weekday, close-hour) rows; bars, mean |1 h move| and
|
||
|4 h move| in $ and in prior-bar ATR units
|
||
by_hour : the same stats collapsed to close-hour
|
||
by_weekday : the same stats collapsed to weekday
|
||
"""
|
||
ks = tuple(ks) if ks else (k,)
|
||
ct = close_times(df, bar_minutes)
|
||
hour = pd.Series(ct.hour, index=df.index)
|
||
wd = pd.Series(ct.weekday, index=df.index)
|
||
atr_prev = wilder_atr(df).shift(1)
|
||
|
||
per_hour = max(1, int(round(60 / bar_minutes)))
|
||
c = df["Close"]
|
||
mv1 = (c - c.shift(per_hour)).abs()
|
||
mv4 = (c - c.shift(4 * per_hour)).abs()
|
||
stats = pd.DataFrame({
|
||
"weekday": wd, "hour": hour,
|
||
"abs1h_usd": mv1, "abs4h_usd": mv4,
|
||
"abs1h_atr": mv1 / atr_prev, "abs4h_atr": mv4 / atr_prev,
|
||
})
|
||
for kk in ks:
|
||
stats[f"burst_{kk:g}x"] = burst_flags(df, kk).astype(float) * 100.0
|
||
|
||
agg = {"abs1h_usd": "mean", "abs4h_usd": "mean",
|
||
"abs1h_atr": "mean", "abs4h_atr": "mean"}
|
||
agg.update({f"burst_{kk:g}x": "mean" for kk in ks})
|
||
|
||
grid = stats.groupby(["weekday", "hour"]).agg(agg)
|
||
grid.insert(0, "bars", stats.groupby(["weekday", "hour"]).size())
|
||
by_hour = stats.groupby("hour").agg(agg)
|
||
by_hour.insert(0, "bars", stats.groupby("hour").size())
|
||
by_wd = stats.groupby("weekday").agg(agg)
|
||
by_wd.insert(0, "bars", stats.groupby("weekday").size())
|
||
|
||
pct_by_k = {kk: grid[f"burst_{kk:g}x"].unstack("hour") for kk in ks}
|
||
return {
|
||
"burst_pct": pct_by_k[k if k in pct_by_k else ks[0]].round(3),
|
||
"burst_pct_by_k": {kk: v.round(3) for kk, v in pct_by_k.items()},
|
||
"absmove": grid.round(3),
|
||
"by_hour": by_hour.round(3),
|
||
"by_weekday": by_wd.round(3),
|
||
}
|
||
|
||
|
||
def post_burst_drift(df: pd.DataFrame, k: float,
|
||
horizons: tuple[int, ...] = (6, 12, 24, 48),
|
||
bar_minutes: int = 5) -> pd.DataFrame:
|
||
"""Signed drift IN the burst's direction over the next h bars, in ATRs.
|
||
|
||
A positive mean means violent bars continue; negative means they snap
|
||
back. Split by the session bucket of the burst bar's close hour.
|
||
"""
|
||
flags = burst_flags(df, k)
|
||
d = burst_dir(df)
|
||
atr_prev = wilder_atr(df).shift(1)
|
||
ct = close_times(df, bar_minutes)
|
||
bucket = pd.Series([session_of(h) for h in ct.hour], index=df.index)
|
||
c = df["Close"]
|
||
|
||
rows = []
|
||
for h in horizons:
|
||
fwd = (c.shift(-h) - c) * d / atr_prev
|
||
sub = fwd[flags].dropna()
|
||
buckets = bucket[flags].reindex(sub.index)
|
||
rows.append({"horizon_bars": h, "bucket": "ALL", "n": int(len(sub)),
|
||
"mean_atr": round(float(sub.mean()), 4) if len(sub) else float("nan"),
|
||
"median_atr": round(float(sub.median()), 4) if len(sub) else float("nan"),
|
||
"pct_positive": round(float((sub > 0).mean() * 100), 2) if len(sub) else float("nan")})
|
||
for b, s in sub.groupby(buckets):
|
||
rows.append({"horizon_bars": h, "bucket": b, "n": int(len(s)),
|
||
"mean_atr": round(float(s.mean()), 4),
|
||
"median_atr": round(float(s.median()), 4),
|
||
"pct_positive": round(float((s > 0).mean() * 100), 2)})
|
||
return pd.DataFrame(rows)
|
||
|
||
|
||
def slot_label(weekday: int, hour: int, day: int) -> str:
|
||
"""Time-slot label for a big move.
|
||
|
||
NOTE: there is no economic-calendar file in this repo, so these are
|
||
TIME SLOTS, not confirmed events — "payroll window" means "the clock
|
||
slot US payrolls normally print in", nothing more.
|
||
"""
|
||
if weekday == 4 and day <= 7 and hour in (12, 13):
|
||
return "first-Friday 12:30 slot (payroll-report window)"
|
||
if weekday == 2 and hour in (18, 19):
|
||
return "Wednesday 18:00 slot (central-bank window)"
|
||
if hour in (12, 13):
|
||
return "12:30 slot (US data window)"
|
||
if hour == 14:
|
||
return "14:00 slot"
|
||
if hour in (21, 22, 23):
|
||
return "settlement/after-hours"
|
||
if hour in (0, 1, 2):
|
||
return "Asia open"
|
||
return SLOT_UNKNOWN
|
||
|
||
|
||
def largest_moves(df: pd.DataFrame, n: int = 40, window_minutes: int = 60,
|
||
bar_minutes: int = 5) -> pd.DataFrame:
|
||
"""The n biggest trailing-`window_minutes` close-to-close moves.
|
||
|
||
Overlapping windows are de-duplicated greedily: the largest move wins and
|
||
anything whose close time is within +/- `window_minutes` of an already
|
||
kept move is dropped, so one violent hour contributes one row.
|
||
"""
|
||
bars = max(1, int(round(window_minutes / bar_minutes)))
|
||
c = df["Close"]
|
||
move = c - c.shift(bars)
|
||
atr_prev = wilder_atr(df).shift(1)
|
||
ct = close_times(df, bar_minutes)
|
||
tbl = pd.DataFrame({
|
||
"close_time": ct, "move_usd": move.to_numpy(),
|
||
"move_atr": (move / atr_prev).to_numpy(),
|
||
}).dropna()
|
||
tbl["abs_usd"] = tbl["move_usd"].abs()
|
||
tbl = tbl.sort_values("abs_usd", ascending=False)
|
||
|
||
keep, taken = [], []
|
||
gap = pd.Timedelta(minutes=window_minutes)
|
||
for _, row in tbl.iterrows():
|
||
t = row["close_time"]
|
||
if any(abs(t - u) <= gap for u in taken):
|
||
continue
|
||
taken.append(t)
|
||
keep.append(row)
|
||
if len(keep) >= n:
|
||
break
|
||
out = pd.DataFrame(keep).reset_index(drop=True)
|
||
if out.empty:
|
||
return out
|
||
out["weekday"] = out["close_time"].dt.weekday
|
||
out["hour"] = out["close_time"].dt.hour
|
||
out["direction"] = np.where(out["move_usd"] > 0, "UP", "DOWN")
|
||
out["slot"] = [slot_label(w, h, t.day) for w, h, t
|
||
in zip(out["weekday"], out["hour"], out["close_time"])]
|
||
out["move_usd"] = out["move_usd"].round(2)
|
||
out["move_atr"] = out["move_atr"].round(2)
|
||
return out[["close_time", "weekday", "hour", "direction", "move_usd",
|
||
"move_atr", "slot"]]
|
||
|
||
|
||
# ───────────────────────── book <-> burst join ──────────────────────
|
||
|
||
def _stats(r: pd.Series) -> dict:
|
||
m = base_metrics(r)
|
||
return {"n": m["n"], "total_r": round(m["total_r"], 2),
|
||
"mean_r": round(m["mean_r"], 3), "wr": round(m["wr"], 2),
|
||
"pf": round(m["pf"], 2) if math.isfinite(m["pf"]) else float("inf")}
|
||
|
||
|
||
def join_book_to_bursts(book_df: pd.DataFrame, bars: pd.DataFrame, k: float,
|
||
within_bars: int = 12,
|
||
bar_minutes: int = 5) -> dict:
|
||
"""Split a trade book by its relationship to burst bars.
|
||
|
||
(a) `after_burst` : decision_time falls within `within_bars` bars after
|
||
the most recent burst's close (bar distance is counted POSITIONALLY on
|
||
the bar index, so weekend gaps cannot inflate it).
|
||
(b) `burst_during` : at least one burst closed inside [entry, exit).
|
||
(c) `burst_agrees` / `burst_opposes`: of (b), whether the FIRST burst that
|
||
printed during the trade pushed with or against the trade direction.
|
||
"""
|
||
ct = close_times(bars, bar_minutes)
|
||
ct_arr = ct.to_numpy()
|
||
flags = burst_flags(bars, k).to_numpy()
|
||
bdir = burst_dir(bars).to_numpy()
|
||
burst_pos = np.flatnonzero(flags)
|
||
|
||
# Last burst position at or before each bar (−1 where none yet).
|
||
last_burst = np.where(flags, np.arange(len(flags)), -1)
|
||
last_burst = np.maximum.accumulate(last_burst)
|
||
|
||
dec = book_df["decision_time"].astype("datetime64[ns, UTC]").to_numpy()
|
||
ent = book_df["entry_time"].astype("datetime64[ns, UTC]").to_numpy()
|
||
exi = book_df["exit_time"].astype("datetime64[ns, UTC]").to_numpy()
|
||
|
||
# Position of the last bar that had CLOSED by decision_time.
|
||
idx = np.searchsorted(ct_arr, dec, side="right") - 1
|
||
valid = idx >= 0
|
||
lb = np.where(valid, last_burst[np.clip(idx, 0, len(flags) - 1)], -1)
|
||
bars_since = np.where((lb >= 0) & valid, idx - lb, np.iinfo(np.int32).max)
|
||
after = pd.Series((bars_since >= 0) & (bars_since <= within_bars),
|
||
index=book_df.index)
|
||
|
||
# Bursts printing inside the open trade.
|
||
bct = ct_arr[burst_pos] if len(burst_pos) else np.array([], dtype=ct_arr.dtype)
|
||
lo = np.searchsorted(bct, ent, side="left")
|
||
hi = np.searchsorted(bct, exi, side="left")
|
||
during = pd.Series(hi > lo, index=book_df.index)
|
||
trade_sign = np.where(
|
||
book_df["direction"].astype(str).str.upper().str.startswith("B"),
|
||
1.0, -1.0)
|
||
has = hi > lo
|
||
first_dir = np.zeros(len(book_df), dtype=float)
|
||
if len(burst_pos) and has.any():
|
||
# hi > lo guarantees lo[has] indexes a real burst.
|
||
first_dir[has] = bdir[burst_pos[lo[has]]]
|
||
agrees = pd.Series(has & (first_dir * trade_sign > 0),
|
||
index=book_df.index)
|
||
opposes = pd.Series(has & (first_dir * trade_sign < 0),
|
||
index=book_df.index)
|
||
|
||
r = book_df["r_multiple"]
|
||
out = {
|
||
"k": k, "within_bars": within_bars,
|
||
"bursts_in_window": int(len(burst_pos)),
|
||
"after_burst": _stats(r[after]),
|
||
"not_after_burst": _stats(r[~after]),
|
||
"burst_during": _stats(r[during]),
|
||
"no_burst_during": _stats(r[~during]),
|
||
"burst_agrees": _stats(r[agrees]),
|
||
"burst_opposes": _stats(r[opposes]),
|
||
"median_bars_since_burst": float(np.median(
|
||
bars_since[bars_since < np.iinfo(np.int32).max])) if (
|
||
bars_since < np.iinfo(np.int32).max).any() else float("nan"),
|
||
}
|
||
return out
|
||
|
||
|
||
# ──────────────────────────── reporting ─────────────────────────────
|
||
|
||
def _fmt_stats(name: str, s: dict) -> str:
|
||
pf = "inf" if math.isinf(s["pf"]) else f"{s['pf']:.2f}"
|
||
return (f" {name:<18} n={s['n']:<5} totalR={s['total_r']:>8.2f} "
|
||
f"meanR={s['mean_r']:>7.3f} WR={s['wr']:>6.2f}% PF={pf}")
|
||
|
||
|
||
def main() -> None:
|
||
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
||
ap.add_argument("--parquet", required=True)
|
||
ap.add_argument("--bar-minutes", type=int, required=True)
|
||
ap.add_argument("--k", default="3,4,5",
|
||
help="burst thresholds in prior-bar ATRs (default 3,4,5)")
|
||
ap.add_argument("--book", default=None)
|
||
ap.add_argument("--book-bar-minutes", type=int, default=None,
|
||
help="bar minutes of the BOOK (default: --bar-minutes)")
|
||
ap.add_argument("--within-bars", type=int, default=12)
|
||
ap.add_argument("--start", default=None)
|
||
ap.add_argument("--end", default=None)
|
||
ap.add_argument("--n-moves", type=int, default=40)
|
||
ap.add_argument("--out", default="backtest/research/burst_map_out")
|
||
args = ap.parse_args()
|
||
|
||
ks = [float(x) for x in args.k.split(",") if x.strip()]
|
||
os.makedirs(args.out, exist_ok=True)
|
||
bars = load_bars(args.parquet, args.start, args.end)
|
||
bm = args.bar_minutes
|
||
primary = ks[0]
|
||
|
||
lines: list[str] = []
|
||
add = lines.append
|
||
add("=" * 78)
|
||
add(f"BURST MAP {os.path.basename(args.parquet)}")
|
||
add("=" * 78)
|
||
add(f"bars : {len(bars)} ({bars.index[0]} -> {bars.index[-1]}"
|
||
f", labels = bar OPEN time)")
|
||
add(f"bar minutes : {bm} hours below are bar CLOSE hours (UTC)")
|
||
add(f"burst definition : range >= K x PRIOR bar Wilder ATR(14); "
|
||
f"K = {', '.join(f'{k:g}' for k in ks)}")
|
||
add("ATR units : always normalised by the PRIOR bar's ATR")
|
||
for k in ks:
|
||
f = burst_flags(bars, k)
|
||
add(f" K={k:g}: {int(f.sum())} burst bars "
|
||
f"({float(f.mean()) * 100:.3f}% of bars)")
|
||
|
||
tabs = hour_weekday_table(bars, primary, bm, ks=tuple(ks))
|
||
add("")
|
||
add(f"── BURST % BY (weekday, close-hour), K={primary:g} ──")
|
||
add(tabs["burst_pct"].fillna(0).round(2).to_string())
|
||
add("")
|
||
add("── MEAN ABSOLUTE MOVE BY CLOSE-HOUR (all weekdays) ──")
|
||
add(tabs["by_hour"].to_string())
|
||
add("")
|
||
add("── MEAN ABSOLUTE MOVE BY WEEKDAY ──")
|
||
add(tabs["by_weekday"].to_string())
|
||
|
||
add("")
|
||
add(f"── POST-BURST DRIFT (in the burst's own direction), K={primary:g} ──")
|
||
drift = post_burst_drift(bars, primary, bar_minutes=bm)
|
||
add(drift.to_string(index=False))
|
||
|
||
add("")
|
||
add(f"── LARGEST {args.n_moves} TRAILING-60-MIN MOVES (de-overlapped) ──")
|
||
add(" slot labels are CLOCK SLOTS — no economic calendar exists in "
|
||
"this repo, so no event is confirmed.")
|
||
moves = largest_moves(bars, n=args.n_moves, bar_minutes=bm)
|
||
add(moves.to_string(index=False))
|
||
|
||
join_rows = []
|
||
if args.book:
|
||
bbm = args.book_bar_minutes or bm
|
||
book = load_book(args.book, bbm)
|
||
add("")
|
||
add(f"── BOOK JOIN {os.path.basename(args.book)} "
|
||
f"({len(book)} trades, book bar = {bbm} min) ──")
|
||
first, last = close_times(bars, bm)[0], close_times(bars, bm)[-1]
|
||
outside = int(((book["decision_time"] < first)
|
||
| (book["exit_time"] > last)).sum())
|
||
if outside:
|
||
add(f" !! {outside} trade(s) start before / end after the bar "
|
||
f"window ({first} .. {last}) — their burst context is "
|
||
f"partial. Widen --start/--end or use a longer parquet.")
|
||
add(" CAUTION: group (b) is survivorship-shaped — a trade only sees "
|
||
"a burst if it survived long enough to still be open, so its high "
|
||
"PF is mostly 'winners last longer', not a burst edge.")
|
||
for k in ks:
|
||
j = join_book_to_bursts(book, bars, k, args.within_bars, bm)
|
||
add(f" K={k:g} ({j['bursts_in_window']} bursts in window; "
|
||
f"median bars since last burst = "
|
||
f"{j['median_bars_since_burst']:.0f})")
|
||
add(f" (a) decided within {args.within_bars} bars of a burst")
|
||
add(_fmt_stats("after_burst", j["after_burst"]))
|
||
add(_fmt_stats("not_after_burst", j["not_after_burst"]))
|
||
add(" (b) a burst printed while the trade was open")
|
||
add(_fmt_stats("burst_during", j["burst_during"]))
|
||
add(_fmt_stats("no_burst_during", j["no_burst_during"]))
|
||
add(" (c) of (b), first burst vs the trade's direction")
|
||
add(_fmt_stats("burst_agrees", j["burst_agrees"]))
|
||
add(_fmt_stats("burst_opposes", j["burst_opposes"]))
|
||
for grp in ("after_burst", "not_after_burst", "burst_during",
|
||
"no_burst_during", "burst_agrees", "burst_opposes"):
|
||
join_rows.append({"k": k, "group": grp,
|
||
"within_bars": args.within_bars,
|
||
**j[grp]})
|
||
|
||
text = "\n".join(lines)
|
||
print(text)
|
||
with open(os.path.join(args.out, "burst_map.txt"), "w") as fh:
|
||
fh.write(text + "\n")
|
||
pd.concat([v.assign(k=kk) for kk, v in tabs["burst_pct_by_k"].items()]
|
||
).to_csv(os.path.join(args.out, "hour_weekday_burst_pct.csv"))
|
||
tabs["absmove"].to_csv(os.path.join(args.out, "hour_weekday_absmove.csv"))
|
||
tabs["by_hour"].to_csv(os.path.join(args.out, "by_hour.csv"))
|
||
tabs["by_weekday"].to_csv(os.path.join(args.out, "by_weekday.csv"))
|
||
pd.concat([post_burst_drift(bars, k, bar_minutes=bm).assign(k=k)
|
||
for k in ks]).to_csv(
|
||
os.path.join(args.out, "post_burst_drift.csv"), index=False)
|
||
moves.to_csv(os.path.join(args.out, "largest_moves.csv"), index=False)
|
||
if join_rows:
|
||
pd.DataFrame(join_rows).to_csv(
|
||
os.path.join(args.out, "book_join.csv"), index=False)
|
||
print(f"\n[written] {os.path.join(args.out, 'burst_map.txt')}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|