diff --git a/.pytest_cache/.gitignore b/.pytest_cache/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..08a7f458f1f002823bc794c47ca1996a57e72c86 --- /dev/null +++ b/.pytest_cache/.gitignore @@ -0,0 +1,2 @@ +# Created by pytest automatically. +* diff --git a/.pytest_cache/CACHEDIR.TAG b/.pytest_cache/CACHEDIR.TAG new file mode 100644 index 0000000000000000000000000000000000000000..fce15ad7eaa74e5682b644c84efb75334c112f95 --- /dev/null +++ b/.pytest_cache/CACHEDIR.TAG @@ -0,0 +1,4 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by pytest. +# For information about cache directory tags, see: +# https://bford.info/cachedir/spec.html diff --git a/.pytest_cache/README.md b/.pytest_cache/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c7526af2448672de4537dfed042ed74daadb17bf --- /dev/null +++ b/.pytest_cache/README.md @@ -0,0 +1,8 @@ +# pytest cache directory # + +This directory contains data from the pytest's cache plugin, +which provides the `--lf` and `--ff` options, as well as the `cache` fixture. + +**Do not** commit this to version control. + +See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information. diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed new file mode 100644 index 0000000000000000000000000000000000000000..b7be23b84d1d8b126c0e28e0aae9b760152de38f --- /dev/null +++ b/.pytest_cache/v/cache/lastfailed @@ -0,0 +1,3 @@ +{ + "tests/test_flow_algo.py::test_uptrend_has_positive_cmf_and_obv": true +} \ No newline at end of file diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids new file mode 100644 index 0000000000000000000000000000000000000000..a254908e485ff2586a29b3272e8aa043a655a64c --- /dev/null +++ b/.pytest_cache/v/cache/nodeids @@ -0,0 +1,86 @@ +[ + "tests/test_flow_algo.py::test_downtrend_has_negative_cmf_and_obv", + "tests/test_flow_algo.py::test_empty_frame_returns_none", + "tests/test_flow_algo.py::test_factor_dict_round_trip", + "tests/test_flow_algo.py::test_factors_finite_and_bounded", + "tests/test_flow_algo.py::test_too_short_returns_none", + "tests/test_flow_algo.py::test_uptrend_has_positive_cmf_and_obv", + "tests/test_flow_algo.py::test_vwap_dev_positive_when_close_above_average", + "tests/test_history.py::test_latest_snapshot_returns_newest", + "tests/test_history.py::test_save_and_list_snapshots", + "tests/test_history.py::test_save_empty_df_returns_none", + "tests/test_history.py::test_snapshot_summary_lists_saved_runs", + "tests/test_history.py::test_with_delta_computes_difference", + "tests/test_history.py::test_with_delta_no_previous", + "tests/test_intraday_factor.py::test_aggression_persistence_negative_when_recent_selling", + "tests/test_intraday_factor.py::test_aggression_persistence_positive_when_recent_buying", + "tests/test_intraday_factor.py::test_aggression_persistence_short_history_uses_all", + "tests/test_intraday_factor.py::test_batch", + "tests/test_intraday_factor.py::test_clipped_to_range", + "tests/test_intraday_factor.py::test_empty_returns_zeros", + "tests/test_intraday_factor.py::test_stub_synthesises", + "tests/test_intraday_factor.py::test_vwap_dev_positive_when_above_vwap", + "tests/test_intraday_factor.py::test_vwap_dev_zero_when_price_equals_vwap", + "tests/test_l2_factor.py::test_ask_heavy_book_is_negative", + "tests/test_l2_factor.py::test_balanced_book_is_near_zero", + "tests/test_l2_factor.py::test_batch_returns_all_tickers", + "tests/test_l2_factor.py::test_bid_heavy_book_is_positive", + "tests/test_l2_factor.py::test_book_lying_detection", + "tests/test_l2_factor.py::test_book_lying_discounts_factor", + "tests/test_l2_factor.py::test_clipped_to_range", + "tests/test_l2_factor.py::test_empty_book_returns_zero", + "tests/test_l2_factor.py::test_spoofed_orders_ignored", + "tests/test_l2_factor.py::test_stub_data_source_synthesises", + "tests/test_options_factor.py::test_batch", + "tests/test_options_factor.py::test_call_otm_spike_is_positive", + "tests/test_options_factor.py::test_call_otm_weighted_heaviest", + "tests/test_options_factor.py::test_empty_returns_zero", + "tests/test_options_factor.py::test_put_otm_spike_is_negative", + "tests/test_options_factor.py::test_steady_chain_is_near_zero", + "tests/test_options_factor.py::test_stub_data_source_synthesises", + "tests/test_options_factor.py::test_zscore_constant_returns_zero", + "tests/test_options_factor.py::test_zscore_uses_today_vs_history", + "tests/test_performance.py::test_auto_improve_end_to_end_with_signal", + "tests/test_performance.py::test_auto_improve_handles_no_data", + "tests/test_performance.py::test_collect_eval_tables_with_explicit_cache", + "tests/test_performance.py::test_evaluate_returns_metrics_dict", + "tests/test_performance.py::test_ic_signal_factor_is_predictive", + "tests/test_performance.py::test_load_learned_weights_missing", + "tests/test_performance.py::test_load_learned_weights_round_trip", + "tests/test_performance.py::test_optimize_finds_signal_factor", + "tests/test_performance.py::test_optimize_without_enough_history_returns_baseline", + "tests/test_performance.py::test_performance_log_grows", + "tests/test_scorer.py::test_empty_factors_returns_empty_df", + "tests/test_scorer.py::test_ratings_assignment", + "tests/test_scorer.py::test_robust_zscore_degenerate_series", + "tests/test_scorer.py::test_robust_zscore_on_outliers", + "tests/test_scorer.py::test_score_dataframe_shape", + "tests/test_scorer.py::test_score_range_and_ordering", + "tests/test_scorer.py::test_top_n_buy_and_sell", + "tests/test_scorer.py::test_uptrend_scores_higher_than_downtrend", + "tests/test_scorer.py::test_weights_change_ordering", + "tests/test_tick_factor.py::test_batch", + "tests/test_tick_factor.py::test_block_aggression_negative_when_block_sells", + "tests/test_tick_factor.py::test_block_aggression_positive_when_block_buys", + "tests/test_tick_factor.py::test_block_share_calculation", + "tests/test_tick_factor.py::test_bucket_boundaries", + "tests/test_tick_factor.py::test_buy_ratio_in_unit_interval", + "tests/test_tick_factor.py::test_empty_ticks", + "tests/test_tick_factor.py::test_no_bid_ask_falls_back_to_rolling_mid", + "tests/test_tick_factor.py::test_sign_at_ask_is_buy", + "tests/test_tick_factor.py::test_sign_at_bid_is_sell", + "tests/test_tick_factor.py::test_sign_at_mid_carries_forward", + "tests/test_tick_factor.py::test_stub_synthesises", + "tests/test_universe.py::test_cached_sectors_for_returns_only_known", + "tests/test_universe.py::test_fresh_filter_excludes_stale", + "tests/test_universe.py::test_save_and_load_cache", + "tests/test_watchlist.py::test_load_malformed_returns_empty", + "tests/test_watchlist.py::test_load_missing_returns_empty", + "tests/test_watchlist.py::test_parse_caps_at_max", + "tests/test_watchlist.py::test_parse_empty_or_none", + "tests/test_watchlist.py::test_parse_lowercase_and_dedup", + "tests/test_watchlist.py::test_parse_mixed_delimiters", + "tests/test_watchlist.py::test_parse_rejects_invalid_symbols", + "tests/test_watchlist.py::test_parse_simple", + "tests/test_watchlist.py::test_save_and_load_round_trip" +] \ No newline at end of file diff --git a/README.md b/README.md index 9f3b19bb61be76063e9ba445bb7fc6a840e5c0c2..ea158f0f87337ac196a6277769e9449c7aae1732 100644 --- a/README.md +++ b/README.md @@ -21,20 +21,30 @@ institutional-style buying or selling pressure. Designed to run on ## What it does -For each of ~5000+ US-listed common stocks, the app computes 5 factors and -combines them into a single **-100 to +100 institutional-flow score**: - -| Factor | What it measures | -|-----------------|---------------------------------------------------------------| -| **CMF(20)** | Chaikin Money Flow over 20 days | -| **OBV slope** | 20-day linear regression of On-Balance Volume, normalized | -| **Big-bar ratio** | Unusual-volume + wide-range bars today vs 20-day baseline | -| **VWAP dev** | Close vs 20-day volume-weighted average price | -| **RVOL signed** | Relative volume, sign-flipped by CMF direction | +For each of ~5000+ US-listed common stocks, the app computes **9 factors** +(5 daily flow proxies + 4 "real" institutional-flow signals) and combines +them into a single **-100 to +100 institutional-flow score**: + +| Factor | What it measures | +|----------------------|---------------------------------------------------------------| +| **CMF(20)** | Chaikin Money Flow over 20 days | +| **OBV slope** | 20-day linear regression of On-Balance Volume, normalized | +| **Big-bar ratio** | Unusual-volume + wide-range bars today vs 20-day baseline | +| **VWAP dev** | Close vs 20-day volume-weighted average price | +| **RVOL signed** | Relative volume, sign-flipped by CMF direction | +| **L2 imbalance** | Top-of-book depth + large resting orders (with spoofing discount) | +| **Unusual options** | Vol/OI z-score across 20 days, weighted by moneyness | +| **Block aggression** | Net buy/sell bias in ≥10k-share trades (Lee-Ready tick rule) | +| **Buy persistence** | Buy-ratio average over last 1 hour of 5-min bars | Factors are z-scored cross-sectionally (median + MAD robust normalization) so the most-extreme name in the universe gets ±100. +The last 4 factors come from a pluggable `FactorDataSource` (see +[Data sources](#data-sources) below). By default they are computed from +synthetic stub data committed to the repo, so the app works on +Hugging Face Spaces with **no live market feed**. + **Rating thresholds** (tweakable in the UI): - `≥ +50` → **Strong Buy** - `+20 … +49` → **Buy** @@ -42,9 +52,11 @@ so the most-extreme name in the universe gets ±100. - `-49 … -20` → **Sell** - `≤ -50` → **Strong Sell** -The default weights are `0.30 / 0.25 / 0.20 / 0.15 / 0.10` for -`CMF / OBV / big-bar / VWAP / RVOL`. All sliders are exposed in the -sidebar accordion. +The default weights are +`0.20 / 0.15 / 0.10 / 0.10 / 0.05` for the daily flow factors and +`0.15 / 0.10 / 0.10 / 0.05` for the institutional factors. +The 5 daily sliders are exposed in the sidebar accordion; the 4 +institutional factors always run at their default weights. ## Project layout @@ -55,7 +67,12 @@ us-flow-scanner/ │ ├── __init__.py │ ├── universe.py # Load & filter ~5000 US tickers │ ├── data_fetcher.py # yfinance + Polygon fallback + cache -│ ├── flow_algo.py # 5-factor computation +│ ├── flow_algo.py # 5 daily-flow factors +│ ├── l2_factor.py # Level-2 large-resting-order factor +│ ├── options_factor.py # Unusual options activity +│ ├── tick_factor.py # Lee-Ready tick + size buckets +│ ├── intraday_factor.py # Intraday VWAP + buy persistence +│ ├── factor_sources.py # StubDataSource (default) + FutuDataSource (live) │ ├── scorer.py # Cross-sectional z-score + composite │ ├── persistence.py # Optional HF Dataset round-trip │ ├── history.py # Per-scan snapshots + delta vs prior @@ -63,7 +80,8 @@ us-flow-scanner/ │ ├── performance.py # IC evaluation + self-tuning weights │ └── paths.py # Centralised, env-var-overridable paths ├── data/ -│ └── us_tickers.csv # ~5260 US common stocks (committed) +│ ├── us_tickers.csv # ~5260 US common stocks (committed) +│ └── stubs/ # 30-ticker synthetic L2/options/ticks/intraday ├── tests/ # pytest suite (isolated disk paths) ├── requirements.txt ├── requirements-dev.txt @@ -80,6 +98,24 @@ python app.py Then open http://localhost:7860. +## Data sources + +The four institutional-flow factors (`l2_imbalance`, `unusual_options`, +`block_aggression`, `buy_persistence`) read from a pluggable +`FactorDataSource` (`scanner/factor_sources.py`). + +| Source | When to use | Config env var | +|---------------------|----------------------------------------------------------|----------------------------| +| `StubDataSource` | **Default.** Reads `data/stubs/` synthetic data | `FSCANNER_DATA_SOURCE` unset or `=stub` | +| `FutuDataSource` | Live Level-2 / options / tick data via local Futu OpenD | `FSCANNER_DATA_SOURCE=futu` + `FUTU_OPEND_HOST` + `FUTU_OPEND_PORT` | + +The Space uses `StubDataSource` so it never depends on a live feed. +To regenerate the stub data: + +```bash +python data/stubs/_build_stubs.py +``` + ## Tests ```bash diff --git a/app.py b/app.py index 599f6b190f8b7dc5eed8e0499cf5d037be272cb4..981670c9b4c810109cc847c924b9914221a974c8 100644 --- a/app.py +++ b/app.py @@ -49,10 +49,15 @@ except Exception as _patch_err: # never let this crash startup print(f"[gradio-client patch skipped: {_patch_err}]", file=sys.stderr) from scanner.data_fetcher import fetch_ohlcv +from scanner.factor_sources import get_data_source from scanner.flow_algo import compute_factors from scanner.history import ( latest_snapshot, save_snapshot, snapshot_summary, with_delta, ) +from scanner.intraday_factor import compute_intraday_factors_batch +from scanner.l2_factor import compute_l2_factors +from scanner.options_factor import compute_options_factors +from scanner.tick_factor import compute_tick_factors_batch from scanner import paths from scanner.performance import ( HORIZON_DAYS, auto_improve, load_learned_meta, @@ -110,7 +115,8 @@ def _format_status() -> str: def _result_columns() -> list[str]: return ["ticker", "name", "rating", "score", "score_delta", "last_close", "adv_dollar", "cmf", "obv_slope", "big_bar_ratio", "vwap_dev", - "rvol_signed"] + "rvol_signed", "l2_imbalance", "unusual_options", + "block_aggression", "buy_persistence"] def _df_for_display(df: Optional[pd.DataFrame]) -> Optional[pd.DataFrame]: @@ -468,8 +474,41 @@ def _do_scan( if not factors: return None, valid_frames, scanned_universe, "No tickers had enough data." + # ---------------------------------------------------------------- + # Institutional-flow factors (L2, options, ticks, intraday). + # Computed from the configured FactorDataSource (StubDataSource by + # default; set FSCANNER_DATA_SOURCE=futu for live Futu OpenD). Any + # data-source exception is swallowed - a missing real feed just + # leaves the new factors at neutral (0.0) and the scan still + # produces a valid ranking from the original 5 flow factors. + # ---------------------------------------------------------------- + progress(0.92, desc="Computing institutional factors (L2/options/ticks/intraday)...") + extra_factors = {"l2_imbalance": {}, "unusual_options": {}, + "block_aggression": {}, "buy_persistence": {}} + inst_tickers = [fs.ticker for fs in factors] + try: + source = get_data_source() + l2 = compute_l2_factors(inst_tickers, source=source) + extra_factors["l2_imbalance"] = l2 + opt = compute_options_factors(inst_tickers, source=source) + extra_factors["unusual_options"] = opt + ticks_df = compute_tick_factors_batch(inst_tickers, source=source) + for t in inst_tickers: + if t in ticks_df.index: + extra_factors["block_aggression"][t] = float( + ticks_df.loc[t, "block_aggression"] + ) + intraday_df = compute_intraday_factors_batch(inst_tickers, source=source) + for t in inst_tickers: + if t in intraday_df.index: + extra_factors["buy_persistence"][t] = float( + intraday_df.loc[t, "aggression_persistence"] + ) + except Exception as e: + print(f"[institutional factors skipped: {e}]", file=sys.stderr) + progress(0.94, desc="Scoring..." ) - df = score_factors(factors, weights=weights) + df = score_factors(factors, weights=weights, extra_factors=extra_factors) # Attach names name_map = dict(zip(universe["ticker"], universe["name"])) diff --git a/data/stubs/_build_stubs.py b/data/stubs/_build_stubs.py new file mode 100644 index 0000000000000000000000000000000000000000..0cd4bbe818616d53fd91776f2e3f7dfce526145a --- /dev/null +++ b/data/stubs/_build_stubs.py @@ -0,0 +1,175 @@ +"""Build the bundled synthetic stub data for the four institutional factors. + +Run locally to regenerate:: + + python data/stubs/_build_stubs.py + +This produces realistic-but-synthetic Level-2, options, tick, and +intraday data for ~30 representative tickers. The Space uses this +data so the math can be demonstrated without any live market feed. +""" + +from __future__ import annotations + +import json +import math +import os +import random +import sys +from datetime import datetime, timedelta + +import numpy as np +import pandas as pd + +HERE = os.path.dirname(os.path.abspath(__file__)) +PROJ = os.path.dirname(os.path.dirname(HERE)) +sys.path.insert(0, PROJ) + + +SAMPLE_TICKERS = [ + # Mega caps (high L2 liquidity, deep options) + "AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "META", "TSLA", "AVGO", + # Mid caps (still liquid, smaller options chains) + "AMD", "NFLX", "CRM", "ORCL", "ADBE", "INTC", "QCOM", + # Sector leaders + "JPM", "BAC", "GS", "MS", "V", "MA", "JNJ", "PFE", "UNH", "XOM", "CVX", + # Recent IPOs / volatile names (unusual options activity test) + "PLTR", "GME", "AMC", "RIOT", +] + + +def _rng(ticker: str, salt: str = "") -> random.Random: + return random.Random(f"{ticker}-{salt}") + + +def build_l2(out_dir: str) -> None: + os.makedirs(out_dir, exist_ok=True) + for t in SAMPLE_TICKERS: + rng = _rng(t, "l2") + # Each ticker gets a "lean" direction: positive = bullish, negative = bearish + # Distribute so ~40% bullish, ~40% bearish, ~20% neutral. + lean = rng.choices([-1, -0.5, 0, 0.5, 1], weights=[0.2, 0.2, 0.2, 0.2, 0.2])[0] + mid = rng.uniform(20, 500) + spread = mid * 0.0005 + bids, asks = [], [] + for i in range(10): + bp = mid - spread / 2 - i * spread * 0.5 + ap = mid + spread / 2 + i * spread * 0.5 + # Lean shifts size distribution: bullish = bigger bids; bearish = bigger asks + bs = int(rng.lognormvariate(6 + lean * 0.5, 1.0)) + as_ = int(rng.lognormvariate(6 - lean * 0.5, 1.0)) + age = round(rng.uniform(1.5, 30.0), 1) # all stable + bids.append([round(bp, 2), bs, "NSDQ", age]) + asks.append([round(ap, 2), as_, "NSDQ", round(rng.uniform(1.5, 30.0), 1)]) + out = { + "ticker": t, + "ts": datetime.utcnow().isoformat() + "Z", + "bids": bids, + "asks": asks, + } + with open(os.path.join(out_dir, f"{t}.json"), "w", encoding="utf-8") as fh: + json.dump(out, fh, indent=2) + + +def build_options(out_dir: str) -> None: + os.makedirs(out_dir, exist_ok=True) + today = pd.Timestamp.utcnow().tz_localize(None).normalize() + for t in SAMPLE_TICKERS: + rng = _rng(t, "opt") + rows = [] + # Some tickers have unusual activity on a "spike day" in the last week + spike_day_offset = rng.randint(1, 5) + spike_side = rng.choice(["call", "put"]) + for d in range(20): + date = today - pd.Timedelta(days=d) + for kind in ("call", "put"): + for bucket in ("itm", "atm", "otm"): + base = rng.lognormvariate(7, 0.6) + # Spike day: 3-5x normal volume for the chosen side + spike = 1.0 + if d == spike_day_offset and kind == spike_side: + spike = rng.uniform(3.0, 6.0) + rows.append({ + "date": date, + "kind": kind, + "moneyness": bucket, + "volume": int(base * spike * rng.uniform(0.7, 1.3)), + "oi": int(base * rng.uniform(3, 10)), + "avg_iv": rng.uniform(0.18, 0.65), + }) + df = pd.DataFrame(rows) + df.to_parquet(os.path.join(out_dir, f"{t}.parquet"), index=False) + + +def build_ticks(out_dir: str) -> None: + os.makedirs(out_dir, exist_ok=True) + for t in SAMPLE_TICKERS: + rng = _rng(t, "ticks") + n = rng.randint(800, 1500) + base = rng.uniform(20, 500) + ts0 = pd.Timestamp("2026-06-02") + pd.Timedelta(hours=9, minutes=30) + rows = [] + price = base + for i in range(n): + dt = pd.Timedelta(seconds=i * 1.5 + rng.uniform(0, 1.5)) + price *= 1 + rng.gauss(0, 0.0005) + spread = price * 0.0003 + sz = int(rng.choices( + [50, 100, 200, 500, 1000, 5000, 10000, 20000], + weights=[0.25, 0.25, 0.15, 0.15, 0.10, 0.05, 0.03, 0.02], + )[0]) + rows.append({ + "ts": ts0 + dt, + "price": round(price, 4), + "size": sz, + "bid": round(price - spread / 2, 4), + "ask": round(price + spread / 2, 4), + }) + pd.DataFrame(rows).to_parquet(os.path.join(out_dir, f"{t}.parquet"), index=False) + + +def build_intraday(out_dir: str) -> None: + os.makedirs(out_dir, exist_ok=True) + for t in SAMPLE_TICKERS: + rng = _rng(t, "intra") + bars = [] + day = pd.Timestamp("2026-06-02") + pd.Timedelta(hours=9, minutes=30) + price = rng.uniform(20, 500) + # Each ticker has an aggression lean + buy_bias = rng.gauss(0, 0.08) + for i in range(78): + ts = day + pd.Timedelta(minutes=i * 5) + o = price + ret = rng.gauss(0, 0.003) + c = o * (1 + ret) + h = max(o, c) * (1 + abs(rng.gauss(0, 0.0015))) + l = min(o, c) * (1 - abs(rng.gauss(0, 0.0015))) + v = int(rng.lognormvariate(13, 0.6)) + ratio = 0.5 + buy_bias + rng.gauss(0, 0.06) + ratio = max(0.30, min(0.70, ratio)) + bv = int(v * ratio) + bars.append({ + "bar_start": ts, "open": o, "high": h, "low": l, "close": c, + "volume": v, "buy_vol": bv, "sell_vol": v - bv, + }) + price = c + pd.DataFrame(bars).to_parquet(os.path.join(out_dir, f"{t}.parquet"), index=False) + + +def main() -> None: + print(f"Building stub data in {HERE}/") + for fn, name in [ + (build_l2, "l2"), + (build_options, "options"), + (build_ticks, "ticks"), + (build_intraday, "intraday"), + ]: + path = os.path.join(HERE, name) + fn(path) + files = len(os.listdir(path)) + print(f" {name}: {files} files") + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/data/stubs/intraday/AAPL.parquet b/data/stubs/intraday/AAPL.parquet new file mode 100644 index 0000000000000000000000000000000000000000..ec34e03a7451259c300ed0d7aedd5b594c8595e5 --- /dev/null +++ b/data/stubs/intraday/AAPL.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:298f7305ae43fff7183b736da8d8eae6821325b062c5cfc7148e3d1e90008777 +size 9865 diff --git a/data/stubs/intraday/ADBE.parquet b/data/stubs/intraday/ADBE.parquet new file mode 100644 index 0000000000000000000000000000000000000000..cf3b8d9667573ed6d38b6ba69b14466c32fe8566 --- /dev/null +++ b/data/stubs/intraday/ADBE.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82018897a34eb7c46ff6c157c966e2dbc0b8a5e43fcb80f0e442d23ee7d99a56 +size 9869 diff --git a/data/stubs/intraday/AMC.parquet b/data/stubs/intraday/AMC.parquet new file mode 100644 index 0000000000000000000000000000000000000000..71dfe802bc6d42eaa8ad26cd5a33aa439894bbaf --- /dev/null +++ b/data/stubs/intraday/AMC.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c33b3c38bb4153a3c353231f2e52455a522876eae5739259b63591a80956ce1c +size 9858 diff --git a/data/stubs/intraday/AMD.parquet b/data/stubs/intraday/AMD.parquet new file mode 100644 index 0000000000000000000000000000000000000000..c6be3c397f3ae363a470f18f7d5a9d0ac1a83033 --- /dev/null +++ b/data/stubs/intraday/AMD.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7140c7baa833545cbb6699e7c3386bfa65220f60697a46ec087777dc21308516 +size 9869 diff --git a/data/stubs/intraday/AMZN.parquet b/data/stubs/intraday/AMZN.parquet new file mode 100644 index 0000000000000000000000000000000000000000..c9aa6cf0d1018e97345143aa6a8e6ad830016581 --- /dev/null +++ b/data/stubs/intraday/AMZN.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfa898fee9d470f01410c04268d5cf3a6f141d7718c883a748d68b6df77af274 +size 9868 diff --git a/data/stubs/intraday/AVGO.parquet b/data/stubs/intraday/AVGO.parquet new file mode 100644 index 0000000000000000000000000000000000000000..5d89b65b4c896c865ac297d5011fc4a0ad9119c3 --- /dev/null +++ b/data/stubs/intraday/AVGO.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ce4dcac779dbc8fdd5a4fe8237f0a027f916d01bb9385ba59d7456ee98fabb2 +size 9871 diff --git a/data/stubs/intraday/BAC.parquet b/data/stubs/intraday/BAC.parquet new file mode 100644 index 0000000000000000000000000000000000000000..fa99066101e10b396ee6c0bcce99b3f439418d8e --- /dev/null +++ b/data/stubs/intraday/BAC.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8b29da1e9e88a213482bffe62c5e8750273fb496522d3af2a379630ed021266 +size 9862 diff --git a/data/stubs/intraday/CRM.parquet b/data/stubs/intraday/CRM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..7988e921399ada57bbe115e47606dadd7e4ce3ac --- /dev/null +++ b/data/stubs/intraday/CRM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35ad2b081f5ddf9cf65ee0d7592fdcbb4efc205035354e856cc89abe1853c473 +size 9860 diff --git a/data/stubs/intraday/CVX.parquet b/data/stubs/intraday/CVX.parquet new file mode 100644 index 0000000000000000000000000000000000000000..efd7602d53fa4272bf52ed982d44938e4a14956d --- /dev/null +++ b/data/stubs/intraday/CVX.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:365691d5be155d46996003180abdd31fb1f913a8d554df8149e699d8c1a61bf7 +size 9879 diff --git a/data/stubs/intraday/GME.parquet b/data/stubs/intraday/GME.parquet new file mode 100644 index 0000000000000000000000000000000000000000..39387011cce42a740db7952d56811bfc0cdcf1f2 --- /dev/null +++ b/data/stubs/intraday/GME.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e24286c1b28cb9abbd9896b936478e33ece309863bbb657a5899f7bc61a4a3dc +size 9862 diff --git a/data/stubs/intraday/GOOGL.parquet b/data/stubs/intraday/GOOGL.parquet new file mode 100644 index 0000000000000000000000000000000000000000..0395907495ec59a33d41a3f4c6e7053365960c89 --- /dev/null +++ b/data/stubs/intraday/GOOGL.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73f89d334d7cdc6f9a9b823cd2994f272e9ec85c79b0fd6537dd9d815d7e8254 +size 9863 diff --git a/data/stubs/intraday/GS.parquet b/data/stubs/intraday/GS.parquet new file mode 100644 index 0000000000000000000000000000000000000000..02082292faa286b1bef71309d380250b3f79a140 --- /dev/null +++ b/data/stubs/intraday/GS.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66918c653bd47f4d320a8a135edb33a2824bedd9e707640efacc30fe52b66a09 +size 9858 diff --git a/data/stubs/intraday/INTC.parquet b/data/stubs/intraday/INTC.parquet new file mode 100644 index 0000000000000000000000000000000000000000..6e043f742e0ba5941404d571b1354012e5191b1a --- /dev/null +++ b/data/stubs/intraday/INTC.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2feafd870509f52503a9a44dd77e5f214d2641f323af2190a51fdc396b29d8f +size 9875 diff --git a/data/stubs/intraday/JNJ.parquet b/data/stubs/intraday/JNJ.parquet new file mode 100644 index 0000000000000000000000000000000000000000..0c1187229b42fed8e086f293a6daa69f83c656de --- /dev/null +++ b/data/stubs/intraday/JNJ.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c838327c83ba33e12743068bc4c25109085a1e08fd4dc1077d62f68dca58e0c8 +size 9863 diff --git a/data/stubs/intraday/JPM.parquet b/data/stubs/intraday/JPM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..bc902123a484e0259c14113a354fc11734772e76 --- /dev/null +++ b/data/stubs/intraday/JPM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52b8c0f9d7631b5e362569096a3408dfee1073f88f69cef1997e968bf2da49bc +size 9864 diff --git a/data/stubs/intraday/MA.parquet b/data/stubs/intraday/MA.parquet new file mode 100644 index 0000000000000000000000000000000000000000..5d2af77915415c07eac191474175215963c864a7 --- /dev/null +++ b/data/stubs/intraday/MA.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58538c46ab8daa87a5ce903b9a1d896062e860bdf736a439cdfe384a9159fcd6 +size 9860 diff --git a/data/stubs/intraday/META.parquet b/data/stubs/intraday/META.parquet new file mode 100644 index 0000000000000000000000000000000000000000..39b25fc83e3556229e72fff2c15628a01736acd9 --- /dev/null +++ b/data/stubs/intraday/META.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae343e29cc0d9eff33b1df9eba200c0499b43c030c5a036147c610f3da0ba63d +size 9865 diff --git a/data/stubs/intraday/MS.parquet b/data/stubs/intraday/MS.parquet new file mode 100644 index 0000000000000000000000000000000000000000..1071b4998615e3dbe26303d69d9f15bf53b7a917 --- /dev/null +++ b/data/stubs/intraday/MS.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75a2fe6ded4fdac33d2acfdc4434821feb43281a127688ae4b9890d27aec4240 +size 9871 diff --git a/data/stubs/intraday/MSFT.parquet b/data/stubs/intraday/MSFT.parquet new file mode 100644 index 0000000000000000000000000000000000000000..79ebe5c3a4ffda86e20059620150bdf569699c8a --- /dev/null +++ b/data/stubs/intraday/MSFT.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de1121e0a053b1a6452f224660718f3dfd57b853680c99668dddb882f6bbcf2b +size 9860 diff --git a/data/stubs/intraday/NFLX.parquet b/data/stubs/intraday/NFLX.parquet new file mode 100644 index 0000000000000000000000000000000000000000..f2035626992851d65e77bfe2e6dc3008ef40e567 --- /dev/null +++ b/data/stubs/intraday/NFLX.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:686d12c7d913e1d4b3a329838d1ad33b186aa8e454d0b9d93ff81005060f4763 +size 9865 diff --git a/data/stubs/intraday/NVDA.parquet b/data/stubs/intraday/NVDA.parquet new file mode 100644 index 0000000000000000000000000000000000000000..06ad226e17929ac1eeb2cea49bbe42365c506335 --- /dev/null +++ b/data/stubs/intraday/NVDA.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e82b91bc034212739526cb525abc80c0954e59b3b6040f52c806d8a2e18abab +size 9876 diff --git a/data/stubs/intraday/ORCL.parquet b/data/stubs/intraday/ORCL.parquet new file mode 100644 index 0000000000000000000000000000000000000000..9ab4a2f95b95d2c26946a1b51276db75695fae79 --- /dev/null +++ b/data/stubs/intraday/ORCL.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77e23fdf16981568005a4aefbe304fb7cfff359e71c37b25937d035125e31abf +size 9871 diff --git a/data/stubs/intraday/PFE.parquet b/data/stubs/intraday/PFE.parquet new file mode 100644 index 0000000000000000000000000000000000000000..12650e01a2d27a5b0bd332cf29c1dbbd2fe78e61 --- /dev/null +++ b/data/stubs/intraday/PFE.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88fbdb3c8ac5b240a4a0f009dbd34967452382d7d1234928767441b9f303493b +size 9867 diff --git a/data/stubs/intraday/PLTR.parquet b/data/stubs/intraday/PLTR.parquet new file mode 100644 index 0000000000000000000000000000000000000000..380fead87448e0bc303e6be23fdf11719810345e --- /dev/null +++ b/data/stubs/intraday/PLTR.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2a27ccd71b6acd5acf428f1c72502b57e76e4a15e664e0c1195782771b156ce +size 9864 diff --git a/data/stubs/intraday/QCOM.parquet b/data/stubs/intraday/QCOM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..e3a8758fa1561661e7942b494dcfb9e8a3d3dfbc --- /dev/null +++ b/data/stubs/intraday/QCOM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d8a666be419f2ff5488b29e1d764ffb8009838d21f492e431990432b06a56c9 +size 9866 diff --git a/data/stubs/intraday/RIOT.parquet b/data/stubs/intraday/RIOT.parquet new file mode 100644 index 0000000000000000000000000000000000000000..c44eb8b852488ec9517fba29f663dc1a51ae9383 --- /dev/null +++ b/data/stubs/intraday/RIOT.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6edfa9aeba6d2ad4067d6293652533446b95bccd04390c7965ae47f917ee60c0 +size 9860 diff --git a/data/stubs/intraday/TSLA.parquet b/data/stubs/intraday/TSLA.parquet new file mode 100644 index 0000000000000000000000000000000000000000..b19a5f7e2e2831b2d008844e8fd6efd3ade15dec --- /dev/null +++ b/data/stubs/intraday/TSLA.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d34a1b99abd622cd4064d685ea2086f7fdea1b4a53ab1938a4b18a429ed9899b +size 9879 diff --git a/data/stubs/intraday/UNH.parquet b/data/stubs/intraday/UNH.parquet new file mode 100644 index 0000000000000000000000000000000000000000..c3d3e4f4ed8c7f8a812f8dd19f2990a30086c66c --- /dev/null +++ b/data/stubs/intraday/UNH.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c20746a690566500734e4759a62845b2a46fe9c05032b3a0a303b3b5c0b6beb +size 9862 diff --git a/data/stubs/intraday/V.parquet b/data/stubs/intraday/V.parquet new file mode 100644 index 0000000000000000000000000000000000000000..83970ac6191f36fecf64983f30614ca3d1ebddfb --- /dev/null +++ b/data/stubs/intraday/V.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb4d635e599b64512e458ffdbbc745d5685628ac863ddd4f1774e70f228f21e6 +size 9859 diff --git a/data/stubs/intraday/XOM.parquet b/data/stubs/intraday/XOM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..b714f873ed35e2e5d94b73dcf97e34ad34f2383b --- /dev/null +++ b/data/stubs/intraday/XOM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f73ba23bb0ad783d5beabb164ddb69dee240f21a3838ebfd442e53596ac307ba +size 9866 diff --git a/data/stubs/l2/AAPL.json b/data/stubs/l2/AAPL.json new file mode 100644 index 0000000000000000000000000000000000000000..6b52c0b2831cf047e2dc847c52e0a54fe4e216f9 --- /dev/null +++ b/data/stubs/l2/AAPL.json @@ -0,0 +1,128 @@ +{ + "ticker": "AAPL", + "ts": "2026-06-03T02:13:03.084745Z", + "bids": [ + [ + 33.32, + 799, + "NSDQ", + 16.2 + ], + [ + 33.31, + 268, + "NSDQ", + 10.1 + ], + [ + 33.3, + 486, + "NSDQ", + 9.4 + ], + [ + 33.29, + 63, + "NSDQ", + 25.0 + ], + [ + 33.28, + 1987, + "NSDQ", + 17.9 + ], + [ + 33.28, + 1599, + "NSDQ", + 24.4 + ], + [ + 33.27, + 221, + "NSDQ", + 7.3 + ], + [ + 33.26, + 189, + "NSDQ", + 20.1 + ], + [ + 33.25, + 142, + "NSDQ", + 15.8 + ], + [ + 33.24, + 849, + "NSDQ", + 21.1 + ] + ], + "asks": [ + [ + 33.33, + 286, + "NSDQ", + 10.1 + ], + [ + 33.34, + 2237, + "NSDQ", + 27.3 + ], + [ + 33.35, + 949, + "NSDQ", + 14.1 + ], + [ + 33.36, + 232, + "NSDQ", + 27.2 + ], + [ + 33.37, + 290, + "NSDQ", + 24.0 + ], + [ + 33.38, + 93, + "NSDQ", + 22.3 + ], + [ + 33.38, + 350, + "NSDQ", + 16.3 + ], + [ + 33.39, + 343, + "NSDQ", + 15.7 + ], + [ + 33.4, + 465, + "NSDQ", + 9.3 + ], + [ + 33.41, + 275, + "NSDQ", + 13.3 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/ADBE.json b/data/stubs/l2/ADBE.json new file mode 100644 index 0000000000000000000000000000000000000000..387aef70200156f487a8603f3e938e35c192db0a --- /dev/null +++ b/data/stubs/l2/ADBE.json @@ -0,0 +1,128 @@ +{ + "ticker": "ADBE", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 496.26, + 1021, + "NSDQ", + 16.1 + ], + [ + 496.14, + 890, + "NSDQ", + 3.6 + ], + [ + 496.01, + 216, + "NSDQ", + 20.9 + ], + [ + 495.89, + 440, + "NSDQ", + 4.2 + ], + [ + 495.77, + 606, + "NSDQ", + 5.4 + ], + [ + 495.64, + 632, + "NSDQ", + 1.8 + ], + [ + 495.52, + 964, + "NSDQ", + 10.5 + ], + [ + 495.39, + 162, + "NSDQ", + 13.5 + ], + [ + 495.27, + 558, + "NSDQ", + 18.2 + ], + [ + 495.14, + 4527, + "NSDQ", + 23.3 + ] + ], + "asks": [ + [ + 496.51, + 297, + "NSDQ", + 17.0 + ], + [ + 496.63, + 202, + "NSDQ", + 11.1 + ], + [ + 496.76, + 55, + "NSDQ", + 25.9 + ], + [ + 496.88, + 757, + "NSDQ", + 22.0 + ], + [ + 497.01, + 158, + "NSDQ", + 13.0 + ], + [ + 497.13, + 147, + "NSDQ", + 28.0 + ], + [ + 497.25, + 501, + "NSDQ", + 24.0 + ], + [ + 497.38, + 266, + "NSDQ", + 7.9 + ], + [ + 497.5, + 124, + "NSDQ", + 10.2 + ], + [ + 497.63, + 1034, + "NSDQ", + 14.9 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/AMC.json b/data/stubs/l2/AMC.json new file mode 100644 index 0000000000000000000000000000000000000000..be1d463022d0e09d0dd5e107d2b5957ec0357b87 --- /dev/null +++ b/data/stubs/l2/AMC.json @@ -0,0 +1,128 @@ +{ + "ticker": "AMC", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 381.12, + 286, + "NSDQ", + 7.3 + ], + [ + 381.02, + 86, + "NSDQ", + 17.7 + ], + [ + 380.93, + 1491, + "NSDQ", + 2.4 + ], + [ + 380.83, + 653, + "NSDQ", + 4.1 + ], + [ + 380.73, + 105, + "NSDQ", + 17.3 + ], + [ + 380.64, + 476, + "NSDQ", + 29.8 + ], + [ + 380.54, + 77, + "NSDQ", + 21.7 + ], + [ + 380.45, + 292, + "NSDQ", + 10.8 + ], + [ + 380.35, + 146, + "NSDQ", + 23.5 + ], + [ + 380.26, + 429, + "NSDQ", + 2.0 + ] + ], + "asks": [ + [ + 381.31, + 981, + "NSDQ", + 11.2 + ], + [ + 381.4, + 941, + "NSDQ", + 12.2 + ], + [ + 381.5, + 301, + "NSDQ", + 14.8 + ], + [ + 381.59, + 2090, + "NSDQ", + 17.9 + ], + [ + 381.69, + 568, + "NSDQ", + 1.7 + ], + [ + 381.78, + 180, + "NSDQ", + 17.0 + ], + [ + 381.88, + 96, + "NSDQ", + 3.3 + ], + [ + 381.97, + 330, + "NSDQ", + 17.5 + ], + [ + 382.07, + 270, + "NSDQ", + 15.9 + ], + [ + 382.16, + 431, + "NSDQ", + 8.5 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/AMD.json b/data/stubs/l2/AMD.json new file mode 100644 index 0000000000000000000000000000000000000000..6ef4bea2736abde541ce4cd18098792ff871e07b --- /dev/null +++ b/data/stubs/l2/AMD.json @@ -0,0 +1,128 @@ +{ + "ticker": "AMD", + "ts": "2026-06-03T02:13:03.094736Z", + "bids": [ + [ + 267.94, + 71, + "NSDQ", + 16.5 + ], + [ + 267.87, + 667, + "NSDQ", + 20.9 + ], + [ + 267.8, + 120, + "NSDQ", + 9.0 + ], + [ + 267.74, + 498, + "NSDQ", + 18.1 + ], + [ + 267.67, + 432, + "NSDQ", + 15.3 + ], + [ + 267.6, + 569, + "NSDQ", + 26.0 + ], + [ + 267.53, + 611, + "NSDQ", + 8.6 + ], + [ + 267.47, + 204, + "NSDQ", + 10.1 + ], + [ + 267.4, + 235, + "NSDQ", + 20.2 + ], + [ + 267.33, + 376, + "NSDQ", + 26.5 + ] + ], + "asks": [ + [ + 268.07, + 523, + "NSDQ", + 2.5 + ], + [ + 268.14, + 4249, + "NSDQ", + 12.9 + ], + [ + 268.2, + 622, + "NSDQ", + 4.7 + ], + [ + 268.27, + 250, + "NSDQ", + 1.6 + ], + [ + 268.34, + 904, + "NSDQ", + 17.1 + ], + [ + 268.41, + 80, + "NSDQ", + 3.8 + ], + [ + 268.47, + 482, + "NSDQ", + 28.4 + ], + [ + 268.54, + 3989, + "NSDQ", + 28.2 + ], + [ + 268.61, + 879, + "NSDQ", + 1.8 + ], + [ + 268.67, + 1201, + "NSDQ", + 15.4 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/AMZN.json b/data/stubs/l2/AMZN.json new file mode 100644 index 0000000000000000000000000000000000000000..03625106e0ae693139cc56b1f1299a8fe6f9a139 --- /dev/null +++ b/data/stubs/l2/AMZN.json @@ -0,0 +1,128 @@ +{ + "ticker": "AMZN", + "ts": "2026-06-03T02:13:03.094736Z", + "bids": [ + [ + 459.3, + 256, + "NSDQ", + 28.0 + ], + [ + 459.18, + 640, + "NSDQ", + 29.5 + ], + [ + 459.07, + 157, + "NSDQ", + 30.0 + ], + [ + 458.95, + 2818, + "NSDQ", + 24.0 + ], + [ + 458.84, + 308, + "NSDQ", + 9.5 + ], + [ + 458.72, + 2333, + "NSDQ", + 27.2 + ], + [ + 458.61, + 3014, + "NSDQ", + 22.4 + ], + [ + 458.49, + 50, + "NSDQ", + 9.2 + ], + [ + 458.38, + 271, + "NSDQ", + 25.7 + ], + [ + 458.26, + 1153, + "NSDQ", + 10.2 + ] + ], + "asks": [ + [ + 459.53, + 103, + "NSDQ", + 22.5 + ], + [ + 459.64, + 858, + "NSDQ", + 4.5 + ], + [ + 459.76, + 84, + "NSDQ", + 18.9 + ], + [ + 459.87, + 25, + "NSDQ", + 7.7 + ], + [ + 459.99, + 269, + "NSDQ", + 10.9 + ], + [ + 460.1, + 777, + "NSDQ", + 18.6 + ], + [ + 460.22, + 92, + "NSDQ", + 20.1 + ], + [ + 460.33, + 830, + "NSDQ", + 22.4 + ], + [ + 460.45, + 196, + "NSDQ", + 20.4 + ], + [ + 460.56, + 994, + "NSDQ", + 9.7 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/AVGO.json b/data/stubs/l2/AVGO.json new file mode 100644 index 0000000000000000000000000000000000000000..e48a3461e7f61c38912401f3a2f443cd3260cbf2 --- /dev/null +++ b/data/stubs/l2/AVGO.json @@ -0,0 +1,128 @@ +{ + "ticker": "AVGO", + "ts": "2026-06-03T02:13:03.094736Z", + "bids": [ + [ + 100.8, + 110, + "NSDQ", + 26.0 + ], + [ + 100.78, + 168, + "NSDQ", + 26.4 + ], + [ + 100.75, + 245, + "NSDQ", + 27.3 + ], + [ + 100.73, + 772, + "NSDQ", + 27.7 + ], + [ + 100.7, + 1962, + "NSDQ", + 10.4 + ], + [ + 100.67, + 271, + "NSDQ", + 12.4 + ], + [ + 100.65, + 826, + "NSDQ", + 23.4 + ], + [ + 100.62, + 385, + "NSDQ", + 26.8 + ], + [ + 100.6, + 346, + "NSDQ", + 4.5 + ], + [ + 100.57, + 432, + "NSDQ", + 7.9 + ] + ], + "asks": [ + [ + 100.85, + 648, + "NSDQ", + 17.6 + ], + [ + 100.88, + 161, + "NSDQ", + 7.7 + ], + [ + 100.9, + 412, + "NSDQ", + 19.0 + ], + [ + 100.93, + 694, + "NSDQ", + 2.3 + ], + [ + 100.95, + 1602, + "NSDQ", + 4.5 + ], + [ + 100.98, + 745, + "NSDQ", + 18.3 + ], + [ + 101.0, + 966, + "NSDQ", + 6.4 + ], + [ + 101.03, + 1183, + "NSDQ", + 2.8 + ], + [ + 101.05, + 273, + "NSDQ", + 22.8 + ], + [ + 101.08, + 173, + "NSDQ", + 5.4 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/BAC.json b/data/stubs/l2/BAC.json new file mode 100644 index 0000000000000000000000000000000000000000..1f81a0841b54782941235fa039f2775e0039d8c7 --- /dev/null +++ b/data/stubs/l2/BAC.json @@ -0,0 +1,128 @@ +{ + "ticker": "BAC", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 372.76, + 344, + "NSDQ", + 16.9 + ], + [ + 372.66, + 261, + "NSDQ", + 22.7 + ], + [ + 372.57, + 272, + "NSDQ", + 12.9 + ], + [ + 372.48, + 315, + "NSDQ", + 29.1 + ], + [ + 372.38, + 1017, + "NSDQ", + 20.7 + ], + [ + 372.29, + 2350, + "NSDQ", + 26.9 + ], + [ + 372.2, + 948, + "NSDQ", + 14.8 + ], + [ + 372.1, + 438, + "NSDQ", + 17.6 + ], + [ + 372.01, + 372, + "NSDQ", + 4.1 + ], + [ + 371.92, + 350, + "NSDQ", + 25.6 + ] + ], + "asks": [ + [ + 372.94, + 202, + "NSDQ", + 16.6 + ], + [ + 373.04, + 55, + "NSDQ", + 13.0 + ], + [ + 373.13, + 61, + "NSDQ", + 11.6 + ], + [ + 373.22, + 170, + "NSDQ", + 18.6 + ], + [ + 373.32, + 318, + "NSDQ", + 1.5 + ], + [ + 373.41, + 168, + "NSDQ", + 19.1 + ], + [ + 373.5, + 66, + "NSDQ", + 24.7 + ], + [ + 373.6, + 112, + "NSDQ", + 29.5 + ], + [ + 373.69, + 129, + "NSDQ", + 16.4 + ], + [ + 373.78, + 215, + "NSDQ", + 18.6 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/CRM.json b/data/stubs/l2/CRM.json new file mode 100644 index 0000000000000000000000000000000000000000..354f4fa7c6046998a3956afaf54d8d2e2a38b29a --- /dev/null +++ b/data/stubs/l2/CRM.json @@ -0,0 +1,128 @@ +{ + "ticker": "CRM", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 294.82, + 314, + "NSDQ", + 15.7 + ], + [ + 294.74, + 1191, + "NSDQ", + 20.5 + ], + [ + 294.67, + 2014, + "NSDQ", + 24.5 + ], + [ + 294.59, + 228, + "NSDQ", + 14.7 + ], + [ + 294.52, + 1408, + "NSDQ", + 28.3 + ], + [ + 294.45, + 341, + "NSDQ", + 21.6 + ], + [ + 294.37, + 396, + "NSDQ", + 12.7 + ], + [ + 294.3, + 567, + "NSDQ", + 15.2 + ], + [ + 294.23, + 694, + "NSDQ", + 20.6 + ], + [ + 294.15, + 457, + "NSDQ", + 19.1 + ] + ], + "asks": [ + [ + 294.96, + 1034, + "NSDQ", + 9.3 + ], + [ + 295.04, + 758, + "NSDQ", + 16.5 + ], + [ + 295.11, + 2466, + "NSDQ", + 29.3 + ], + [ + 295.18, + 536, + "NSDQ", + 18.4 + ], + [ + 295.26, + 490, + "NSDQ", + 17.2 + ], + [ + 295.33, + 1480, + "NSDQ", + 21.2 + ], + [ + 295.41, + 1280, + "NSDQ", + 17.7 + ], + [ + 295.48, + 587, + "NSDQ", + 7.3 + ], + [ + 295.55, + 278, + "NSDQ", + 21.0 + ], + [ + 295.63, + 1060, + "NSDQ", + 11.3 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/CVX.json b/data/stubs/l2/CVX.json new file mode 100644 index 0000000000000000000000000000000000000000..fa18c92d79b78b34e4d05f611fd0eecd73fc6dc0 --- /dev/null +++ b/data/stubs/l2/CVX.json @@ -0,0 +1,128 @@ +{ + "ticker": "CVX", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 346.98, + 224, + "NSDQ", + 20.5 + ], + [ + 346.89, + 2087, + "NSDQ", + 21.1 + ], + [ + 346.8, + 1395, + "NSDQ", + 10.7 + ], + [ + 346.72, + 287, + "NSDQ", + 25.8 + ], + [ + 346.63, + 1373, + "NSDQ", + 17.5 + ], + [ + 346.54, + 1756, + "NSDQ", + 25.6 + ], + [ + 346.46, + 917, + "NSDQ", + 12.6 + ], + [ + 346.37, + 424, + "NSDQ", + 11.3 + ], + [ + 346.28, + 841, + "NSDQ", + 7.3 + ], + [ + 346.2, + 439, + "NSDQ", + 13.8 + ] + ], + "asks": [ + [ + 347.15, + 126, + "NSDQ", + 11.4 + ], + [ + 347.24, + 136, + "NSDQ", + 2.2 + ], + [ + 347.32, + 548, + "NSDQ", + 14.6 + ], + [ + 347.41, + 135, + "NSDQ", + 19.0 + ], + [ + 347.5, + 1123, + "NSDQ", + 7.7 + ], + [ + 347.58, + 178, + "NSDQ", + 8.6 + ], + [ + 347.67, + 305, + "NSDQ", + 13.2 + ], + [ + 347.76, + 412, + "NSDQ", + 10.2 + ], + [ + 347.84, + 139, + "NSDQ", + 23.4 + ], + [ + 347.93, + 573, + "NSDQ", + 17.5 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/GME.json b/data/stubs/l2/GME.json new file mode 100644 index 0000000000000000000000000000000000000000..87929fef425db1beff11de30d0583ac89c2e4c7f --- /dev/null +++ b/data/stubs/l2/GME.json @@ -0,0 +1,128 @@ +{ + "ticker": "GME", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 146.47, + 288, + "NSDQ", + 9.6 + ], + [ + 146.43, + 298, + "NSDQ", + 21.3 + ], + [ + 146.39, + 140, + "NSDQ", + 10.5 + ], + [ + 146.36, + 374, + "NSDQ", + 18.6 + ], + [ + 146.32, + 526, + "NSDQ", + 4.4 + ], + [ + 146.28, + 1620, + "NSDQ", + 10.3 + ], + [ + 146.25, + 407, + "NSDQ", + 12.0 + ], + [ + 146.21, + 209, + "NSDQ", + 18.8 + ], + [ + 146.17, + 262, + "NSDQ", + 26.3 + ], + [ + 146.14, + 168, + "NSDQ", + 19.5 + ] + ], + "asks": [ + [ + 146.54, + 579, + "NSDQ", + 22.8 + ], + [ + 146.58, + 508, + "NSDQ", + 26.3 + ], + [ + 146.61, + 1172, + "NSDQ", + 25.7 + ], + [ + 146.65, + 1320, + "NSDQ", + 20.0 + ], + [ + 146.69, + 422, + "NSDQ", + 15.7 + ], + [ + 146.72, + 88, + "NSDQ", + 12.4 + ], + [ + 146.76, + 109, + "NSDQ", + 16.5 + ], + [ + 146.79, + 333, + "NSDQ", + 12.2 + ], + [ + 146.83, + 406, + "NSDQ", + 6.2 + ], + [ + 146.87, + 177, + "NSDQ", + 29.5 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/GOOGL.json b/data/stubs/l2/GOOGL.json new file mode 100644 index 0000000000000000000000000000000000000000..afbc77b24ade4fbde4d21321f29c6fb397dcf7d4 --- /dev/null +++ b/data/stubs/l2/GOOGL.json @@ -0,0 +1,128 @@ +{ + "ticker": "GOOGL", + "ts": "2026-06-03T02:13:03.094736Z", + "bids": [ + [ + 481.63, + 238, + "NSDQ", + 16.6 + ], + [ + 481.51, + 269, + "NSDQ", + 21.0 + ], + [ + 481.39, + 1355, + "NSDQ", + 10.5 + ], + [ + 481.27, + 91, + "NSDQ", + 14.5 + ], + [ + 481.15, + 1091, + "NSDQ", + 12.3 + ], + [ + 481.03, + 115, + "NSDQ", + 17.2 + ], + [ + 480.91, + 131, + "NSDQ", + 17.7 + ], + [ + 480.78, + 679, + "NSDQ", + 6.3 + ], + [ + 480.66, + 156, + "NSDQ", + 29.2 + ], + [ + 480.54, + 538, + "NSDQ", + 20.9 + ] + ], + "asks": [ + [ + 481.87, + 1097, + "NSDQ", + 26.7 + ], + [ + 481.99, + 310, + "NSDQ", + 24.5 + ], + [ + 482.11, + 1357, + "NSDQ", + 10.3 + ], + [ + 482.23, + 1664, + "NSDQ", + 5.6 + ], + [ + 482.35, + 1273, + "NSDQ", + 12.1 + ], + [ + 482.47, + 1003, + "NSDQ", + 28.8 + ], + [ + 482.59, + 1621, + "NSDQ", + 10.9 + ], + [ + 482.71, + 1486, + "NSDQ", + 24.4 + ], + [ + 482.83, + 579, + "NSDQ", + 2.5 + ], + [ + 482.95, + 4857, + "NSDQ", + 20.9 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/GS.json b/data/stubs/l2/GS.json new file mode 100644 index 0000000000000000000000000000000000000000..ce901ae59287240e917c6f61c39d338d96b4471d --- /dev/null +++ b/data/stubs/l2/GS.json @@ -0,0 +1,128 @@ +{ + "ticker": "GS", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 203.18, + 123, + "NSDQ", + 4.4 + ], + [ + 203.13, + 163, + "NSDQ", + 18.2 + ], + [ + 203.08, + 1150, + "NSDQ", + 2.9 + ], + [ + 203.03, + 23, + "NSDQ", + 2.9 + ], + [ + 202.98, + 171, + "NSDQ", + 5.6 + ], + [ + 202.93, + 2206, + "NSDQ", + 8.2 + ], + [ + 202.88, + 784, + "NSDQ", + 15.6 + ], + [ + 202.83, + 515, + "NSDQ", + 9.6 + ], + [ + 202.78, + 883, + "NSDQ", + 24.2 + ], + [ + 202.73, + 100, + "NSDQ", + 6.2 + ] + ], + "asks": [ + [ + 203.29, + 3501, + "NSDQ", + 28.9 + ], + [ + 203.34, + 173, + "NSDQ", + 27.3 + ], + [ + 203.39, + 507, + "NSDQ", + 5.8 + ], + [ + 203.44, + 1431, + "NSDQ", + 16.3 + ], + [ + 203.49, + 1297, + "NSDQ", + 16.1 + ], + [ + 203.54, + 217, + "NSDQ", + 7.9 + ], + [ + 203.59, + 128, + "NSDQ", + 28.6 + ], + [ + 203.64, + 84, + "NSDQ", + 16.2 + ], + [ + 203.69, + 429, + "NSDQ", + 11.0 + ], + [ + 203.74, + 94, + "NSDQ", + 20.8 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/INTC.json b/data/stubs/l2/INTC.json new file mode 100644 index 0000000000000000000000000000000000000000..a1b3b5ee08898c16352e34b8b96ff856b96bcac3 --- /dev/null +++ b/data/stubs/l2/INTC.json @@ -0,0 +1,128 @@ +{ + "ticker": "INTC", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 260.39, + 819, + "NSDQ", + 20.2 + ], + [ + 260.32, + 368, + "NSDQ", + 17.4 + ], + [ + 260.26, + 150, + "NSDQ", + 21.4 + ], + [ + 260.19, + 1361, + "NSDQ", + 5.6 + ], + [ + 260.12, + 387, + "NSDQ", + 15.3 + ], + [ + 260.06, + 1185, + "NSDQ", + 13.2 + ], + [ + 259.99, + 51, + "NSDQ", + 9.3 + ], + [ + 259.93, + 126, + "NSDQ", + 14.1 + ], + [ + 259.86, + 112, + "NSDQ", + 5.3 + ], + [ + 259.8, + 310, + "NSDQ", + 5.2 + ] + ], + "asks": [ + [ + 260.52, + 277, + "NSDQ", + 26.6 + ], + [ + 260.58, + 567, + "NSDQ", + 7.3 + ], + [ + 260.65, + 274, + "NSDQ", + 29.7 + ], + [ + 260.71, + 2263, + "NSDQ", + 8.2 + ], + [ + 260.78, + 1005, + "NSDQ", + 14.1 + ], + [ + 260.84, + 673, + "NSDQ", + 17.3 + ], + [ + 260.91, + 208, + "NSDQ", + 15.8 + ], + [ + 260.97, + 678, + "NSDQ", + 4.2 + ], + [ + 261.04, + 760, + "NSDQ", + 10.8 + ], + [ + 261.1, + 339, + "NSDQ", + 15.4 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/JNJ.json b/data/stubs/l2/JNJ.json new file mode 100644 index 0000000000000000000000000000000000000000..362ca9b153f68eae38da91bac4a5f19f59761866 --- /dev/null +++ b/data/stubs/l2/JNJ.json @@ -0,0 +1,128 @@ +{ + "ticker": "JNJ", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 462.58, + 899, + "NSDQ", + 23.9 + ], + [ + 462.47, + 188, + "NSDQ", + 7.4 + ], + [ + 462.35, + 1865, + "NSDQ", + 11.3 + ], + [ + 462.24, + 195, + "NSDQ", + 29.1 + ], + [ + 462.12, + 1712, + "NSDQ", + 28.3 + ], + [ + 462.01, + 265, + "NSDQ", + 3.6 + ], + [ + 461.89, + 1260, + "NSDQ", + 9.5 + ], + [ + 461.77, + 96, + "NSDQ", + 11.3 + ], + [ + 461.66, + 240, + "NSDQ", + 2.7 + ], + [ + 461.54, + 1238, + "NSDQ", + 7.5 + ] + ], + "asks": [ + [ + 462.82, + 801, + "NSDQ", + 10.2 + ], + [ + 462.93, + 38, + "NSDQ", + 13.6 + ], + [ + 463.05, + 66, + "NSDQ", + 2.4 + ], + [ + 463.16, + 275, + "NSDQ", + 22.2 + ], + [ + 463.28, + 403, + "NSDQ", + 11.2 + ], + [ + 463.39, + 97, + "NSDQ", + 13.5 + ], + [ + 463.51, + 189, + "NSDQ", + 5.8 + ], + [ + 463.63, + 181, + "NSDQ", + 8.5 + ], + [ + 463.74, + 415, + "NSDQ", + 28.5 + ], + [ + 463.86, + 189, + "NSDQ", + 16.3 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/JPM.json b/data/stubs/l2/JPM.json new file mode 100644 index 0000000000000000000000000000000000000000..5e83806214ce60cbf9df208094d81726bb9dbc63 --- /dev/null +++ b/data/stubs/l2/JPM.json @@ -0,0 +1,128 @@ +{ + "ticker": "JPM", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 148.45, + 2319, + "NSDQ", + 3.0 + ], + [ + 148.41, + 514, + "NSDQ", + 12.3 + ], + [ + 148.37, + 1311, + "NSDQ", + 8.0 + ], + [ + 148.34, + 1016, + "NSDQ", + 1.5 + ], + [ + 148.3, + 252, + "NSDQ", + 3.2 + ], + [ + 148.26, + 773, + "NSDQ", + 6.3 + ], + [ + 148.23, + 321, + "NSDQ", + 26.1 + ], + [ + 148.19, + 815, + "NSDQ", + 7.0 + ], + [ + 148.15, + 1574, + "NSDQ", + 17.2 + ], + [ + 148.11, + 3747, + "NSDQ", + 13.3 + ] + ], + "asks": [ + [ + 148.52, + 440, + "NSDQ", + 18.1 + ], + [ + 148.56, + 110, + "NSDQ", + 5.9 + ], + [ + 148.6, + 575, + "NSDQ", + 19.7 + ], + [ + 148.63, + 104, + "NSDQ", + 26.2 + ], + [ + 148.67, + 1436, + "NSDQ", + 19.8 + ], + [ + 148.71, + 310, + "NSDQ", + 8.9 + ], + [ + 148.75, + 78, + "NSDQ", + 28.1 + ], + [ + 148.78, + 331, + "NSDQ", + 27.4 + ], + [ + 148.82, + 85, + "NSDQ", + 25.8 + ], + [ + 148.86, + 127, + "NSDQ", + 10.2 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/MA.json b/data/stubs/l2/MA.json new file mode 100644 index 0000000000000000000000000000000000000000..6e0b8658550f6175166755361a2a00e66fef3226 --- /dev/null +++ b/data/stubs/l2/MA.json @@ -0,0 +1,128 @@ +{ + "ticker": "MA", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 238.21, + 754, + "NSDQ", + 17.8 + ], + [ + 238.15, + 214, + "NSDQ", + 27.3 + ], + [ + 238.09, + 128, + "NSDQ", + 20.0 + ], + [ + 238.03, + 726, + "NSDQ", + 4.4 + ], + [ + 237.97, + 391, + "NSDQ", + 29.7 + ], + [ + 237.91, + 336, + "NSDQ", + 23.0 + ], + [ + 237.85, + 416, + "NSDQ", + 19.6 + ], + [ + 237.79, + 527, + "NSDQ", + 21.4 + ], + [ + 237.73, + 344, + "NSDQ", + 21.8 + ], + [ + 237.67, + 474, + "NSDQ", + 25.0 + ] + ], + "asks": [ + [ + 238.33, + 422, + "NSDQ", + 9.9 + ], + [ + 238.39, + 102, + "NSDQ", + 23.9 + ], + [ + 238.45, + 296, + "NSDQ", + 24.1 + ], + [ + 238.51, + 531, + "NSDQ", + 5.2 + ], + [ + 238.57, + 1227, + "NSDQ", + 23.6 + ], + [ + 238.63, + 541, + "NSDQ", + 13.4 + ], + [ + 238.69, + 284, + "NSDQ", + 23.2 + ], + [ + 238.75, + 554, + "NSDQ", + 14.4 + ], + [ + 238.81, + 545, + "NSDQ", + 8.2 + ], + [ + 238.87, + 1368, + "NSDQ", + 4.9 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/META.json b/data/stubs/l2/META.json new file mode 100644 index 0000000000000000000000000000000000000000..96e50f049a1a045284bcd52c85f434d53909f809 --- /dev/null +++ b/data/stubs/l2/META.json @@ -0,0 +1,128 @@ +{ + "ticker": "META", + "ts": "2026-06-03T02:13:03.094736Z", + "bids": [ + [ + 158.66, + 248, + "NSDQ", + 3.9 + ], + [ + 158.62, + 97, + "NSDQ", + 13.7 + ], + [ + 158.58, + 1182, + "NSDQ", + 24.8 + ], + [ + 158.54, + 451, + "NSDQ", + 14.6 + ], + [ + 158.5, + 128, + "NSDQ", + 22.6 + ], + [ + 158.46, + 314, + "NSDQ", + 5.1 + ], + [ + 158.42, + 2466, + "NSDQ", + 21.9 + ], + [ + 158.38, + 231, + "NSDQ", + 4.5 + ], + [ + 158.34, + 412, + "NSDQ", + 6.0 + ], + [ + 158.3, + 540, + "NSDQ", + 11.1 + ] + ], + "asks": [ + [ + 158.73, + 802, + "NSDQ", + 8.4 + ], + [ + 158.77, + 119, + "NSDQ", + 16.3 + ], + [ + 158.81, + 232, + "NSDQ", + 20.0 + ], + [ + 158.85, + 234, + "NSDQ", + 16.1 + ], + [ + 158.89, + 300, + "NSDQ", + 28.2 + ], + [ + 158.93, + 116, + "NSDQ", + 29.9 + ], + [ + 158.97, + 138, + "NSDQ", + 29.0 + ], + [ + 159.01, + 852, + "NSDQ", + 21.2 + ], + [ + 159.05, + 98, + "NSDQ", + 18.1 + ], + [ + 159.09, + 246, + "NSDQ", + 28.4 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/MS.json b/data/stubs/l2/MS.json new file mode 100644 index 0000000000000000000000000000000000000000..30238a6e4ea3e8e968aa476e1961e2270b69d49d --- /dev/null +++ b/data/stubs/l2/MS.json @@ -0,0 +1,128 @@ +{ + "ticker": "MS", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 289.43, + 304, + "NSDQ", + 28.4 + ], + [ + 289.35, + 205, + "NSDQ", + 24.4 + ], + [ + 289.28, + 171, + "NSDQ", + 28.6 + ], + [ + 289.21, + 795, + "NSDQ", + 12.6 + ], + [ + 289.14, + 84, + "NSDQ", + 22.4 + ], + [ + 289.06, + 1025, + "NSDQ", + 12.9 + ], + [ + 288.99, + 503, + "NSDQ", + 11.9 + ], + [ + 288.92, + 47, + "NSDQ", + 24.5 + ], + [ + 288.85, + 29, + "NSDQ", + 27.0 + ], + [ + 288.78, + 85, + "NSDQ", + 27.8 + ] + ], + "asks": [ + [ + 289.57, + 3745, + "NSDQ", + 19.7 + ], + [ + 289.64, + 262, + "NSDQ", + 15.6 + ], + [ + 289.72, + 436, + "NSDQ", + 28.1 + ], + [ + 289.79, + 232, + "NSDQ", + 11.4 + ], + [ + 289.86, + 1248, + "NSDQ", + 23.5 + ], + [ + 289.93, + 686, + "NSDQ", + 7.9 + ], + [ + 290.01, + 476, + "NSDQ", + 29.7 + ], + [ + 290.08, + 1799, + "NSDQ", + 26.7 + ], + [ + 290.15, + 859, + "NSDQ", + 21.0 + ], + [ + 290.22, + 170, + "NSDQ", + 26.8 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/MSFT.json b/data/stubs/l2/MSFT.json new file mode 100644 index 0000000000000000000000000000000000000000..708e3a5fca7698eb22465b8a4b21b1bddeea4f29 --- /dev/null +++ b/data/stubs/l2/MSFT.json @@ -0,0 +1,128 @@ +{ + "ticker": "MSFT", + "ts": "2026-06-03T02:13:03.094736Z", + "bids": [ + [ + 291.68, + 199, + "NSDQ", + 6.0 + ], + [ + 291.6, + 173, + "NSDQ", + 14.7 + ], + [ + 291.53, + 484, + "NSDQ", + 23.5 + ], + [ + 291.46, + 100, + "NSDQ", + 7.9 + ], + [ + 291.38, + 91, + "NSDQ", + 8.8 + ], + [ + 291.31, + 217, + "NSDQ", + 6.5 + ], + [ + 291.24, + 485, + "NSDQ", + 19.9 + ], + [ + 291.17, + 340, + "NSDQ", + 11.7 + ], + [ + 291.09, + 253, + "NSDQ", + 17.7 + ], + [ + 291.02, + 621, + "NSDQ", + 21.2 + ] + ], + "asks": [ + [ + 291.82, + 210, + "NSDQ", + 20.0 + ], + [ + 291.9, + 283, + "NSDQ", + 8.7 + ], + [ + 291.97, + 4158, + "NSDQ", + 22.8 + ], + [ + 292.04, + 94, + "NSDQ", + 24.8 + ], + [ + 292.11, + 223, + "NSDQ", + 21.3 + ], + [ + 292.19, + 837, + "NSDQ", + 11.2 + ], + [ + 292.26, + 2552, + "NSDQ", + 9.9 + ], + [ + 292.33, + 389, + "NSDQ", + 14.4 + ], + [ + 292.41, + 898, + "NSDQ", + 10.6 + ], + [ + 292.48, + 398, + "NSDQ", + 2.6 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/NFLX.json b/data/stubs/l2/NFLX.json new file mode 100644 index 0000000000000000000000000000000000000000..9eccfdacf97fe24a17ba3c02a6a868e2fdd8f1a9 --- /dev/null +++ b/data/stubs/l2/NFLX.json @@ -0,0 +1,128 @@ +{ + "ticker": "NFLX", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 131.05, + 241, + "NSDQ", + 9.0 + ], + [ + 131.02, + 1084, + "NSDQ", + 22.0 + ], + [ + 130.99, + 3108, + "NSDQ", + 10.3 + ], + [ + 130.95, + 867, + "NSDQ", + 17.3 + ], + [ + 130.92, + 860, + "NSDQ", + 12.4 + ], + [ + 130.89, + 640, + "NSDQ", + 1.6 + ], + [ + 130.86, + 496, + "NSDQ", + 26.6 + ], + [ + 130.82, + 198, + "NSDQ", + 14.1 + ], + [ + 130.79, + 381, + "NSDQ", + 23.8 + ], + [ + 130.76, + 2425, + "NSDQ", + 14.8 + ] + ], + "asks": [ + [ + 131.12, + 332, + "NSDQ", + 19.7 + ], + [ + 131.15, + 362, + "NSDQ", + 29.3 + ], + [ + 131.18, + 120, + "NSDQ", + 14.8 + ], + [ + 131.22, + 1578, + "NSDQ", + 25.6 + ], + [ + 131.25, + 462, + "NSDQ", + 9.3 + ], + [ + 131.28, + 864, + "NSDQ", + 18.1 + ], + [ + 131.32, + 890, + "NSDQ", + 10.5 + ], + [ + 131.35, + 8908, + "NSDQ", + 7.9 + ], + [ + 131.38, + 244, + "NSDQ", + 3.5 + ], + [ + 131.41, + 870, + "NSDQ", + 9.7 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/NVDA.json b/data/stubs/l2/NVDA.json new file mode 100644 index 0000000000000000000000000000000000000000..f312314bcf2d662d70cd2ef4933dd811c56aa8b2 --- /dev/null +++ b/data/stubs/l2/NVDA.json @@ -0,0 +1,128 @@ +{ + "ticker": "NVDA", + "ts": "2026-06-03T02:13:03.094736Z", + "bids": [ + [ + 261.72, + 542, + "NSDQ", + 8.1 + ], + [ + 261.65, + 2089, + "NSDQ", + 18.8 + ], + [ + 261.58, + 176, + "NSDQ", + 17.2 + ], + [ + 261.52, + 49, + "NSDQ", + 21.6 + ], + [ + 261.45, + 79, + "NSDQ", + 11.8 + ], + [ + 261.39, + 660, + "NSDQ", + 9.7 + ], + [ + 261.32, + 72, + "NSDQ", + 17.6 + ], + [ + 261.26, + 601, + "NSDQ", + 8.9 + ], + [ + 261.19, + 982, + "NSDQ", + 22.1 + ], + [ + 261.13, + 168, + "NSDQ", + 15.7 + ] + ], + "asks": [ + [ + 261.85, + 83, + "NSDQ", + 16.4 + ], + [ + 261.91, + 684, + "NSDQ", + 9.5 + ], + [ + 261.98, + 791, + "NSDQ", + 21.4 + ], + [ + 262.04, + 141, + "NSDQ", + 22.2 + ], + [ + 262.11, + 257, + "NSDQ", + 14.7 + ], + [ + 262.17, + 262, + "NSDQ", + 12.6 + ], + [ + 262.24, + 548, + "NSDQ", + 13.2 + ], + [ + 262.3, + 7799, + "NSDQ", + 8.8 + ], + [ + 262.37, + 2090, + "NSDQ", + 15.5 + ], + [ + 262.44, + 623, + "NSDQ", + 11.7 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/ORCL.json b/data/stubs/l2/ORCL.json new file mode 100644 index 0000000000000000000000000000000000000000..ce46fc276fe7ea8073c5c67a344b2bb222935e72 --- /dev/null +++ b/data/stubs/l2/ORCL.json @@ -0,0 +1,128 @@ +{ + "ticker": "ORCL", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 312.71, + 120, + "NSDQ", + 2.7 + ], + [ + 312.64, + 683, + "NSDQ", + 1.9 + ], + [ + 312.56, + 193, + "NSDQ", + 22.8 + ], + [ + 312.48, + 193, + "NSDQ", + 9.0 + ], + [ + 312.4, + 324, + "NSDQ", + 29.6 + ], + [ + 312.32, + 265, + "NSDQ", + 11.1 + ], + [ + 312.25, + 1634, + "NSDQ", + 10.1 + ], + [ + 312.17, + 949, + "NSDQ", + 13.6 + ], + [ + 312.09, + 110, + "NSDQ", + 22.6 + ], + [ + 312.01, + 495, + "NSDQ", + 1.8 + ] + ], + "asks": [ + [ + 312.87, + 168, + "NSDQ", + 22.4 + ], + [ + 312.95, + 2402, + "NSDQ", + 19.2 + ], + [ + 313.03, + 666, + "NSDQ", + 17.3 + ], + [ + 313.11, + 1432, + "NSDQ", + 25.7 + ], + [ + 313.18, + 919, + "NSDQ", + 10.2 + ], + [ + 313.26, + 5284, + "NSDQ", + 24.4 + ], + [ + 313.34, + 2077, + "NSDQ", + 3.5 + ], + [ + 313.42, + 120, + "NSDQ", + 12.2 + ], + [ + 313.5, + 1084, + "NSDQ", + 8.5 + ], + [ + 313.58, + 1309, + "NSDQ", + 23.9 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/PFE.json b/data/stubs/l2/PFE.json new file mode 100644 index 0000000000000000000000000000000000000000..83b381c1403300410a43fc38895fa41db6954552 --- /dev/null +++ b/data/stubs/l2/PFE.json @@ -0,0 +1,128 @@ +{ + "ticker": "PFE", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 273.02, + 109, + "NSDQ", + 12.7 + ], + [ + 272.95, + 1151, + "NSDQ", + 2.3 + ], + [ + 272.88, + 1248, + "NSDQ", + 21.4 + ], + [ + 272.81, + 100, + "NSDQ", + 5.6 + ], + [ + 272.75, + 166, + "NSDQ", + 19.9 + ], + [ + 272.68, + 309, + "NSDQ", + 9.8 + ], + [ + 272.61, + 820, + "NSDQ", + 27.6 + ], + [ + 272.54, + 82, + "NSDQ", + 8.1 + ], + [ + 272.47, + 119, + "NSDQ", + 3.8 + ], + [ + 272.4, + 131, + "NSDQ", + 8.9 + ] + ], + "asks": [ + [ + 273.16, + 5301, + "NSDQ", + 14.3 + ], + [ + 273.22, + 783, + "NSDQ", + 7.7 + ], + [ + 273.29, + 1777, + "NSDQ", + 7.8 + ], + [ + 273.36, + 726, + "NSDQ", + 3.0 + ], + [ + 273.43, + 105, + "NSDQ", + 24.6 + ], + [ + 273.5, + 159, + "NSDQ", + 18.1 + ], + [ + 273.57, + 511, + "NSDQ", + 6.7 + ], + [ + 273.63, + 1482, + "NSDQ", + 24.2 + ], + [ + 273.7, + 778, + "NSDQ", + 4.4 + ], + [ + 273.77, + 462, + "NSDQ", + 28.2 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/PLTR.json b/data/stubs/l2/PLTR.json new file mode 100644 index 0000000000000000000000000000000000000000..d0b953ec2a7a2b3e1ee289a0bedf76df46c44bd4 --- /dev/null +++ b/data/stubs/l2/PLTR.json @@ -0,0 +1,128 @@ +{ + "ticker": "PLTR", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 452.2, + 431, + "NSDQ", + 22.5 + ], + [ + 452.08, + 621, + "NSDQ", + 3.0 + ], + [ + 451.97, + 1195, + "NSDQ", + 9.0 + ], + [ + 451.86, + 789, + "NSDQ", + 3.3 + ], + [ + 451.75, + 290, + "NSDQ", + 8.9 + ], + [ + 451.63, + 200, + "NSDQ", + 23.4 + ], + [ + 451.52, + 365, + "NSDQ", + 27.1 + ], + [ + 451.41, + 270, + "NSDQ", + 2.8 + ], + [ + 451.29, + 135, + "NSDQ", + 28.2 + ], + [ + 451.18, + 414, + "NSDQ", + 8.6 + ] + ], + "asks": [ + [ + 452.42, + 790, + "NSDQ", + 4.3 + ], + [ + 452.54, + 102, + "NSDQ", + 14.5 + ], + [ + 452.65, + 3151, + "NSDQ", + 24.4 + ], + [ + 452.76, + 258, + "NSDQ", + 29.9 + ], + [ + 452.88, + 297, + "NSDQ", + 18.0 + ], + [ + 452.99, + 400, + "NSDQ", + 20.7 + ], + [ + 453.1, + 170, + "NSDQ", + 26.2 + ], + [ + 453.22, + 212, + "NSDQ", + 18.2 + ], + [ + 453.33, + 207, + "NSDQ", + 22.6 + ], + [ + 453.44, + 391, + "NSDQ", + 4.7 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/QCOM.json b/data/stubs/l2/QCOM.json new file mode 100644 index 0000000000000000000000000000000000000000..926561209ce7bec5999a6a52f65b34821b37fd7f --- /dev/null +++ b/data/stubs/l2/QCOM.json @@ -0,0 +1,128 @@ +{ + "ticker": "QCOM", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 273.06, + 292, + "NSDQ", + 16.1 + ], + [ + 272.99, + 197, + "NSDQ", + 23.8 + ], + [ + 272.92, + 88, + "NSDQ", + 15.9 + ], + [ + 272.85, + 102, + "NSDQ", + 24.6 + ], + [ + 272.79, + 85, + "NSDQ", + 15.5 + ], + [ + 272.72, + 412, + "NSDQ", + 15.8 + ], + [ + 272.65, + 80, + "NSDQ", + 3.3 + ], + [ + 272.58, + 79, + "NSDQ", + 7.1 + ], + [ + 272.51, + 196, + "NSDQ", + 23.4 + ], + [ + 272.44, + 127, + "NSDQ", + 22.3 + ] + ], + "asks": [ + [ + 273.2, + 1875, + "NSDQ", + 12.4 + ], + [ + 273.26, + 1220, + "NSDQ", + 12.2 + ], + [ + 273.33, + 553, + "NSDQ", + 11.9 + ], + [ + 273.4, + 679, + "NSDQ", + 10.2 + ], + [ + 273.47, + 1655, + "NSDQ", + 3.1 + ], + [ + 273.54, + 158, + "NSDQ", + 1.6 + ], + [ + 273.61, + 1307, + "NSDQ", + 6.8 + ], + [ + 273.67, + 601, + "NSDQ", + 25.2 + ], + [ + 273.74, + 3727, + "NSDQ", + 6.2 + ], + [ + 273.81, + 378, + "NSDQ", + 13.2 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/RIOT.json b/data/stubs/l2/RIOT.json new file mode 100644 index 0000000000000000000000000000000000000000..abe4709912cca5e9c43e8fd08123ad03b4de0ad8 --- /dev/null +++ b/data/stubs/l2/RIOT.json @@ -0,0 +1,128 @@ +{ + "ticker": "RIOT", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 254.58, + 283, + "NSDQ", + 4.3 + ], + [ + 254.52, + 12, + "NSDQ", + 19.3 + ], + [ + 254.46, + 69, + "NSDQ", + 20.6 + ], + [ + 254.39, + 736, + "NSDQ", + 12.7 + ], + [ + 254.33, + 215, + "NSDQ", + 23.0 + ], + [ + 254.26, + 554, + "NSDQ", + 3.9 + ], + [ + 254.2, + 39, + "NSDQ", + 18.1 + ], + [ + 254.14, + 174, + "NSDQ", + 3.6 + ], + [ + 254.07, + 178, + "NSDQ", + 14.5 + ], + [ + 254.01, + 466, + "NSDQ", + 14.3 + ] + ], + "asks": [ + [ + 254.71, + 1528, + "NSDQ", + 24.3 + ], + [ + 254.77, + 350, + "NSDQ", + 6.6 + ], + [ + 254.84, + 784, + "NSDQ", + 23.2 + ], + [ + 254.9, + 3332, + "NSDQ", + 10.6 + ], + [ + 254.96, + 1978, + "NSDQ", + 17.3 + ], + [ + 255.03, + 379, + "NSDQ", + 2.1 + ], + [ + 255.09, + 651, + "NSDQ", + 10.3 + ], + [ + 255.16, + 346, + "NSDQ", + 21.7 + ], + [ + 255.22, + 3931, + "NSDQ", + 13.7 + ], + [ + 255.28, + 474, + "NSDQ", + 25.1 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/TSLA.json b/data/stubs/l2/TSLA.json new file mode 100644 index 0000000000000000000000000000000000000000..776c61fa1a0f3ef44b3fcdd232805b68140ef645 --- /dev/null +++ b/data/stubs/l2/TSLA.json @@ -0,0 +1,128 @@ +{ + "ticker": "TSLA", + "ts": "2026-06-03T02:13:03.094736Z", + "bids": [ + [ + 254.58, + 520, + "NSDQ", + 26.9 + ], + [ + 254.51, + 1578, + "NSDQ", + 10.6 + ], + [ + 254.45, + 185, + "NSDQ", + 19.0 + ], + [ + 254.39, + 275, + "NSDQ", + 17.4 + ], + [ + 254.32, + 454, + "NSDQ", + 2.0 + ], + [ + 254.26, + 294, + "NSDQ", + 9.8 + ], + [ + 254.19, + 407, + "NSDQ", + 13.6 + ], + [ + 254.13, + 1015, + "NSDQ", + 15.1 + ], + [ + 254.07, + 331, + "NSDQ", + 14.0 + ], + [ + 254.0, + 453, + "NSDQ", + 5.7 + ] + ], + "asks": [ + [ + 254.7, + 46, + "NSDQ", + 5.4 + ], + [ + 254.77, + 232, + "NSDQ", + 28.4 + ], + [ + 254.83, + 124, + "NSDQ", + 12.2 + ], + [ + 254.89, + 121, + "NSDQ", + 8.1 + ], + [ + 254.96, + 93, + "NSDQ", + 2.2 + ], + [ + 255.02, + 673, + "NSDQ", + 16.9 + ], + [ + 255.09, + 321, + "NSDQ", + 21.7 + ], + [ + 255.15, + 243, + "NSDQ", + 21.1 + ], + [ + 255.21, + 240, + "NSDQ", + 28.5 + ], + [ + 255.28, + 254, + "NSDQ", + 11.9 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/UNH.json b/data/stubs/l2/UNH.json new file mode 100644 index 0000000000000000000000000000000000000000..f3b1e8f1ecd750f8875ce3865aecb7d340569b74 --- /dev/null +++ b/data/stubs/l2/UNH.json @@ -0,0 +1,128 @@ +{ + "ticker": "UNH", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 237.85, + 870, + "NSDQ", + 16.8 + ], + [ + 237.79, + 2441, + "NSDQ", + 17.4 + ], + [ + 237.73, + 831, + "NSDQ", + 26.8 + ], + [ + 237.67, + 1571, + "NSDQ", + 8.7 + ], + [ + 237.61, + 516, + "NSDQ", + 20.5 + ], + [ + 237.55, + 193, + "NSDQ", + 29.5 + ], + [ + 237.5, + 227, + "NSDQ", + 14.1 + ], + [ + 237.44, + 317, + "NSDQ", + 26.6 + ], + [ + 237.38, + 1974, + "NSDQ", + 11.3 + ], + [ + 237.32, + 125, + "NSDQ", + 11.1 + ] + ], + "asks": [ + [ + 237.97, + 516, + "NSDQ", + 21.2 + ], + [ + 238.03, + 77, + "NSDQ", + 13.0 + ], + [ + 238.09, + 6107, + "NSDQ", + 4.0 + ], + [ + 238.15, + 693, + "NSDQ", + 10.0 + ], + [ + 238.21, + 130, + "NSDQ", + 11.0 + ], + [ + 238.27, + 214, + "NSDQ", + 15.8 + ], + [ + 238.33, + 276, + "NSDQ", + 3.0 + ], + [ + 238.39, + 644, + "NSDQ", + 11.7 + ], + [ + 238.45, + 668, + "NSDQ", + 11.5 + ], + [ + 238.51, + 470, + "NSDQ", + 19.3 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/V.json b/data/stubs/l2/V.json new file mode 100644 index 0000000000000000000000000000000000000000..315ac007d59a4840b3bd23d691cea3324725ef5d --- /dev/null +++ b/data/stubs/l2/V.json @@ -0,0 +1,128 @@ +{ + "ticker": "V", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 266.45, + 454, + "NSDQ", + 16.5 + ], + [ + 266.39, + 77, + "NSDQ", + 22.5 + ], + [ + 266.32, + 387, + "NSDQ", + 2.2 + ], + [ + 266.25, + 130, + "NSDQ", + 12.7 + ], + [ + 266.19, + 629, + "NSDQ", + 17.6 + ], + [ + 266.12, + 269, + "NSDQ", + 8.9 + ], + [ + 266.05, + 2157, + "NSDQ", + 22.0 + ], + [ + 265.99, + 2875, + "NSDQ", + 19.6 + ], + [ + 265.92, + 825, + "NSDQ", + 3.8 + ], + [ + 265.85, + 2215, + "NSDQ", + 28.5 + ] + ], + "asks": [ + [ + 266.59, + 104, + "NSDQ", + 27.0 + ], + [ + 266.65, + 21, + "NSDQ", + 13.2 + ], + [ + 266.72, + 365, + "NSDQ", + 15.1 + ], + [ + 266.79, + 314, + "NSDQ", + 22.1 + ], + [ + 266.85, + 865, + "NSDQ", + 9.6 + ], + [ + 266.92, + 333, + "NSDQ", + 18.5 + ], + [ + 266.99, + 848, + "NSDQ", + 20.8 + ], + [ + 267.05, + 2427, + "NSDQ", + 15.7 + ], + [ + 267.12, + 925, + "NSDQ", + 23.3 + ], + [ + 267.19, + 62, + "NSDQ", + 22.9 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/l2/XOM.json b/data/stubs/l2/XOM.json new file mode 100644 index 0000000000000000000000000000000000000000..f18031c26777f3741cf47b3a121225c1a1b1fa3c --- /dev/null +++ b/data/stubs/l2/XOM.json @@ -0,0 +1,128 @@ +{ + "ticker": "XOM", + "ts": "2026-06-03T02:13:03.098593Z", + "bids": [ + [ + 112.94, + 667, + "NSDQ", + 24.6 + ], + [ + 112.91, + 559, + "NSDQ", + 16.4 + ], + [ + 112.89, + 171, + "NSDQ", + 20.3 + ], + [ + 112.86, + 553, + "NSDQ", + 2.1 + ], + [ + 112.83, + 616, + "NSDQ", + 14.8 + ], + [ + 112.8, + 2017, + "NSDQ", + 27.4 + ], + [ + 112.77, + 258, + "NSDQ", + 6.8 + ], + [ + 112.74, + 477, + "NSDQ", + 6.5 + ], + [ + 112.72, + 375, + "NSDQ", + 2.4 + ], + [ + 112.69, + 602, + "NSDQ", + 16.9 + ] + ], + "asks": [ + [ + 113.0, + 961, + "NSDQ", + 11.2 + ], + [ + 113.03, + 883, + "NSDQ", + 14.7 + ], + [ + 113.05, + 184, + "NSDQ", + 7.8 + ], + [ + 113.08, + 192, + "NSDQ", + 6.9 + ], + [ + 113.11, + 67, + "NSDQ", + 11.7 + ], + [ + 113.14, + 101, + "NSDQ", + 2.5 + ], + [ + 113.17, + 103, + "NSDQ", + 10.3 + ], + [ + 113.2, + 429, + "NSDQ", + 6.4 + ], + [ + 113.22, + 202, + "NSDQ", + 7.8 + ], + [ + 113.25, + 2308, + "NSDQ", + 3.2 + ] + ] +} \ No newline at end of file diff --git a/data/stubs/options/AAPL.parquet b/data/stubs/options/AAPL.parquet new file mode 100644 index 0000000000000000000000000000000000000000..71026d059471ea44ec94a735dedd507e2a7bfa4f --- /dev/null +++ b/data/stubs/options/AAPL.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e44c0944f757a0213532bd8d122b32d639422d0b695061a3b5bc98e7b6efecc +size 6410 diff --git a/data/stubs/options/ADBE.parquet b/data/stubs/options/ADBE.parquet new file mode 100644 index 0000000000000000000000000000000000000000..24f83e62ef21cbe8fd18c09d9fbabfda7709d960 --- /dev/null +++ b/data/stubs/options/ADBE.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb2a03f14b878a373e105fddd68043bbf4921ae461c5292997fa8d52eadac34 +size 6406 diff --git a/data/stubs/options/AMC.parquet b/data/stubs/options/AMC.parquet new file mode 100644 index 0000000000000000000000000000000000000000..27db3ef032698e571a11a9b839ce9daa096f0a55 --- /dev/null +++ b/data/stubs/options/AMC.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5876aaac7e632a1115f034f3197f1badc5307d007c46802b45ba22134936420 +size 6419 diff --git a/data/stubs/options/AMD.parquet b/data/stubs/options/AMD.parquet new file mode 100644 index 0000000000000000000000000000000000000000..1fcab375e6157acb149ac5ed3dc318e241001cb4 --- /dev/null +++ b/data/stubs/options/AMD.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c4076c8aaa4c336a08146ceaeab48cdd9ac757d554ee5c313a9bf03f09dca8c +size 6407 diff --git a/data/stubs/options/AMZN.parquet b/data/stubs/options/AMZN.parquet new file mode 100644 index 0000000000000000000000000000000000000000..acdda96f33d46a303236b429b1caad379e593779 --- /dev/null +++ b/data/stubs/options/AMZN.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f87e0e4de22e71010d839f2c342a86766a93119a2088311f2776c73cfd99f66 +size 6424 diff --git a/data/stubs/options/AVGO.parquet b/data/stubs/options/AVGO.parquet new file mode 100644 index 0000000000000000000000000000000000000000..6c7fdf62a13296a963fbf45c4b19fa08d6a96ad4 --- /dev/null +++ b/data/stubs/options/AVGO.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16878f41c6e825f2e97198a6d18e3c6527274ba203b94d29f113f46427886ee4 +size 6429 diff --git a/data/stubs/options/BAC.parquet b/data/stubs/options/BAC.parquet new file mode 100644 index 0000000000000000000000000000000000000000..8ec4c71800148706117183194d7d0dee7785848c --- /dev/null +++ b/data/stubs/options/BAC.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4735270353f13b92c87b22df664e9f9a5f8a7f9aca1609170ed936242a77c8d1 +size 6423 diff --git a/data/stubs/options/CRM.parquet b/data/stubs/options/CRM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..ebd70343412722d34e8d590e8bf829de38a38966 --- /dev/null +++ b/data/stubs/options/CRM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:915677e9d2c08f3d41ab40ed1365c25e0aecb6c467dcb7aa67d2a0d023a54619 +size 6429 diff --git a/data/stubs/options/CVX.parquet b/data/stubs/options/CVX.parquet new file mode 100644 index 0000000000000000000000000000000000000000..b039bd843636c1501e674123e5e362274fb0b46b --- /dev/null +++ b/data/stubs/options/CVX.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0948fc7402bd720736da492d399b2e571f9266a26b73727cb263a6024e9df63a +size 6413 diff --git a/data/stubs/options/GME.parquet b/data/stubs/options/GME.parquet new file mode 100644 index 0000000000000000000000000000000000000000..b84760342d1cbd54c68bb5bea43ee53856b378de --- /dev/null +++ b/data/stubs/options/GME.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80e07e1cf47da2ffa5ec76ddab26f78e72d8be3970de3e8ea2a3f7b68af035ef +size 6396 diff --git a/data/stubs/options/GOOGL.parquet b/data/stubs/options/GOOGL.parquet new file mode 100644 index 0000000000000000000000000000000000000000..38e5ff72d1735069c0febded77b07cf67d1bc133 --- /dev/null +++ b/data/stubs/options/GOOGL.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec395199fefceed5ddd04bcb0a302e6dc72c2c52e6ddb0c769336c1375e64b16 +size 6432 diff --git a/data/stubs/options/GS.parquet b/data/stubs/options/GS.parquet new file mode 100644 index 0000000000000000000000000000000000000000..9f7289ee4311fb7f22567bb50ec0e2d32f496da1 --- /dev/null +++ b/data/stubs/options/GS.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b7c9e42eafe4f84e1ecc2957d9a51119b3fb7a3e31d502734ad7b5a694f29e2 +size 6424 diff --git a/data/stubs/options/INTC.parquet b/data/stubs/options/INTC.parquet new file mode 100644 index 0000000000000000000000000000000000000000..28ee9b18f48a8a690511850d8a7a7cb1fc317702 --- /dev/null +++ b/data/stubs/options/INTC.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73ad49c4bcc361635b91a4c73d6a15a21f3e4191e9317c610d5c0ec058dee27d +size 6411 diff --git a/data/stubs/options/JNJ.parquet b/data/stubs/options/JNJ.parquet new file mode 100644 index 0000000000000000000000000000000000000000..7b7df3e7adfc2b79f1b6e2e58efdd64b64bb5416 --- /dev/null +++ b/data/stubs/options/JNJ.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c46fddae6f6bf070a52147fb7648fbd500cac3bf4d10f2a7f17c5fa57c80885 +size 6412 diff --git a/data/stubs/options/JPM.parquet b/data/stubs/options/JPM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..751c52791a17958df52ba148af3f3d25015fd11c --- /dev/null +++ b/data/stubs/options/JPM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e4475579e423c963e7ae721b20440856654f1c67cacaaa851a0924d045bdea6 +size 6410 diff --git a/data/stubs/options/MA.parquet b/data/stubs/options/MA.parquet new file mode 100644 index 0000000000000000000000000000000000000000..e0b3bfa6bee0daf94e83b25f4b55a205c072c052 --- /dev/null +++ b/data/stubs/options/MA.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5273d5a5499dceb326e5becf66857d42ad1b3eaa8321c0569f46187df12b6d2a +size 6400 diff --git a/data/stubs/options/META.parquet b/data/stubs/options/META.parquet new file mode 100644 index 0000000000000000000000000000000000000000..3f64ccb7281fbb247a8fda5b247596e2118ebe30 --- /dev/null +++ b/data/stubs/options/META.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c52a42487cbe201cba8a5670da0c84d51417f9feae79d2cd8fd27c4ae489c567 +size 6409 diff --git a/data/stubs/options/MS.parquet b/data/stubs/options/MS.parquet new file mode 100644 index 0000000000000000000000000000000000000000..0b3d36ffd88570c36cb78b22b542a33b28ee7f7d --- /dev/null +++ b/data/stubs/options/MS.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:415c8090904aad8e45eb1552c35b7de4ee788c7a8149a61290b66478de8cce2f +size 6417 diff --git a/data/stubs/options/MSFT.parquet b/data/stubs/options/MSFT.parquet new file mode 100644 index 0000000000000000000000000000000000000000..f6e0873a0f3198e1e900d0771a7c7cd87176aaf5 --- /dev/null +++ b/data/stubs/options/MSFT.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19b8e5950138a138bcb93b3272d32cd3668e62bc7a57328b9602ff996fdc2924 +size 6414 diff --git a/data/stubs/options/NFLX.parquet b/data/stubs/options/NFLX.parquet new file mode 100644 index 0000000000000000000000000000000000000000..a0192b822d7fc2df256a63bdf121f8a1a42fa673 --- /dev/null +++ b/data/stubs/options/NFLX.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba7e4aebe5e65cb407608c6b01164a567f9e274193941cb45ae051e6ec1c5dc8 +size 6407 diff --git a/data/stubs/options/NVDA.parquet b/data/stubs/options/NVDA.parquet new file mode 100644 index 0000000000000000000000000000000000000000..59230a140563cba00ea3e57dd3d09b23cb154275 --- /dev/null +++ b/data/stubs/options/NVDA.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01b57f906a794acb93705acaf90928e4c01d372ecd722216865d6e5db7099e03 +size 6408 diff --git a/data/stubs/options/ORCL.parquet b/data/stubs/options/ORCL.parquet new file mode 100644 index 0000000000000000000000000000000000000000..48e8f3c93f888f9ac950faac8ca52e1ab7c7aaf3 --- /dev/null +++ b/data/stubs/options/ORCL.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:306822434d3c27817b2fdcd6b6f18bb80d010a6627d7a9e32dd05e908ad95627 +size 6394 diff --git a/data/stubs/options/PFE.parquet b/data/stubs/options/PFE.parquet new file mode 100644 index 0000000000000000000000000000000000000000..84790dd8beb61068528290e33b979e2f22ae034d --- /dev/null +++ b/data/stubs/options/PFE.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:478dc4a310836a04cdb4698a555e3f6da6ec9f62d40a7caff64db4addb4330f5 +size 6421 diff --git a/data/stubs/options/PLTR.parquet b/data/stubs/options/PLTR.parquet new file mode 100644 index 0000000000000000000000000000000000000000..f1e911f946e932ba1a99b165c9c5df7cb7c4f78e --- /dev/null +++ b/data/stubs/options/PLTR.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d02a0dfc8fd4a7203ed2c2d17b4cf7aed6c8005a5af5b76f6cc2ad12d37df75 +size 6410 diff --git a/data/stubs/options/QCOM.parquet b/data/stubs/options/QCOM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..99ac39cfc258373e1051607fce1c40ff75d8539d --- /dev/null +++ b/data/stubs/options/QCOM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0430ceb7b99669ce8a092e6a0adaec14ed0cc23e5a471854269a978dbfdb0fd8 +size 6412 diff --git a/data/stubs/options/RIOT.parquet b/data/stubs/options/RIOT.parquet new file mode 100644 index 0000000000000000000000000000000000000000..354dd0b62f2b000e741b86e8c06105afd12f5ed9 --- /dev/null +++ b/data/stubs/options/RIOT.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01291a30ee92c9605542732047e9fa0c4152ecd253fcd0634617ec7ccb140f3c +size 6405 diff --git a/data/stubs/options/TSLA.parquet b/data/stubs/options/TSLA.parquet new file mode 100644 index 0000000000000000000000000000000000000000..a03418c209a1c2e440086fdf010895a342fc1d65 --- /dev/null +++ b/data/stubs/options/TSLA.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcf04ffed9c9a742bb96c5d02c974dc6b683367463e6dbbb0e8317ea81fa271d +size 6412 diff --git a/data/stubs/options/UNH.parquet b/data/stubs/options/UNH.parquet new file mode 100644 index 0000000000000000000000000000000000000000..3bb9feb822d687d11d8d8042893b3c407c0ee9f8 --- /dev/null +++ b/data/stubs/options/UNH.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf0aa2b132818dbcb0f70c8d9e76f5a645299af23c3dc8a1623b670fa4ad68b9 +size 6413 diff --git a/data/stubs/options/V.parquet b/data/stubs/options/V.parquet new file mode 100644 index 0000000000000000000000000000000000000000..677d6eb407597e7871a86881b1d34c32e7662b45 --- /dev/null +++ b/data/stubs/options/V.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fea9f200b10d7b98ef7121a9669db4afc8296ce6d24c39311017ac0d8e3a6926 +size 6418 diff --git a/data/stubs/options/XOM.parquet b/data/stubs/options/XOM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..5a644ce37895bf54fa919e618eee940c3ec3621c --- /dev/null +++ b/data/stubs/options/XOM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b31f9e53f81f6924818c1368fcc8609131e755667e2e1ad6a9c3c7d16bca7a0 +size 6412 diff --git a/data/stubs/ticks/AAPL.parquet b/data/stubs/ticks/AAPL.parquet new file mode 100644 index 0000000000000000000000000000000000000000..33dd72338671292d0e6536a343dd91ba0cae80d5 --- /dev/null +++ b/data/stubs/ticks/AAPL.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fbb67e42c4a9c80dc223aba9fdd2d730e912db5114becb7d7b1bf018bc9a0e2 +size 35102 diff --git a/data/stubs/ticks/ADBE.parquet b/data/stubs/ticks/ADBE.parquet new file mode 100644 index 0000000000000000000000000000000000000000..8c91f9d498526cd2eeb42fe62b5399ab4d5a930e --- /dev/null +++ b/data/stubs/ticks/ADBE.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbf783e2ef558c3d5059f4b1ba212b7ed800c7803ef952f27e27b18d44f5df30 +size 50640 diff --git a/data/stubs/ticks/AMC.parquet b/data/stubs/ticks/AMC.parquet new file mode 100644 index 0000000000000000000000000000000000000000..d3b0789a874727a3a73bbd4ace2d6f5f8ecc9572 --- /dev/null +++ b/data/stubs/ticks/AMC.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:916c363821e73fc28c6ad38ba11dbaa1598dc5b30e6c379992b7b8e656f81810 +size 44227 diff --git a/data/stubs/ticks/AMD.parquet b/data/stubs/ticks/AMD.parquet new file mode 100644 index 0000000000000000000000000000000000000000..898a869a8b7d4284f4e37acc969b7a698cca49d1 --- /dev/null +++ b/data/stubs/ticks/AMD.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1095eda0af187e58b92a1014245ec153078dbe2bc6b04ee5cb1439fc7506eda +size 41494 diff --git a/data/stubs/ticks/AMZN.parquet b/data/stubs/ticks/AMZN.parquet new file mode 100644 index 0000000000000000000000000000000000000000..31e36888f29b9de85df39ade5e90792adaed62ba --- /dev/null +++ b/data/stubs/ticks/AMZN.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d12b5fd8b15830d0d80e3bcc8471bd2571f2d732cd0a33dae742f216de3af08 +size 49857 diff --git a/data/stubs/ticks/AVGO.parquet b/data/stubs/ticks/AVGO.parquet new file mode 100644 index 0000000000000000000000000000000000000000..b0db39537f5a34fbe431c1f8b505ae893ca72552 --- /dev/null +++ b/data/stubs/ticks/AVGO.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b83ff49f91c9a7661b708b969c5ec5d25e8930fbf4108f776a3dca10495b1eb +size 32693 diff --git a/data/stubs/ticks/BAC.parquet b/data/stubs/ticks/BAC.parquet new file mode 100644 index 0000000000000000000000000000000000000000..801fdc9bf598e17f386cee48cc3d6bebe5729049 --- /dev/null +++ b/data/stubs/ticks/BAC.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd5d104f23a57ef74ef438fd87635d0dd2705df7d8ed855f072e0188ae97211e +size 40025 diff --git a/data/stubs/ticks/CRM.parquet b/data/stubs/ticks/CRM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..c029360767cdacb677b44a00486f5418e1b32d8a --- /dev/null +++ b/data/stubs/ticks/CRM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:055ff87505be47742f2b362bed007a3fce68d661819fd5f1852430087b425cba +size 33866 diff --git a/data/stubs/ticks/CVX.parquet b/data/stubs/ticks/CVX.parquet new file mode 100644 index 0000000000000000000000000000000000000000..1b60385490d85d47d39ac5e860e26c6ac25b9ce1 --- /dev/null +++ b/data/stubs/ticks/CVX.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3361e1b1ae902b6e4054761c3e4e4bd1ccb508b6410cdca20b582269a8467c7c +size 30862 diff --git a/data/stubs/ticks/GME.parquet b/data/stubs/ticks/GME.parquet new file mode 100644 index 0000000000000000000000000000000000000000..277d01026bd3e812e7d770e4a0afc04f1f4fd94a --- /dev/null +++ b/data/stubs/ticks/GME.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c637dde14c778753fc70154ce2562da9ab63ee504910d11fac4166c70d03cfe +size 38987 diff --git a/data/stubs/ticks/GOOGL.parquet b/data/stubs/ticks/GOOGL.parquet new file mode 100644 index 0000000000000000000000000000000000000000..1ed5c39370ab944160bcdd3e9354e2fd10ebf113 --- /dev/null +++ b/data/stubs/ticks/GOOGL.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32c9b80bf550badf7cb6459336ae9a3c91e759c1a5724112554c40874d1af9cf +size 38810 diff --git a/data/stubs/ticks/GS.parquet b/data/stubs/ticks/GS.parquet new file mode 100644 index 0000000000000000000000000000000000000000..dd9ea49967fc1d13f2291bee36ff41e914227183 --- /dev/null +++ b/data/stubs/ticks/GS.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f44a9a80bad6a763bd56c7c181216f6b04ffe7806f19bbc6193c9b15b9ee616 +size 34308 diff --git a/data/stubs/ticks/INTC.parquet b/data/stubs/ticks/INTC.parquet new file mode 100644 index 0000000000000000000000000000000000000000..35f58501ef5e053249bfde354f158fef51c603f7 --- /dev/null +++ b/data/stubs/ticks/INTC.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1d59366050c84ecb0c5483900f80aaf172baf1cbf411256b49a2bebe2a7ed33 +size 31802 diff --git a/data/stubs/ticks/JNJ.parquet b/data/stubs/ticks/JNJ.parquet new file mode 100644 index 0000000000000000000000000000000000000000..1ecb0ec7a6040f8c52a4358f84aee13e16f8e7af --- /dev/null +++ b/data/stubs/ticks/JNJ.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70d238b80c5405c126524c96187274b9957e96c072bf650847135780ea9a1269 +size 33983 diff --git a/data/stubs/ticks/JPM.parquet b/data/stubs/ticks/JPM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..972e3fe3734a405de7a85f4301acf521d51d83ce --- /dev/null +++ b/data/stubs/ticks/JPM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30826bb4917261f6be054590f7b716b8b16ea4917a43929155f094e5f61cbeed +size 43407 diff --git a/data/stubs/ticks/MA.parquet b/data/stubs/ticks/MA.parquet new file mode 100644 index 0000000000000000000000000000000000000000..5d3986fa97a97cd023d6b9965c9cdecd2eb20aeb --- /dev/null +++ b/data/stubs/ticks/MA.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:362e8ee1643a2a8350bff67a70fa2cf06dfaf9d3171d68387a5cdaeee7b1b998 +size 44722 diff --git a/data/stubs/ticks/META.parquet b/data/stubs/ticks/META.parquet new file mode 100644 index 0000000000000000000000000000000000000000..e080c09c5d4e186f05a807533b2ce3a0ca8afba2 --- /dev/null +++ b/data/stubs/ticks/META.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4642fb03d8b9f146a47699dd94475313200f172961e1358d1e7f20be5d60f399 +size 33917 diff --git a/data/stubs/ticks/MS.parquet b/data/stubs/ticks/MS.parquet new file mode 100644 index 0000000000000000000000000000000000000000..404eca6ffcf198b997240c5fbe02127a65da6ec1 --- /dev/null +++ b/data/stubs/ticks/MS.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad05a9af106812e7911dbb12471afea6fd8fc736255fad05cc6bbf7d65d9d7a9 +size 37390 diff --git a/data/stubs/ticks/MSFT.parquet b/data/stubs/ticks/MSFT.parquet new file mode 100644 index 0000000000000000000000000000000000000000..d85b6c0deb8b7b1bc623ef07e9d82d6dea382121 --- /dev/null +++ b/data/stubs/ticks/MSFT.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fcc4a13a3b8a97bd1a5d699fe1b36dd7da03efe90237c18270fc7a4c1ea5d90 +size 48843 diff --git a/data/stubs/ticks/NFLX.parquet b/data/stubs/ticks/NFLX.parquet new file mode 100644 index 0000000000000000000000000000000000000000..a26da4c0e7ff2b9f03823cd251209c411fa31abf --- /dev/null +++ b/data/stubs/ticks/NFLX.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26a8bf563d3fb28dade21333d71c727951423e02406deb51fdb115865c0447a8 +size 47040 diff --git a/data/stubs/ticks/NVDA.parquet b/data/stubs/ticks/NVDA.parquet new file mode 100644 index 0000000000000000000000000000000000000000..c3d4fb90ee26c113a060e7c599a928867f0363b3 --- /dev/null +++ b/data/stubs/ticks/NVDA.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c629e829a225bb07af4ed63190113a7b02c00033719bd09b8f7efaa0e56336b7 +size 39044 diff --git a/data/stubs/ticks/ORCL.parquet b/data/stubs/ticks/ORCL.parquet new file mode 100644 index 0000000000000000000000000000000000000000..550463e676e8cd106b83229a6b4b2a671b03d91c --- /dev/null +++ b/data/stubs/ticks/ORCL.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25919724d0b771b827065654b275b6764b0e017dd2702bf8a793f465ebc1a3c6 +size 42972 diff --git a/data/stubs/ticks/PFE.parquet b/data/stubs/ticks/PFE.parquet new file mode 100644 index 0000000000000000000000000000000000000000..e82eadfc49377ffec9d177e083d3626b5d294f92 --- /dev/null +++ b/data/stubs/ticks/PFE.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1da2a1c5884b23c84e5a13e64afb6e5019700bac8b6722f13bba16fa174b0f76 +size 42304 diff --git a/data/stubs/ticks/PLTR.parquet b/data/stubs/ticks/PLTR.parquet new file mode 100644 index 0000000000000000000000000000000000000000..9eae87b3a317ea55f451655baa3b33b5fc1b1641 --- /dev/null +++ b/data/stubs/ticks/PLTR.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e150f69d90a8a5bcfe8137f960158a31ad2754e9f26d2b255cebd414696d807 +size 37503 diff --git a/data/stubs/ticks/QCOM.parquet b/data/stubs/ticks/QCOM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..01751bc0460836cb0b75601298c4a3a39582dab6 --- /dev/null +++ b/data/stubs/ticks/QCOM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22033937a17024e3119be3a540e68c3974e97ac4e50d7595fead5fcd82492a9b +size 41239 diff --git a/data/stubs/ticks/RIOT.parquet b/data/stubs/ticks/RIOT.parquet new file mode 100644 index 0000000000000000000000000000000000000000..ea4b2c0dc2085777785f867a055e56efc7de97c1 --- /dev/null +++ b/data/stubs/ticks/RIOT.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cecfc36d331fd941c775b265ae31a875c592c56ffb8e8659d2af6c3211703a96 +size 44237 diff --git a/data/stubs/ticks/TSLA.parquet b/data/stubs/ticks/TSLA.parquet new file mode 100644 index 0000000000000000000000000000000000000000..fb3eaf177e97cbb4392b9a25f79082a7bb8cc014 --- /dev/null +++ b/data/stubs/ticks/TSLA.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05d5927218e6882f7f3e6f4a86c57ece22fcc1d29f6ada16bdaae3cf23fefb4c +size 36401 diff --git a/data/stubs/ticks/UNH.parquet b/data/stubs/ticks/UNH.parquet new file mode 100644 index 0000000000000000000000000000000000000000..01e9b4cf53f588075299b5600ae5a8341e55e6db --- /dev/null +++ b/data/stubs/ticks/UNH.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0c47b1e4d93ea50befa9fd3c1c243156e3a32f70f3406bb089d34869a4f72b9 +size 45410 diff --git a/data/stubs/ticks/V.parquet b/data/stubs/ticks/V.parquet new file mode 100644 index 0000000000000000000000000000000000000000..3936335d2e005c9144ecea34fc3096a9a1811a80 --- /dev/null +++ b/data/stubs/ticks/V.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c574ae89a01e4c57f21ba5e984385d433b229d90add16de08e6022c02f1672f +size 50423 diff --git a/data/stubs/ticks/XOM.parquet b/data/stubs/ticks/XOM.parquet new file mode 100644 index 0000000000000000000000000000000000000000..c7e68cf89933b5e9d9727ba83efa6b9caee359e1 --- /dev/null +++ b/data/stubs/ticks/XOM.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d675c52b541adf65b54bf97fceab3bb7d71d1e697c697fbe1709e4ba750a7e2 +size 37289 diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..4584de7e860925957a2daa58cfb4de8acc1e4802 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = tests +pythonpath = . diff --git a/scanner/factor_sources.py b/scanner/factor_sources.py new file mode 100644 index 0000000000000000000000000000000000000000..b4beeec3f9aa8f1f5cd3b9d69cd13da8ed569cd3 --- /dev/null +++ b/scanner/factor_sources.py @@ -0,0 +1,367 @@ +"""Data-source interface for the four "real" institutional flow factors. + +The scanner runs on Hugging Face Spaces (no live market data feed) but the +math should be identical whether the data comes from a local Futu OpenD +gateway or from the bundled synthetic stubs. This module defines a +:func:`get_data_source` factory that returns either: + +* :class:`StubDataSource` - reads pre-baked JSON / parquet files in + ``data/stubs/`` (default on the Space, no network required) +* :class:`FutuDataSource` - live Level-2 / options / tick data via the + Futu OpenD gateway (set ``FSCANNER_DATA_SOURCE=futu``) + +Each factor module (``l2_factor``, ``options_factor``, ``tick_factor``, +``intraday_factor``) calls the relevant method on whichever source is +active. +""" + +from __future__ import annotations + +import json +import os +import random +from datetime import datetime, timedelta +from typing import Optional, Protocol + +import pandas as pd + +from . import paths + + +# --------------------------------------------------------------------------- +# Interface +# --------------------------------------------------------------------------- + +class FactorDataSource(Protocol): + """Abstract data source for the four institutional-flow factors.""" + + name: str + + def get_l2_snapshot(self, ticker: str) -> Optional[dict]: + """Return the most recent Level-2 order-book snapshot for ``ticker``. + + Schema:: + + { + "ticker": "AAPL", + "ts": "2026-06-02T14:30:00Z", + "bids": [[price, size, mpid, age_sec], ...], # top N + "asks": [[price, size, mpid, age_sec], ...], + } + + ``age_sec`` is how long the order has been sitting on the book; + it is used to mitigate spoofing. + """ + ... + + def get_options_history( + self, ticker: str, lookback_days: int = 20 + ) -> Optional[pd.DataFrame]: + """Return daily options-chain aggregates for ``ticker``. + + Schema (one row per ``(date, kind, moneyness_bucket)``):: + + date datetime64 + kind 'call' | 'put' + moneyness 'itm' | 'atm' | 'otm' (delta-based bucket) + volume int + oi int + avg_iv float + """ + ... + + def get_ticks( + self, ticker: str, date: Optional[str] = None + ) -> Optional[pd.DataFrame]: + """Return raw trade ticks for ``ticker`` on ``date`` (YYYY-MM-DD). + + Schema:: + + ts datetime64 + price float + size int + bid float # NBBO bid at time of trade + ask float # NBBO ask at time of trade + """ + ... + + def get_intraday_bars( + self, ticker: str, date: Optional[str] = None, bar_minutes: int = 5 + ) -> Optional[pd.DataFrame]: + """Return pre-aggregated intraday bars (5-min default). + + Schema:: + + bar_start datetime64 + open float + high float + low float + close float + volume int + buy_vol int # buy-initiated + sell_vol int # sell-initiated + """ + ... + + +# --------------------------------------------------------------------------- +# Stub data source (bundled synthetic data) +# --------------------------------------------------------------------------- + +class StubDataSource: + """Reads pre-baked stub data from ``data/stubs/``. + + The stub data is generated at module-build time by + ``data/stubs/_build_stubs.py`` and committed to the repo so the app + can demonstrate the four factors on the Space without any live feed. + """ + + name = "stub" + + def __init__(self, stub_dir: Optional[str] = None) -> None: + self.dir = stub_dir or paths.STUB_DIR + + def get_l2_snapshot(self, ticker: str) -> Optional[dict]: + path = os.path.join(self.dir, "l2", f"{ticker}.json") + if not os.path.exists(path): + return self._synth_l2(ticker) + try: + with open(path, "r", encoding="utf-8") as fh: + return json.load(fh) + except Exception: + return None + + def get_options_history( + self, ticker: str, lookback_days: int = 20 + ) -> Optional[pd.DataFrame]: + path = os.path.join(self.dir, "options", f"{ticker}.parquet") + if not os.path.exists(path): + return self._synth_options(ticker, lookback_days) + try: + df = pd.read_parquet(path) + if df.empty: + return None + cutoff = pd.Timestamp.utcnow().tz_localize(None) - pd.Timedelta(days=lookback_days) + return df[df["date"] >= cutoff].reset_index(drop=True) + except Exception: + return None + + def get_ticks( + self, ticker: str, date: Optional[str] = None + ) -> Optional[pd.DataFrame]: + path = os.path.join(self.dir, "ticks", f"{ticker}.parquet") + if not os.path.exists(path): + return self._synth_ticks(ticker, date) + try: + return pd.read_parquet(path) + except Exception: + return None + + def get_intraday_bars( + self, ticker: str, date: Optional[str] = None, bar_minutes: int = 5 + ) -> Optional[pd.DataFrame]: + path = os.path.join(self.dir, "intraday", f"{ticker}.parquet") + if not os.path.exists(path): + return self._synth_intraday(ticker) + try: + return pd.read_parquet(path) + except Exception: + return None + + # -- Synthetic fallbacks (deterministic per ticker) ------------------ + + @staticmethod + def _seeded(ticker: str) -> random.Random: + return random.Random(f"stub-{ticker}") + + def _synth_l2(self, ticker: str) -> dict: + rng = self._seeded(ticker) + mid = rng.uniform(20, 500) + spread = mid * 0.0005 + bids, asks = [], [] + for i in range(10): + bp = mid - spread / 2 - i * spread * 0.5 + ap = mid + spread / 2 + i * spread * 0.5 + bs = int(rng.lognormvariate(6, 1.2)) + as_ = int(rng.lognormvariate(6, 1.2)) + bids.append([round(bp, 2), bs, "NSDQ", round(rng.uniform(1.2, 30), 1)]) + asks.append([round(ap, 2), as_, "NSDQ", round(rng.uniform(1.2, 30), 1)]) + return { + "ticker": ticker, + "ts": datetime.utcnow().isoformat() + "Z", + "bids": bids, + "asks": asks, + } + + def _synth_options( + self, ticker: str, lookback_days: int + ) -> pd.DataFrame: + rng = self._seeded(ticker + "-opt") + today = pd.Timestamp.utcnow().tz_localize(None).normalize() + rows = [] + for d in range(lookback_days): + date = today - pd.Timedelta(days=d) + for kind in ("call", "put"): + for bucket in ("itm", "atm", "otm"): + base = rng.lognormvariate(7, 0.8) + rows.append({ + "date": date, + "kind": kind, + "moneyness": bucket, + "volume": int(base * rng.uniform(0.5, 1.5)), + "oi": int(base * rng.uniform(3, 10)), + "avg_iv": rng.uniform(0.18, 0.65), + }) + return pd.DataFrame(rows) + + def _synth_ticks(self, ticker: str, date: Optional[str]) -> pd.DataFrame: + rng = self._seeded(ticker + "-ticks") + n = rng.randint(800, 1500) + base = rng.uniform(20, 500) + ts0 = pd.Timestamp(date or "2026-06-02", tz=None) + pd.Timedelta(hours=9, minutes=30) + ticks = [] + price = base + for i in range(n): + dt = pd.Timedelta(seconds=i * 1.5 + rng.uniform(0, 1.5)) + price *= 1 + rng.gauss(0, 0.0005) + spread = price * 0.0003 + side = rng.random() + sz = int(rng.choices([50, 100, 200, 500, 1000, 5000, 10000, 20000], + weights=[0.25, 0.25, 0.15, 0.15, 0.10, 0.05, 0.03, 0.02])[0]) + ticks.append({ + "ts": ts0 + dt, + "price": round(price, 4), + "size": sz, + "bid": round(price - spread / 2, 4), + "ask": round(price + spread / 2, 4), + }) + return pd.DataFrame(ticks) + + def _synth_intraday(self, ticker: str) -> pd.DataFrame: + rng = self._seeded(ticker + "-intra") + bars = [] + day = pd.Timestamp("2026-06-02") + pd.Timedelta(hours=9, minutes=30) + price = rng.uniform(20, 500) + for i in range(78): # 78 * 5min = 6.5h trading day + ts = day + pd.Timedelta(minutes=i * 5) + o = price + ret = rng.gauss(0, 0.003) + c = o * (1 + ret) + h = max(o, c) * (1 + abs(rng.gauss(0, 0.0015))) + l = min(o, c) * (1 - abs(rng.gauss(0, 0.0015))) + v = int(rng.lognormvariate(13, 0.6)) + buy_ratio = 0.5 + rng.gauss(0, 0.08) + buy_ratio = max(0.30, min(0.70, buy_ratio)) + bv = int(v * buy_ratio) + bars.append({ + "bar_start": ts, + "open": o, "high": h, "low": l, "close": c, "volume": v, + "buy_vol": bv, "sell_vol": v - bv, + }) + price = c + return pd.DataFrame(bars) + + +# --------------------------------------------------------------------------- +# Live Futu OpenD adapter (optional) +# --------------------------------------------------------------------------- + +class FutuDataSource: + """Live Level-2 / options / tick data via Futu OpenD. + + To use this, the user runs Futu OpenD locally and sets:: + + export FSCANNER_DATA_SOURCE=futu + export FUTU_OPEND_HOST=127.0.0.1 + export FUTU_OPEND_PORT=11111 + + The Space will not have OpenD reachable, so :func:`get_data_source` + will fall back to the stub source automatically. + """ + + name = "futu" + + def __init__(self, host: str = "127.0.0.1", port: int = 11111) -> None: + self.host = host + self.port = port + self._ctx = None + + def _ensure_ctx(self): + if self._ctx is None: + try: + import futu as ft # type: ignore + except ImportError as e: + raise RuntimeError( + "futu-api is not installed. `pip install futu-api` and " + "make sure Futu OpenD is running." + ) from e + self._ctx = ft.OpenQuoteContext(host=self.host, port=self.port) + return self._ctx + + def get_l2_snapshot(self, ticker: str) -> Optional[dict]: + try: + ctx = self._ensure_ctx() + code = f"US.{ticker}" + ret, data = ctx.get_order_book(code, num=10) + if ret != 0 or data is None or data.empty: + return None + # Futu returns bids and asks as two DataFrames + bids_df = data[0] # (price, volume, turnover, orderid) + asks_df = data[1] + return { + "ticker": ticker, + "ts": datetime.utcnow().isoformat() + "Z", + "bids": bids_df.values.tolist(), + "asks": asks_df.values.tolist(), + } + except Exception: + return None + + def get_options_history( + self, ticker: str, lookback_days: int = 20 + ) -> Optional[pd.DataFrame]: + # Simplified: would need to pull chain + historical IV. Stub fallback + # for the demo. + return StubDataSource().get_options_history(ticker, lookback_days) + + def get_ticks( + self, ticker: str, date: Optional[str] = None + ) -> Optional[pd.DataFrame]: + # Live tick feeds are behind a paid tier; stub fallback. + return StubDataSource().get_ticks(ticker, date) + + def get_intraday_bars( + self, ticker: str, date: Optional[str] = None, bar_minutes: int = 5 + ) -> Optional[pd.DataFrame]: + return StubDataSource().get_intraday_bars(ticker, date, bar_minutes) + + +# --------------------------------------------------------------------------- +# Factory +# --------------------------------------------------------------------------- + +_source: Optional[FactorDataSource] = None + + +def get_data_source() -> FactorDataSource: + """Return the configured data source (singleton).""" + global _source + if _source is not None: + return _source + which = os.environ.get("FSCANNER_DATA_SOURCE", "stub").lower() + if which == "futu": + host = os.environ.get("FUTU_OPEND_HOST", "127.0.0.1") + port = int(os.environ.get("FUTU_OPEND_PORT", "11111")) + try: + _source = FutuDataSource(host=host, port=port) + except Exception: + _source = StubDataSource() + else: + _source = StubDataSource() + return _source + + +def reset_data_source() -> None: + """For tests - force a re-init on next :func:`get_data_source` call.""" + global _source + _source = None diff --git a/scanner/intraday_factor.py b/scanner/intraday_factor.py new file mode 100644 index 0000000000000000000000000000000000000000..33a13edd02554923e84f65a47ac141fb11fedf70 --- /dev/null +++ b/scanner/intraday_factor.py @@ -0,0 +1,85 @@ +"""Intraday VWAP + signed aggression factor. + +Methodology: + + Bar-level aggression per 5-min bar: + buy_ratio_bar = buy_vol / (buy_vol + sell_vol) + + Two factors: + + intraday_vwap_dev = (close - vwap_session) / vwap_session + + aggression_persistence = mean of buy_ratio over the last + ``PERSIST_BARS`` bars, minus 0.5 + (i.e. a centred buy-imbalance) + + Both are bounded in [-3, +3] for cross-sectional z-scoring. +""" + +from __future__ import annotations + +from typing import Optional + +import numpy as np +import pandas as pd + +from .factor_sources import get_data_source + + +PERSIST_BARS = 12 # last 12 * 5min = 1 hour of aggression + + +def compute_intraday_factors( + ticker: str, + source=None, +) -> dict[str, float]: + """Return ``{vwap_dev, aggression_persistence}`` for ``ticker``.""" + empty = {"vwap_dev": 0.0, "aggression_persistence": 0.0} + if source is None: + source = get_data_source() + bars = source.get_intraday_bars(ticker) + if bars is None or bars.empty: + return empty + + bars = bars.copy().sort_values("bar_start").reset_index(drop=True) + + # Session VWAP: typical-price * volume / total volume + tp = (bars["high"] + bars["low"] + bars["close"]) / 3.0 + pv = (tp * bars["volume"]).cumsum() + v = bars["volume"].cumsum().replace(0, np.nan) + session_vwap = (pv / v).ffill().bfill() + + last_close = float(bars["close"].iloc[-1]) + last_vwap = float(session_vwap.iloc[-1]) + if last_vwap > 0: + vwap_dev = (last_close - last_vwap) / last_vwap + else: + vwap_dev = 0.0 + + # Aggression persistence: mean buy ratio of last PERSIST_BARS - 0.5 + total_vol = (bars["buy_vol"] + bars["sell_vol"]).replace(0, np.nan) + buy_ratio_bar = (bars["buy_vol"] / total_vol).fillna(0.5) + if len(buy_ratio_bar) >= PERSIST_BARS: + recent = buy_ratio_bar.iloc[-PERSIST_BARS:] + else: + recent = buy_ratio_bar + aggression_persistence = float(recent.mean() - 0.5) + + return { + "vwap_dev": float(np.clip(vwap_dev * 5, -3, 3)), # scale up small % + "aggression_persistence": float(np.clip(aggression_persistence * 5, -3, 3)), + } + + +def compute_intraday_factors_batch( + tickers: list[str], + source=None, +) -> pd.DataFrame: + if source is None: + source = get_data_source() + rows = [] + for t in tickers: + f = compute_intraday_factors(t, source=source) + f["ticker"] = t + rows.append(f) + return pd.DataFrame(rows).set_index("ticker") diff --git a/scanner/l2_factor.py b/scanner/l2_factor.py new file mode 100644 index 0000000000000000000000000000000000000000..2a2bd38d006e346066d59fbeb5e8cedc3cb141f5 --- /dev/null +++ b/scanner/l2_factor.py @@ -0,0 +1,127 @@ +"""Level-2 large-resting-order factor. + +Methodology (see design doc): + + L2 = clip(+0.40 * depth_ratio + + 0.30 * big_ratio + + 0.30 * tanh(microprice_dev * 50), + -3, +3) + + depth_ratio = dollar-depth on bid side / total top-5 dollar depth + big_ratio = count of "large" (>= BIG_SIZE) bids / total large orders + microprice = (best_bid*ask_sz + best_ask*bid_sz) / (bid_sz + ask_sz) + +Spoofing mitigation: + + - Only count orders with age_sec > SPOOF_AGE_THRESH (default 1.0s). + - If the book is "lying" (depth-heavy side has price action going the + other way) discount the factor. + +Output is a single float, intended to be z-scored cross-sectionally +together with the other 8 factors in :mod:`scanner.scorer`. +""" + +from __future__ import annotations + +import math +from typing import Optional + +from .factor_sources import get_data_source + + +# Tunable constants +BIG_SIZE = 10_000 # shares - threshold for "large" order +TOP_LEVELS = 5 # top N levels for depth +SPOOF_AGE_THRESH = 1.0 # seconds - orders younger than this ignored +MICROPRICE_SCALE = 50.0 # tanh slope +CLIP_RANGE = 3.0 + + +def _book_lying( + depth_ratio: float, recent_return: float = 0.0 +) -> bool: + """Detect a "lying" book: heavy bid-side but price keeps falling (or + the symmetric case). ``recent_return`` is the 5-min return in decimal + (e.g. -0.005 = -0.5%). + """ + if depth_ratio > 0.6 and recent_return < -0.001: + return True + if depth_ratio < 0.4 and recent_return > 0.001: + return True + return False + + +def compute_l2_factor( + ticker: str, + recent_return: float = 0.0, + source=None, +) -> float: + """Compute the Level-2 large-resting-order factor for ``ticker``. + + Returns 0.0 if no book is available (caller should treat as missing, + not as a true neutral). + """ + if source is None: + source = get_data_source() + book = source.get_l2_snapshot(ticker) + if not book: + return 0.0 + + bids = book.get("bids") or [] + asks = book.get("asks") or [] + if not bids or not asks: + return 0.0 + + # Filter to stable (non-spoofed) orders + bids_stable = [b for b in bids if len(b) >= 4 and b[3] >= SPOOF_AGE_THRESH] + asks_stable = [a for a in asks if len(a) >= 4 and a[3] >= SPOOF_AGE_THRESH] + if not bids_stable or not asks_stable: + return 0.0 + + # 1. Depth ratio + depth_bid = sum(p * s for p, s, *_ in bids_stable[:TOP_LEVELS]) + depth_ask = sum(p * s for p, s, *_ in asks_stable[:TOP_LEVELS]) + total = depth_bid + depth_ask + if total <= 0: + return 0.0 + depth_ratio = depth_bid / total + + # 2. Large-order count ratio + big_bid = sum(1 for _, s, *_ in bids_stable if s >= BIG_SIZE) + big_ask = sum(1 for _, s, *_ in asks_stable if s >= BIG_SIZE) + big_total = big_bid + big_ask + big_ratio = (big_bid / big_total) if big_total > 0 else 0.5 + + # 3. Microprice deviation + best_bid, best_bid_sz = bids_stable[0][0], bids_stable[0][1] + best_ask, best_ask_sz = asks_stable[0][0], asks_stable[0][1] + microprice = (best_bid * best_ask_sz + best_ask * best_bid_sz) / (best_bid_sz + best_ask_sz) + mid = (best_bid + best_ask) / 2.0 + if mid > 0: + microprice_dev = (microprice - mid) / mid + else: + microprice_dev = 0.0 + + raw = ( + 0.40 * (depth_ratio - 0.5) # center: 0 = neutral + + 0.30 * (big_ratio - 0.5) # center: 0 = neutral + + 0.30 * math.tanh(microprice_dev * MICROPRICE_SCALE) # already centered + ) + + # Spoofing discount + if _book_lying(depth_ratio, recent_return): + raw *= 0.3 + + return max(-CLIP_RANGE, min(CLIP_RANGE, raw)) + + +def compute_l2_factors( + tickers: list[str], + returns: Optional[dict[str, float]] = None, + source=None, +) -> dict[str, float]: + """Vectorised helper: returns ``{ticker: factor}`` for all tickers.""" + returns = returns or {} + if source is None: + source = get_data_source() + return {t: compute_l2_factor(t, returns.get(t, 0.0), source) for t in tickers} diff --git a/scanner/options_factor.py b/scanner/options_factor.py new file mode 100644 index 0000000000000000000000000000000000000000..544935e7c4c51924ee9efbefe240f4d675d41df2 --- /dev/null +++ b/scanner/options_factor.py @@ -0,0 +1,90 @@ +"""Unusual options activity factor. + +Methodology: + + For each (kind, moneyness_bucket) over the last ``LOOKBACK_DAYS``: + vol_oi = volume / open_interest + z_vol_oi = (today's vol_oi - mean) / std (over lookback window) + + Final score is the vol-weighted sum of z-scores, with far-OTM calls + weighted heaviest (because they're the "lottery ticket" signal most + associated with informed buying): + + OPT = + z_call_otm * 1.0 + + z_call_atm * 0.6 + + z_call_itm * 0.3 + - z_put_otm * 1.0 + - z_put_atm * 0.6 + - z_put_itm * 0.3 + +Output is a single float. +""" + +from __future__ import annotations + +from typing import Optional + +import numpy as np +import pandas as pd + +from .factor_sources import get_data_source + + +LOOKBACK_DAYS = 20 + +WEIGHTS = { + ("call", "otm"): +1.0, + ("call", "atm"): +0.6, + ("call", "itm"): +0.3, + ("put", "otm"): -1.0, + ("put", "atm"): -0.6, + ("put", "itm"): -0.3, +} + + +def _zscore(series: pd.Series) -> float: + """Z-score of the last value against the rest. Returns 0 if degenerate.""" + if len(series) < 3: + return 0.0 + s = series.astype(float) + if s.std() == 0 or not np.isfinite(s.std()): + return 0.0 + return float((s.iloc[-1] - s.mean()) / s.std()) + + +def compute_options_factor( + ticker: str, + source=None, + lookback_days: int = LOOKBACK_DAYS, +) -> float: + """Compute the unusual options activity factor for ``ticker``.""" + if source is None: + source = get_data_source() + df = source.get_options_history(ticker, lookback_days=lookback_days) + if df is None or df.empty: + return 0.0 + + # vol_oi per (date, kind, moneyness) + df = df.copy() + df["vol_oi"] = df["volume"] / df["oi"].replace(0, np.nan) + df = df.dropna(subset=["vol_oi"]) + + score = 0.0 + for (kind, bucket), w in WEIGHTS.items(): + sub = df[(df["kind"] == kind) & (df["moneyness"] == bucket)] + if sub.empty: + continue + sub = sub.sort_values("date") + z = _zscore(sub["vol_oi"]) + score += w * z + + return float(np.clip(score, -5.0, 5.0)) + + +def compute_options_factors( + tickers: list[str], + source=None, +) -> dict[str, float]: + if source is None: + source = get_data_source() + return {t: compute_options_factor(t, source) for t in tickers} diff --git a/scanner/paths.py b/scanner/paths.py index 1740722c0649bcff1fc3a10fef7ae84ac9c44bf0..b078d323c47d67de998ed65de236681321410fca 100644 --- a/scanner/paths.py +++ b/scanner/paths.py @@ -56,9 +56,34 @@ PERFORMANCE_LOG_PATH: str = os.environ.get( RESULTS_CSV_PATH: str = os.path.join(TMP_DIR, "us_flow_scan_results.csv") +L2_CACHE_PATH: str = os.environ.get( + "FSCANNER_L2_CACHE", + os.path.join(TMP_DIR, "us_flow_scanner_l2.parquet"), +) + +OPTIONS_CACHE_PATH: str = os.environ.get( + "FSCANNER_OPTIONS_CACHE", + os.path.join(TMP_DIR, "us_flow_scanner_options.parquet"), +) + +TICK_CACHE_DIR: str = os.environ.get( + "FSCANNER_TICK_CACHE", + os.path.join(TMP_DIR, "us_flow_scanner_ticks"), +) + +INTRADAY_CACHE_DIR: str = os.environ.get( + "FSCANNER_INTRADAY_CACHE", + os.path.join(TMP_DIR, "us_flow_scanner_intraday"), +) + +STUB_DIR: str = os.environ.get( + "FSCANNER_STUB_DIR", + os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "stubs"), +) + def ensure_dirs() -> None: - for d in (TMP_DIR, HISTORY_DIR): + for d in (TMP_DIR, HISTORY_DIR, TICK_CACHE_DIR, INTRADAY_CACHE_DIR): try: os.makedirs(d, exist_ok=True) except OSError: diff --git a/scanner/scorer.py b/scanner/scorer.py index e78d06a7d4f4b611d850bfcb7f090ba84bf4b5d2..53e679e38b8f3a90785724463eb3159e501e421f 100644 --- a/scanner/scorer.py +++ b/scanner/scorer.py @@ -3,21 +3,33 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Iterable - +from typing import Iterable, Optional import numpy as np import pandas as pd from .flow_algo import FactorSet -FACTOR_KEYS = ["cmf", "obv_slope", "big_bar_ratio", "vwap_dev", "rvol_signed"] +FACTOR_KEYS = [ + # Original price-derived factors + "cmf", "obv_slope", "big_bar_ratio", "vwap_dev", "rvol_signed", + # New "real" institutional flow factors + "l2_imbalance", # Level-2 large-resting-order book asymmetry + "unusual_options", # Vol/OI z-score (far-OTM weighted) + "block_aggression", # Tick-level block buy-sell imbalance + "buy_persistence", # Intraday buy-initiated ratio, smoothed +] DEFAULT_WEIGHTS = { - "cmf": 0.30, - "obv_slope": 0.25, - "big_bar_ratio": 0.20, - "vwap_dev": 0.15, - "rvol_signed": 0.10, + "cmf": 0.20, + "obv_slope": 0.15, + "big_bar_ratio": 0.10, + "vwap_dev": 0.10, + "rvol_signed": 0.05, + "l2_imbalance": 0.15, + "unusual_options": 0.10, + "block_aggression": 0.10, + "buy_persistence": 0.05, } +INSTITUTIONAL_KEYS = ["l2_imbalance", "unusual_options", "block_aggression", "buy_persistence"] def robust_zscore(series: pd.Series) -> pd.Series: @@ -49,10 +61,16 @@ def to_dataframe(factors: list[FactorSet]) -> pd.DataFrame: def score_factors( factors: list[FactorSet], weights: dict[str, float] | None = None, + extra_factors: Optional[dict[str, dict[str, float]]] = None, ) -> pd.DataFrame: """Cross-sectionally z-score the factors, weight, sum, scale to [-100, +100]. - Returns the full DataFrame sorted by score descending. + Returns the full DataFrame sorted by score descending. Any + institutional-flow factor not present in :class:`FactorSet` (because + it comes from a separate pipeline: Level-2, options, ticks, intraday) + will be filled with 0.0 (neutral) before z-scoring unless an + ``extra_factors`` dict is supplied. ``extra_factors`` is keyed by + factor name (e.g. ``"l2_imbalance"``) -> ``{ticker: value}``. """ if not factors: return pd.DataFrame() @@ -62,6 +80,20 @@ def score_factors( if df.empty: return df + # Ensure every factor column exists (fill missing with 0.0). This + # lets the original 5-factor pipeline and the 4 institutional + # factors plug in independently. + for k in FACTOR_KEYS: + if k not in df.columns: + df[k] = 0.0 + + # Plug in extra institutional factors (L2, options, ticks, intraday) + if extra_factors: + for k, by_ticker in extra_factors.items(): + if k not in FACTOR_KEYS: + continue + df[k] = df["ticker"].map(by_ticker).fillna(0.0) + # Normalise each factor for k in FACTOR_KEYS: df[f"{k}_z"] = _zscore(df[k]) diff --git a/scanner/tick_factor.py b/scanner/tick_factor.py new file mode 100644 index 0000000000000000000000000000000000000000..5a5f823314da1b5cbb7d95bedca001b00edf06d8 --- /dev/null +++ b/scanner/tick_factor.py @@ -0,0 +1,128 @@ +"""Tick-level trade-size distribution factors. + +Methodology: + + Each trade is bucketed by size: + retail < 100 shares + small 100-1,000 + medium 1,000-10,000 + block >= 10,000 + + Each trade is signed (buy vs sell) via the Lee-Ready tick rule: + if price > mid: buy + if price < mid: sell + if price == mid: use prior tick's sign (default to 0) + + Two factors are produced: + + block_share = block_vol / total_vol (range 0..1) + block_aggression = (block_buys - block_sells) / block_vol (range -1..+1) + + ``block_share`` is z-scored cross-sectionally; ``block_aggression`` is + used directly (already bounded in [-1, +1]). + + ``buy_ratio`` is the all-size signed volume ratio; included here for + convenience so the scoring code can pick it up alongside the other + intraday metrics. +""" + +from __future__ import annotations + +from typing import Optional + +import numpy as np +import pandas as pd + +from .factor_sources import get_data_source + + +# Bucket thresholds (shares) +RETAIL_MAX = 100 +SMALL_MAX = 1_000 +MEDIUM_MAX = 10_000 + + +def _bucket(size: int) -> str: + if size < RETAIL_MAX: + return "retail" + if size < SMALL_MAX: + return "small" + if size < MEDIUM_MAX: + return "medium" + return "block" + + +def _sign_trades(ticks: pd.DataFrame) -> pd.Series: + """Lee-Ready tick rule: sign each trade vs the prevailing mid.""" + mid = (ticks["bid"] + ticks["ask"]) / 2.0 + sign = pd.Series(0, index=ticks.index, dtype=int) + sign[ticks["price"] > mid] = 1 + sign[ticks["price"] < mid] = -1 + + # Trades at the mid: carry forward the prior sign + at_mid = ticks["price"] == mid + if at_mid.any(): + prior = sign.replace(0, np.nan).ffill().fillna(0).astype(int) + sign[at_mid] = prior[at_mid] + return sign + + +def compute_tick_factors( + ticker: str, + source=None, + date: Optional[str] = None, +) -> dict[str, float]: + """Return ``{block_share, block_aggression, buy_ratio}`` for ``ticker``.""" + empty = {"block_share": 0.0, "block_aggression": 0.0, "buy_ratio": 0.5} + if source is None: + source = get_data_source() + ticks = source.get_ticks(ticker, date=date) + if ticks is None or ticks.empty: + return empty + + if "bid" not in ticks.columns or "ask" not in ticks.columns: + # Fall back to rolling mid from price + ticks = ticks.copy() + ticks["mid"] = ticks["price"].rolling(20, min_periods=1).mean() + ticks["bid"] = ticks["mid"] - ticks["mid"] * 0.0003 + ticks["ask"] = ticks["mid"] + ticks["mid"] * 0.0003 + + sign = _sign_trades(ticks) + ticks = ticks.assign(sign=sign, bucket=ticks["size"].apply(_bucket)) + + total_vol = int(ticks["size"].sum()) + if total_vol <= 0: + return empty + + # Block bucket + block = ticks[ticks["bucket"] == "block"] + block_vol = int(block["size"].sum()) + block_buys = int(block.loc[block["sign"] == 1, "size"].sum()) + block_sells = int(block.loc[block["sign"] == -1, "size"].sum()) + + block_share = block_vol / total_vol + block_aggression = ( + (block_buys - block_sells) / block_vol if block_vol > 0 else 0.0 + ) + buy_ratio = float((sign == 1).sum()) / max(1, len(sign)) + + return { + "block_share": float(block_share), + "block_aggression": float(block_aggression), + "buy_ratio": float(buy_ratio), + } + + +def compute_tick_factors_batch( + tickers: list[str], + source=None, +) -> pd.DataFrame: + """Return a DataFrame indexed by ticker with the three tick factors.""" + if source is None: + source = get_data_source() + rows = [] + for t in tickers: + f = compute_tick_factors(t, source=source) + f["ticker"] = t + rows.append(f) + return pd.DataFrame(rows).set_index("ticker") diff --git a/tests/__init__.py b/tests/__init__.py index 49cc8ef0e116cef009fe0bd72473a964bbd07f9b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 Binary files a/tests/__init__.py and b/tests/__init__.py differ diff --git a/tests/conftest.py b/tests/conftest.py index 6e276f28a0106de561dfa53e540ae56080d044a5..99ad9bfb45a6db5ae73bab25e54a88d9bb3c3f29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,6 +37,16 @@ def isolated_paths(tmp_path, monkeypatch): str(tmp_path / "perf.parquet")) monkeypatch.setattr(paths, "RESULTS_CSV_PATH", str(tmp_path / "results.csv")) + monkeypatch.setattr(paths, "L2_CACHE_PATH", + str(tmp_path / "l2.parquet")) + monkeypatch.setattr(paths, "OPTIONS_CACHE_PATH", + str(tmp_path / "options.parquet")) + monkeypatch.setattr(paths, "TICK_CACHE_DIR", + str(tmp_path / "ticks")) + monkeypatch.setattr(paths, "INTRADAY_CACHE_DIR", + str(tmp_path / "intraday")) + monkeypatch.setattr(paths, "STUB_DIR", + str(tmp_path / "stubs")) yield diff --git a/tests/test_intraday_factor.py b/tests/test_intraday_factor.py new file mode 100644 index 0000000000000000000000000000000000000000..4d86faef9a0a261b17f71211401edbb712e9b02c --- /dev/null +++ b/tests/test_intraday_factor.py @@ -0,0 +1,117 @@ +"""Tests for the intraday VWAP + signed aggression factor.""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +import pytest + +from scanner.factor_sources import StubDataSource +from scanner.intraday_factor import ( + PERSIST_BARS, + compute_intraday_factors, + compute_intraday_factors_batch, +) + + +def _bars(prices, vols, buy_ratios): + """Build 5-min bars: each bar has open=high=low=close=price, vol=vol, + buy_vol = vol * buy_ratio. + """ + n = len(prices) + rows = [] + for i in range(n): + bv = int(vols[i] * buy_ratios[i]) + rows.append({ + "bar_start": pd.Timestamp("2026-06-02 09:30") + pd.Timedelta(minutes=5 * i), + "open": prices[i], "high": prices[i], "low": prices[i], "close": prices[i], + "volume": vols[i], + "buy_vol": bv, + "sell_vol": vols[i] - bv, + }) + return pd.DataFrame(rows) + + +class _StaticSource: + def __init__(self, df): + self._df = df + def get_intraday_bars(self, ticker, date=None, bar_minutes=5): + return self._df + + +def test_vwap_dev_zero_when_price_equals_vwap(): + bars = _bars(prices=[100, 100, 100, 100], vols=[1000, 1000, 1000, 1000], buy_ratios=[0.5, 0.5, 0.5, 0.5]) + f = compute_intraday_factors("X", source=_StaticSource(bars)) + assert f["vwap_dev"] == pytest.approx(0.0, abs=0.01) + + +def test_vwap_dev_positive_when_above_vwap(): + # 4 bars at 100, last bar at 110 -> session VWAP = (100*3 + 110*1) / 4 = 102.5 + # close = 110, dev = (110 - 102.5) / 102.5 = 0.073 + bars = _bars(prices=[100, 100, 100, 110], vols=[1000, 1000, 1000, 1000], buy_ratios=[0.5]*4) + f = compute_intraday_factors("X", source=_StaticSource(bars)) + assert f["vwap_dev"] > 0 + + +def test_aggression_persistence_positive_when_recent_buying(): + bars = _bars( + prices=[100] * 30, + vols=[1000] * 30, + buy_ratios=[0.5] * 18 + [0.7] * 12, # last 12 bars are 70% buys + ) + f = compute_intraday_factors("X", source=_StaticSource(bars)) + # Persistence = mean(0.7*12) - 0.5 = 0.2 + assert f["aggression_persistence"] > 0 + + +def test_aggression_persistence_negative_when_recent_selling(): + bars = _bars( + prices=[100] * 30, + vols=[1000] * 30, + buy_ratios=[0.5] * 18 + [0.3] * 12, + ) + f = compute_intraday_factors("X", source=_StaticSource(bars)) + assert f["aggression_persistence"] < 0 + + +def test_aggression_persistence_short_history_uses_all(): + bars = _bars( + prices=[100] * 3, + vols=[1000] * 3, + buy_ratios=[0.6, 0.7, 0.7], + ) + f = compute_intraday_factors("X", source=_StaticSource(bars)) + # mean of 0.667 - 0.5 = 0.167 -> positive + assert f["aggression_persistence"] > 0 + + +def test_clipped_to_range(): + # extreme prices to push vwap_dev off scale + bars = _bars( + prices=[100] * 4 + [200], + vols=[1000] * 5, + buy_ratios=[0.5] * 5, + ) + f = compute_intraday_factors("X", source=_StaticSource(bars)) + assert -3 <= f["vwap_dev"] <= 3 + assert -3 <= f["aggression_persistence"] <= 3 + + +def test_empty_returns_zeros(): + empty = pd.DataFrame(columns=["bar_start", "open", "high", "low", "close", "volume", "buy_vol", "sell_vol"]) + f = compute_intraday_factors("X", source=_StaticSource(empty)) + assert f == {"vwap_dev": 0.0, "aggression_persistence": 0.0} + + +def test_batch(): + bars = _bars(prices=[100]*5, vols=[1000]*5, buy_ratios=[0.5]*5) + out = compute_intraday_factors_batch(["A", "B"], source=_StaticSource(bars)) + assert set(out.index) == {"A", "B"} + assert "vwap_dev" in out.columns + + +def test_stub_synthesises(): + src = StubDataSource(stub_dir="/nonexistent") + df = src.get_intraday_bars("AAPL") + assert df is not None and not df.empty + assert "bar_start" in df.columns diff --git a/tests/test_l2_factor.py b/tests/test_l2_factor.py new file mode 100644 index 0000000000000000000000000000000000000000..13df108f41b22875214469da3fb17d82894bf76b --- /dev/null +++ b/tests/test_l2_factor.py @@ -0,0 +1,139 @@ +"""Tests for the Level-2 large-resting-order factor.""" + +from __future__ import annotations + +import math + +import pytest + +from scanner.factor_sources import StubDataSource +from scanner.l2_factor import ( + BIG_SIZE, + CLIP_RANGE, + SPOOF_AGE_THRESH, + TOP_LEVELS, + _book_lying, + compute_l2_factor, + compute_l2_factors, +) + + +def _book(bids, asks, age=5.0): + """Wrap bid/ask lists in the dict schema the factor expects.""" + return { + "ticker": "TEST", + "ts": "2026-06-02T14:30:00Z", + "bids": [[p, s, "NSDQ", age] for p, s in bids], + "asks": [[p, s, "NSDQ", age] for p, s in asks], + } + + +class _StaticSource: + """Data source that returns the same book for every ticker.""" + def __init__(self, book): + self._book = book + def get_l2_snapshot(self, ticker): + return self._book + + +def test_balanced_book_is_near_zero(): + book = _book( + [(100.0, 1000), (99.99, 800), (99.98, 600)], + [(100.01, 1000), (100.02, 800), (100.03, 600)], + ) + f = compute_l2_factor("X", source=_StaticSource(book)) + assert -0.5 < f < 0.5, f"expected near-zero for balanced book, got {f}" + + +def test_bid_heavy_book_is_positive(): + book = _book( + [(100.0, 50000), (99.99, 40000), (99.98, 30000), (99.97, 20000), (99.96, 10000)], + [(100.01, 200), (100.02, 200), (100.03, 200), (100.04, 200), (100.05, 200)], + ) + f = compute_l2_factor("X", source=_StaticSource(book)) + assert f > 0.3, f"expected positive for bid-heavy book, got {f}" + + +def test_ask_heavy_book_is_negative(): + book = _book( + [(100.0, 200), (99.99, 200), (99.98, 200), (99.97, 200), (99.96, 200)], + [(100.01, 50000), (100.02, 40000), (100.03, 30000), (100.04, 20000), (100.05, 10000)], + ) + f = compute_l2_factor("X", source=_StaticSource(book)) + assert f < -0.3, f"expected negative for ask-heavy book, got {f}" + + +def test_clipped_to_range(): + book = _book( + [(100.0, 1_000_000)] * TOP_LEVELS, + [(100.01, 1)] * TOP_LEVELS, + ) + f = compute_l2_factor("X", source=_StaticSource(book)) + assert -CLIP_RANGE <= f <= CLIP_RANGE + + +def test_spoofed_orders_ignored(): + """Orders with age < SPOOF_AGE_THRESH should be filtered out.""" + book = _book( + [(100.0, 50000)], # one big bid + [(100.01, 200)], + ) + # With normal ages - all included, factor should be bid-heavy + f_normal = compute_l2_factor("X", source=_StaticSource(book)) + assert f_normal > 0.2 + # With spoof ages (0.1s) - the big bid is filtered, book looks thin + book_spoofed = { + "ticker": "X", + "ts": "2026-06-02T14:30:00Z", + "bids": [[100.0, 50000, "NSDQ", 0.1]], # spoof! + "asks": [[100.01, 200, "NSDQ", 5.0]], + } + f_spoofed = compute_l2_factor("X", source=_StaticSource(book_spoofed)) + # Spoofed book should be weaker (closer to 0) + assert abs(f_spoofed) < abs(f_normal) + + +def test_book_lying_detection(): + assert _book_lying(0.7, -0.005) is True # bid-heavy but falling + assert _book_lying(0.7, 0.0) is False # bid-heavy and flat + assert _book_lying(0.3, 0.005) is True # ask-heavy but rising + assert _book_lying(0.5, 0.0) is False # neutral + + +def test_book_lying_discounts_factor(): + book = _book( + [(100.0, 50000), (99.99, 40000), (99.98, 30000), (99.97, 20000), (99.96, 10000)], + [(100.01, 200), (100.02, 200), (100.03, 200), (100.04, 200), (100.05, 200)], + ) + f_honest = compute_l2_factor("X", recent_return=0.001, source=_StaticSource(book)) + f_lying = compute_l2_factor("X", recent_return=-0.01, source=_StaticSource(book)) + # "Lying" book should produce a smaller absolute factor + assert abs(f_lying) < abs(f_honest) + + +def test_empty_book_returns_zero(): + f = compute_l2_factor("X", source=_StaticSource(None)) + assert f == 0.0 + f = compute_l2_factor("X", source=_StaticSource({"bids": [], "asks": []})) + assert f == 0.0 + + +def test_batch_returns_all_tickers(): + book = _book( + [(100.0, 1000), (99.99, 800)], + [(100.01, 1000), (100.02, 800)], + ) + src = _StaticSource(book) + out = compute_l2_factors(["A", "B", "C"], source=src) + assert set(out.keys()) == {"A", "B", "C"} + for v in out.values(): + assert -CLIP_RANGE <= v <= CLIP_RANGE + + +def test_stub_data_source_synthesises(): + src = StubDataSource(stub_dir="/nonexistent") + book = src.get_l2_snapshot("AAPL") + assert book is not None + assert "bids" in book and "asks" in book + assert len(book["bids"]) == 10 + assert all(len(b) == 4 for b in book["bids"]) diff --git a/tests/test_options_factor.py b/tests/test_options_factor.py new file mode 100644 index 0000000000000000000000000000000000000000..5380b9eed3475defb2bc30d0227ab7c684b680c4 --- /dev/null +++ b/tests/test_options_factor.py @@ -0,0 +1,112 @@ +"""Tests for the unusual-options-activity factor.""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +import pytest + +from scanner.factor_sources import StubDataSource +from scanner.options_factor import ( + LOOKBACK_DAYS, + WEIGHTS, + _zscore, + compute_options_factor, + compute_options_factors, +) + + +def _steady_chain(days: int = 20) -> pd.DataFrame: + """Flat chain: same vol/oi for every day -> z = 0.""" + today = pd.Timestamp("2026-06-02") + rows = [] + for d in range(days): + for kind in ("call", "put"): + for bucket in ("itm", "atm", "otm"): + rows.append({ + "date": today - pd.Timedelta(days=d), + "kind": kind, + "moneyness": bucket, + "volume": 1000, + "oi": 10_000, + "avg_iv": 0.30, + }) + return pd.DataFrame(rows) + + +def _spike_chain(spike_side: str = "call", spike_bucket: str = "otm") -> pd.DataFrame: + """Chain where today has a big spike on one (side, bucket) cell.""" + today = pd.Timestamp("2026-06-02") + rows = [] + for d in range(LOOKBACK_DAYS): + for kind in ("call", "put"): + for bucket in ("itm", "atm", "otm"): + if d == 0 and kind == spike_side and bucket == spike_bucket: + vol, oi = 10_000, 10_000 # vol_oi = 1.0 + else: + vol, oi = 1000, 10_000 # vol_oi = 0.1 + rows.append({ + "date": today - pd.Timedelta(days=d), + "kind": kind, "moneyness": bucket, + "volume": vol, "oi": oi, "avg_iv": 0.30, + }) + return pd.DataFrame(rows) + + +class _StaticSource: + def __init__(self, df): + self._df = df + def get_options_history(self, ticker, lookback_days=20): + return self._df + + +def test_steady_chain_is_near_zero(): + f = compute_options_factor("X", source=_StaticSource(_steady_chain())) + assert abs(f) < 0.5, f"steady chain should be near zero, got {f}" + + +def test_call_otm_spike_is_positive(): + f = compute_options_factor("X", source=_StaticSource(_spike_chain("call", "otm"))) + assert f > 0, f"call OTM spike should be positive, got {f}" + + +def test_put_otm_spike_is_negative(): + f = compute_options_factor("X", source=_StaticSource(_spike_chain("put", "otm"))) + assert f < 0, f"put OTM spike should be negative, got {f}" + + +def test_call_otm_weighted_heaviest(): + """A spike in call_otm should produce a more positive factor than the + same magnitude spike in call_itm (because of the weight).""" + f_otm = compute_options_factor("X", source=_StaticSource(_spike_chain("call", "otm"))) + f_itm = compute_options_factor("X", source=_StaticSource(_spike_chain("call", "itm"))) + assert f_otm > f_itm + + +def test_empty_returns_zero(): + empty = pd.DataFrame(columns=["date", "kind", "moneyness", "volume", "oi", "avg_iv"]) + assert compute_options_factor("X", source=_StaticSource(empty)) == 0.0 + assert compute_options_factor("X", source=_StaticSource(None)) == 0.0 + + +def test_zscore_uses_today_vs_history(): + s = pd.Series([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.5]) + z = _zscore(s) + assert z > 2 # well above neutral + + +def test_zscore_constant_returns_zero(): + s = pd.Series([0.5] * 10) + assert _zscore(s) == 0.0 + + +def test_batch(): + f = compute_options_factors(["A", "B"], source=_StaticSource(_steady_chain())) + assert set(f.keys()) == {"A", "B"} + + +def test_stub_data_source_synthesises(): + src = StubDataSource(stub_dir="/nonexistent") + df = src.get_options_history("AAPL", lookback_days=20) + assert df is not None and not df.empty + assert {"date", "kind", "moneyness", "volume", "oi"}.issubset(df.columns) diff --git a/tests/test_performance.py b/tests/test_performance.py index ecbdea08e267520479810d63e75c0e75456477e5..ae7fa8760f6376d3a0bb2dcf64443aa2c43e064e 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -182,7 +182,9 @@ def test_load_learned_weights_round_trip(): assert save_learned_weights(w, metrics={"mean_ic": 0.123}) loaded = load_learned_weights() assert loaded is not None - for k in FACTOR_KEYS: + # Check the keys we actually saved round-trip; new factor keys + # added to FACTOR_KEYS later are not required to be present. + for k in w: assert loaded[k] == pytest.approx(w[k]) meta = load_learned_meta() assert meta["metrics"]["mean_ic"] == pytest.approx(0.123) diff --git a/tests/test_tick_factor.py b/tests/test_tick_factor.py new file mode 100644 index 0000000000000000000000000000000000000000..3bad511e099f30656fb814d1cbaae30db7d4463a --- /dev/null +++ b/tests/test_tick_factor.py @@ -0,0 +1,137 @@ +"""Tests for the tick-level trade-size + buy-initiated ratio factors.""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +import pytest + +from scanner.factor_sources import StubDataSource +from scanner.tick_factor import ( + _bucket, + _sign_trades, + compute_tick_factors, + compute_tick_factors_batch, +) + + +def test_bucket_boundaries(): + assert _bucket(50) == "retail" + assert _bucket(99) == "retail" + assert _bucket(100) == "small" + assert _bucket(999) == "small" + assert _bucket(1_000) == "medium" + assert _bucket(9_999) == "medium" + assert _bucket(10_000) == "block" + assert _bucket(100_000) == "block" + + +def _ticks(prices, sizes, bid=99.99, ask=100.01): + """Build a tick frame with the given prices/sizes at a fixed mid.""" + n = len(prices) + return pd.DataFrame({ + "ts": pd.date_range("2026-06-02 09:30", periods=n, freq="1s"), + "price": prices, + "size": sizes, + "bid": [bid] * n, + "ask": [ask] * n, + }) + + +def test_sign_at_ask_is_buy(): + df = _ticks(prices=[100.02, 100.02], sizes=[100, 100]) + signs = _sign_trades(df) + assert (signs == 1).all() + + +def test_sign_at_bid_is_sell(): + df = _ticks(prices=[99.98, 99.98], sizes=[100, 100]) + signs = _sign_trades(df) + assert (signs == -1).all() + + +def test_sign_at_mid_carries_forward(): + df = _ticks( + prices=[100.02, 100.00, 100.00, 99.98], # buy, mid, mid, sell + sizes=[100, 100, 100, 100], + ) + signs = _sign_trades(df).tolist() + # [1, carry(1), carry(1), -1] + assert signs == [1, 1, 1, -1] + + +def test_block_share_calculation(): + df = _ticks( + prices=[100.02] * 4, + sizes=[100, 1000, 5000, 15000], # 15k block out of 21.1k total + ) + f = compute_tick_factors("X", source=_StaticTickSource(df)) + # 15000 / (100 + 1000 + 5000 + 15000) = 0.71 + assert 0.70 < f["block_share"] < 0.72 + + +def test_block_aggression_positive_when_block_buys(): + df = _ticks( + prices=[100.02] * 3, # all buys (at ask) + sizes=[500, 5000, 20000], # 20k block + ) + f = compute_tick_factors("X", source=_StaticTickSource(df)) + assert f["block_aggression"] == pytest.approx(1.0) + + +def test_block_aggression_negative_when_block_sells(): + df = _ticks( + prices=[99.98] * 3, # all sells (at bid) + sizes=[500, 5000, 20000], + ) + f = compute_tick_factors("X", source=_StaticTickSource(df)) + assert f["block_aggression"] == pytest.approx(-1.0) + + +def test_buy_ratio_in_unit_interval(): + df = _ticks( + prices=[100.02, 100.02, 99.98, 99.98], + sizes=[100, 200, 300, 400], + ) + f = compute_tick_factors("X", source=_StaticTickSource(df)) + assert 0.0 <= f["buy_ratio"] <= 1.0 + + +def test_empty_ticks(): + f = compute_tick_factors("X", source=_StaticTickSource(pd.DataFrame())) + assert f["block_share"] == 0.0 + assert f["block_aggression"] == 0.0 + assert f["buy_ratio"] == 0.5 + + +def test_no_bid_ask_falls_back_to_rolling_mid(): + df = pd.DataFrame({ + "ts": pd.date_range("2026-06-02 09:30", periods=50, freq="1s"), + "price": 100 + np.cumsum(np.random.default_rng(1).normal(0, 0.01, 50)), + "size": [100] * 50, + }) + f = compute_tick_factors("X", source=_StaticTickSource(df)) + assert "buy_ratio" in f + + +def test_batch(): + df = _ticks(prices=[100.02] * 2, sizes=[500, 10000]) + src = _StaticTickSource(df) + out = compute_tick_factors_batch(["A", "B"], source=src) + assert set(out.index) == {"A", "B"} + assert "block_share" in out.columns + + +def test_stub_synthesises(): + src = StubDataSource(stub_dir="/nonexistent") + df = src.get_ticks("AAPL") + assert df is not None and not df.empty + assert {"ts", "price", "size"}.issubset(df.columns) + + +# --- helper source --- +class _StaticTickSource: + def __init__(self, df): + self._df = df + def get_ticks(self, ticker, date=None): + return self._df