2026-05-21 17:50:45 +02:00
|
|
|
"""Compute indicators on multiple timeframes from a single 1-minute feed.
|
|
|
|
|
|
2026-06-17 01:49:11 +02:00
|
|
|
Loads the 1-minute history with Wickra's native ``CandleReader`` and rolls it up
|
|
|
|
|
to coarser timeframes with the native ``Resampler`` — no manual CSV parsing and
|
|
|
|
|
no hand-written bucketing. NumPy is used only to run the batch indicators over
|
|
|
|
|
each resampled series.
|
2026-05-21 17:50:45 +02:00
|
|
|
|
|
|
|
|
Run with::
|
|
|
|
|
|
|
|
|
|
python -m examples.python.multi_timeframe path/to/1m.csv
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
2026-06-17 01:49:11 +02:00
|
|
|
from typing import List, Tuple
|
2026-05-21 17:50:45 +02:00
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
|
import wickra as ta
|
|
|
|
|
|
2026-06-17 01:49:11 +02:00
|
|
|
# A candle as the data layer yields it: (open, high, low, close, volume, timestamp).
|
|
|
|
|
Candle = Tuple[float, float, float, float, float, int]
|
2026-05-21 17:50:45 +02:00
|
|
|
|
2026-06-17 01:49:11 +02:00
|
|
|
|
|
|
|
|
def load(path: str) -> List[Candle]:
|
|
|
|
|
"""Read an OHLCV CSV into candles with ``CandleReader`` (validates the
|
|
|
|
|
``timestamp,open,high,low,close,volume`` header; raises ``ValueError`` on a
|
|
|
|
|
malformed header or row)."""
|
|
|
|
|
with open(path, encoding="utf-8") as f:
|
|
|
|
|
return ta.CandleReader(f.read()).read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resample(candles: List[Candle], bucket_ms: int) -> List[Candle]:
|
|
|
|
|
"""Aggregate 1-minute candles into ``bucket_ms``-sized candles with the
|
|
|
|
|
native streaming ``Resampler``."""
|
|
|
|
|
r = ta.Resampler(bucket_ms)
|
|
|
|
|
out: List[Candle] = []
|
|
|
|
|
for o, h, l, c, v, ts in candles:
|
|
|
|
|
agg = r.update(o, h, l, c, v, ts)
|
|
|
|
|
if agg is not None:
|
|
|
|
|
out.append(agg)
|
|
|
|
|
tail = r.flush()
|
|
|
|
|
if tail is not None:
|
|
|
|
|
out.append(tail)
|
|
|
|
|
return out
|
2026-05-21 17:50:45 +02:00
|
|
|
|
|
|
|
|
|
2026-06-17 01:49:11 +02:00
|
|
|
def summarize(label: str, candles: List[Candle]) -> None:
|
|
|
|
|
if not candles:
|
2026-05-22 12:29:40 +02:00
|
|
|
print(f" {label:<10} (empty series — nothing to summarize)")
|
|
|
|
|
return
|
2026-06-17 01:49:11 +02:00
|
|
|
# Transpose into contiguous 1-D columns (batch() needs C-contiguous arrays).
|
|
|
|
|
_o, high, low, close, _v, _ts = (np.array(col, dtype=np.float64) for col in zip(*candles))
|
2026-05-21 17:50:45 +02:00
|
|
|
rsi = ta.RSI(14).batch(close)
|
|
|
|
|
macd = ta.MACD().batch(close)
|
|
|
|
|
adx = ta.ADX(14).batch(high, low, close)
|
|
|
|
|
last_macd_hist = macd[~np.isnan(macd[:, 2])][-1, 2] if np.any(~np.isnan(macd[:, 2])) else float("nan")
|
|
|
|
|
last_adx = adx[~np.isnan(adx[:, 2])][-1, 2] if np.any(~np.isnan(adx[:, 2])) else float("nan")
|
2026-05-22 12:29:40 +02:00
|
|
|
valid_rsi = rsi[~np.isnan(rsi)]
|
|
|
|
|
last_rsi = valid_rsi[-1] if valid_rsi.size else float("nan")
|
2026-05-21 17:50:45 +02:00
|
|
|
print(
|
|
|
|
|
f" {label:<10} bars={close.size:>5} "
|
2026-05-22 12:29:40 +02:00
|
|
|
f"last_close={close[-1]:>10.4f} rsi={last_rsi:>6.2f} "
|
2026-05-21 17:50:45 +02:00
|
|
|
f"macd_hist={last_macd_hist:+.4f} adx={last_adx:>6.2f}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0] if __doc__ else None)
|
|
|
|
|
p.add_argument("path", help="Path to a 1-minute OHLCV CSV (timestamps in milliseconds)")
|
|
|
|
|
args = p.parse_args()
|
|
|
|
|
|
2026-06-17 01:49:11 +02:00
|
|
|
candles = load(args.path)
|
2026-05-21 17:50:45 +02:00
|
|
|
one_minute = 60_000
|
|
|
|
|
|
|
|
|
|
print(f"Multi-timeframe view of {args.path}")
|
2026-06-17 01:49:11 +02:00
|
|
|
summarize("1m", candles)
|
2026-05-21 17:50:45 +02:00
|
|
|
for label, bucket in [("5m", 5), ("15m", 15), ("1h", 60), ("4h", 240), ("1d", 1440)]:
|
2026-06-17 01:49:11 +02:00
|
|
|
if len(candles) < bucket:
|
2026-05-21 17:50:45 +02:00
|
|
|
continue
|
2026-06-17 01:49:11 +02:00
|
|
|
summarize(label, resample(candles, bucket * one_minute))
|
2026-05-21 17:50:45 +02:00
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|