Download tests/test_qt3_space.py from s-ttp/updraft: direct link, hf CLI and curl.
- Browser
- Download file 17.5 kB
-
https://huggingface.co/spaces/s-ttp/updraft/resolve/main/tests/test_qt3_space.py
- Command line
-
hf download hf://spaces/s-ttp/updraft/tests/test_qt3_space.py
-
curl -L -o test_qt3_space.py https://huggingface.co/spaces/s-ttp/updraft/resolve/main/tests/test_qt3_space.py
17.5 kB
| """Quality-Trend 3 Space: data client, ranking, rebalance rules and guards, execution, performance, API auth. | |
| No network: Sharadar and Alpaca are faked.""" | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| from datetime import date, datetime | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| import pytest | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| from qt3 import config, performance, ranking, rebalance, sharadar # noqa: E402 | |
| from qt3.broker import BrokerError # noqa: E402 | |
| # --------------------------------------------------------------------------- Sharadar client | |
| class _R: | |
| def __init__(self, code, body): | |
| self.status_code, self._b = code, body | |
| def json(self): | |
| return self._b | |
| class FakeHTTP: | |
| def __init__(self, pages): | |
| self.pages, self.calls = pages, [] | |
| def get(self, url, params=None, timeout=None): | |
| self.calls.append((url, dict(params))) | |
| return _R(200, self.pages[len(self.calls) - 1]) | |
| def test_sharadar_paginates_and_maps_filters(): | |
| cols = [{"name": "ticker"}, {"name": "date"}] | |
| s = FakeHTTP([{"datatable": {"data": [["A", "2026-10-01"]], "columns": cols}, "meta": {"next_cursor_id": "c2"}}, | |
| {"datatable": {"data": [["B", "2026-10-01"]], "columns": cols}, "meta": {"next_cursor_id": None}}]) | |
| df = sharadar.query("SEP", "KEY", session=s, ticker="A,B", date__gte="2026-01-01", columns=["ticker", "date"]) | |
| assert list(df["ticker"]) == ["A", "B"] | |
| p0, p1 = s.calls[0][1], s.calls[1][1] | |
| assert p0["date.gte"] == "2026-01-01" and p0["qopts.columns"] == "ticker,date" and "qopts.cursor_id" not in p0 | |
| assert p1["qopts.cursor_id"] == "c2" | |
| with pytest.raises(sharadar.SharadarError): | |
| sharadar.query("SEP", "", session=s) | |
| # --------------------------------------------------------------------------- ranking (steps 4-5) | |
| def _universe(n=40, seed=0): | |
| rng = np.random.default_rng(seed) | |
| u = pd.DataFrame({"ticker": [f"T{i:02d}" for i in range(n)], "price": 50.0, "dv20": 2e7, | |
| "currency": "USD", "ma50_200": rng.normal(0, .1, n), "mom_6_1": rng.normal(0, .2, n)}) | |
| for c in ("roe", "roa", "gross_margin", "fcf_conversion", "fcf_yield", "buyback_yield", | |
| "net_share_reduction_yoy", "basic_diluted_gap", "equity_financing"): | |
| u[c] = rng.normal(0, 1, n) | |
| return u | |
| def test_rank_table_pool_and_order(): | |
| u = _universe() | |
| u.loc[0, "currency"] = "JPY" # foreign reporter excluded | |
| u.loc[1, "price"] = 3.0 # below $5 excluded | |
| pool = ranking.rank_table(u) | |
| assert "T00" not in set(pool["ticker"]) and "T01" not in set(pool["ticker"]) | |
| assert pool.attrs["liquid_usd"] == 38 | |
| assert len(pool) == pytest.approx(38 * 0.3, abs=1) | |
| assert pool["trend"].is_monotonic_decreasing and list(pool["rank"]) == list(range(1, len(pool) + 1)) | |
| def test_build_uses_api_tables_and_returns_ranked(monkeypatch): | |
| u = _universe(60) | |
| days = pd.bdate_range(end="2026-10-09", periods=260) | |
| def fetch(table, key, **f): | |
| assert table == "DAILY" | |
| return pd.DataFrame({"ticker": u["ticker"], "date": "2026-10-09", "marketcap": np.arange(60, 0, -1) * 1e3}) | |
| def fetch_t(table, key, tickers, **f): | |
| if table == "TICKERS": | |
| return pd.concat([pd.DataFrame({"table": t, "ticker": tickers, "name": "Co", "sector": "Tech", | |
| "industry": "X", "currency": "USD"}) for t in ("SEP", "SF1")]) | |
| if table == "SEP": | |
| rows = [] | |
| for i, t in enumerate(tickers): | |
| c = 100 * np.exp(np.cumsum(np.full(len(days), (i - 30) / 6000))) | |
| rows.append(pd.DataFrame({"ticker": t, "date": days, "close": c, "volume": 1e6, "closeadj": c})) | |
| return pd.concat(rows) | |
| return pd.DataFrame() # SF1 empty β no quality inputs β empty pool is fine | |
| r = ranking.build("KEY", today=datetime(2026, 10, 10), fetch=fetch, fetch_tickers=fetch_t) | |
| assert r["prices_as_of"] == "2026-10-09" and r["universe"] == 60 | |
| # --------------------------------------------------------------------------- rebalance rules | |
| RANKED = ["A", "B", "C", "D", "E", "F", "G", "H"] | |
| def test_target_list_keeps_top6_and_refills(): | |
| final, why = rebalance.target_list(RANKED, ["F", "H", "ZZ"]) | |
| assert final == ["A", "B", "F"] # F (#6) kept; H (#8) and ZZ sold; refill A, B | |
| assert "#8" in why["H"] and "left the quality pool" in why["ZZ"] | |
| assert rebalance.target_list(RANKED, [])[0] == ["A", "B", "C"] | |
| def test_plan_initial_equal_thirds_with_buffer(): | |
| px = {s: 100.0 for s in "ABC"} | |
| p = rebalance.plan(["A", "B", "C"], {}, px, cash=10_000.0, fractional=True) | |
| buys = {o["symbol"]: o["value"] for o in p["orders"]} | |
| assert set(buys) == {"A", "B", "C"} and all(v == pytest.approx(3300, abs=1) for v in buys.values()) | |
| assert p["cash_after"] == pytest.approx(100, abs=1) | |
| def test_plan_sells_first_exit_and_trim(): | |
| px = {"A": 200.0, "B": 100.0, "C": 100.0, "OLD": 10.0} | |
| p = rebalance.plan(["A", "B", "C"], {"A": 30, "B": 10, "OLD": 100}, px, cash=0.0, | |
| reasons={"OLD": "now ranked #9"}, fractional=True) | |
| sides = [(o["symbol"], o["side"]) for o in p["orders"]] | |
| assert sides[0] == ("OLD", "sell") and p["orders"][0]["close_all"] | |
| assert ("A", "sell") in sides and sides.index(("A", "sell")) < sides.index(("C", "buy")) | |
| def test_plan_whole_shares_and_missing_price(): | |
| p = rebalance.plan(["A", "B", "C"], {}, {s: 333.0 for s in "ABC"}, cash=3000.0, fractional=False) | |
| assert all(float(o["shares"]).is_integer() for o in p["orders"]) | |
| with pytest.raises(ValueError, match="C"): | |
| rebalance.plan(["A", "B", "C"], {}, {"A": 1, "B": 1}, cash=10.0) | |
| # --------------------------------------------------------------------------- guards + execution (fake Alpaca) | |
| class FakeAlpaca: | |
| def __init__(self, open_=True, orders=(), days=None, fill="filled"): | |
| self.open_, self._orders, self.fill = open_, list(orders), fill | |
| self._days = days or ["2026-11-02", "2026-11-03", "2026-11-04", "2026-11-05", "2026-11-06", "2026-11-09", | |
| "2026-12-01"] | |
| self.submitted = [] | |
| self.cash = 0.0 | |
| def clock(self): | |
| return {"is_open": self.open_, "next_open": "2026-11-03T09:30:00-05:00"} | |
| def trading_days(self, s, e): | |
| return self._days | |
| def orders(self, after=None, status="all", limit=500): | |
| return self._orders | |
| def submit(self, symbol, side, qty=None, notional=None, client_order_id=None): | |
| self.submitted.append({"symbol": symbol, "side": side, "qty": qty, "notional": notional, "cid": client_order_id}) | |
| if side == "sell": | |
| self.cash += 1000.0 | |
| return {"id": f"o{len(self.submitted)}", "status": "accepted"} | |
| def wait(self, ids, seconds=0, poll=0): | |
| return {i: {"status": self.fill} for i in ids} | |
| def account(self): | |
| return {"cash": self.cash, "non_marginable_buying_power": self.cash, "equity": 10_000.0} | |
| NOV2 = datetime(2026, 11, 2, 10, 30, tzinfo=rebalance.NY) | |
| FRESH = {"ranked": [{"ticker": t} for t in RANKED], "prices_as_of": "2026-10-30"} | |
| def test_check_first_run_and_guards(): | |
| assert rebalance.check(FakeAlpaca(), FRESH, NOV2)["ok"] # first run, day 1, open | |
| assert "market closed" in rebalance.check(FakeAlpaca(open_=False), FRESH, NOV2)["reasons"][0] | |
| stale = {**FRESH, "prices_as_of": "2026-10-20"} | |
| assert any("days old" in r for r in rebalance.check(FakeAlpaca(), stale, NOV2)["reasons"]) | |
| done = [{"client_order_id": "qt3-20261102-A-buy", "side": "buy", "status": "filled", | |
| "created_at": "2026-11-02T15:00:01.123456789Z"}] | |
| c = rebalance.check(FakeAlpaca(orders=done), FRESH, NOV2) | |
| assert not c["ok"] and "already rebalanced this month" in c["reasons"] and c["done_this_month"] | |
| old = [{"client_order_id": "qt3-20261001-A-buy", "side": "buy", "status": "filled", | |
| "created_at": "2026-10-01T15:00:00Z"}] | |
| late = datetime(2026, 11, 9, 10, 30, tzinfo=rebalance.NY) # 6th trading day | |
| assert any("window" in r for r in rebalance.check(FakeAlpaca(orders=old), FRESH, late)["reasons"]) | |
| fresh_late = {**FRESH, "prices_as_of": "2026-11-06"} | |
| assert rebalance.check(FakeAlpaca(orders=old), fresh_late, late, force=True)["ok"] # force skips window | |
| assert not rebalance.check(FakeAlpaca(orders=old), FRESH, late, force=True)["ok"] # never stale data | |
| def test_execute_sells_then_scaled_buys_with_ids(): | |
| a = FakeAlpaca() | |
| p = {"fractional": True, "orders": [ | |
| {"symbol": "OLD", "side": "sell", "shares": 5.0, "value": 1000.0}, | |
| {"symbol": "A", "side": "buy", "shares": 1.0, "value": 600.0}, | |
| {"symbol": "B", "side": "buy", "shares": 1.0, "value": 600.0}]} | |
| res = rebalance.execute(a, p, today=date(2026, 11, 2)) | |
| assert [r["side"] for r in res] == ["sell", "buy", "buy"] | |
| assert a.submitted[0]["cid"] == "qt3-20261102-OLD-sell" | |
| # cash after sells 1000 β 1% of equity (100) = 900 β buys scaled 900/1200 | |
| assert a.submitted[1]["notional"] == pytest.approx(450.0) and a.submitted[1]["cid"] == "qt3-20261102-A-buy" | |
| def test_execute_defers_buys_if_sells_not_filled(): | |
| a = FakeAlpaca(fill="new") | |
| res = rebalance.execute(a, {"fractional": True, "orders": [ | |
| {"symbol": "OLD", "side": "sell", "shares": 1.0, "value": 10.0}, | |
| {"symbol": "A", "side": "buy", "shares": 1.0, "value": 10.0}]}, today=date(2026, 11, 2), wait_s=0) | |
| assert res[-1]["status"].startswith("deferred") and all(s["side"] == "sell" for s in a.submitted) | |
| # --------------------------------------------------------------------------- performance | |
| def test_twr_excludes_deposits_and_drawdown(): | |
| g = performance.twr(["d1", "d2", "d3"], [1000.0, 2100.0, 1890.0], {"d2": 1000.0}) | |
| assert g[1] == pytest.approx(1.10) and g[2] == pytest.approx(0.99) | |
| assert performance.max_drawdown([1.0, 1.2, 0.9, 1.3]) == pytest.approx(-0.25) | |
| # --------------------------------------------------------------------------- API auth | |
| def test_rebalance_endpoint_requires_token(monkeypatch, tmp_path): | |
| from fastapi.testclient import TestClient | |
| monkeypatch.setattr(config, "REBALANCE_TOKEN", "secret-token") | |
| monkeypatch.setattr(config, "NDL_KEY", "") | |
| monkeypatch.setattr(config, "CACHE_DIR", str(tmp_path)) | |
| (tmp_path / "ranking.json").write_text(json.dumps({**FRESH, "built": datetime.now().astimezone().isoformat()})) | |
| import app as space | |
| c = TestClient(space.app) | |
| assert c.get("/api/health").json()["rebalance_enabled"] | |
| r = c.post("/api/rebalance?dry=0", headers={"X-Rebalance-Token": "wrong"}) | |
| assert r.status_code == 403 | |
| # --------------------------------------------------------------------------- in-Space scheduler | |
| def test_scheduler_window_and_guarded_run(monkeypatch): | |
| from qt3 import scheduler | |
| ny = rebalance.NY | |
| assert scheduler.due(datetime(2026, 11, 2, 10, 15, tzinfo=ny)) # Monday 10:15 New York | |
| assert not scheduler.due(datetime(2026, 11, 2, 9, 45, tzinfo=ny)) # before 10:00 | |
| assert not scheduler.due(datetime(2026, 11, 2, 15, 45, tzinfo=ny)) # after 15:30 | |
| assert not scheduler.due(datetime(2026, 11, 7, 11, 0, tzinfo=ny)) # Saturday | |
| monkeypatch.setattr(ranking, "refresh", lambda force=False: FRESH) | |
| a = FakeAlpaca(open_=False) | |
| out = scheduler.run_once(alpaca=a) | |
| assert not out["executed"] and "market closed" in out["reasons"][0] and a.submitted == [] | |
| assert scheduler._lock.acquire(blocking=False) # lock released afterwards | |
| scheduler._lock.release() | |
| def test_scheduler_not_started_without_keys(monkeypatch): | |
| from qt3 import scheduler | |
| monkeypatch.setattr(config, "ALPACA_KEY", "") | |
| monkeypatch.setattr(scheduler, "STATE", {**scheduler.STATE, "running": False}) | |
| assert scheduler.start() is False | |
| def test_bench_value_same_money_and_deposits(): | |
| # S&P +10% on day 2; a $500 deposit on day 3 with S&P flat β benchmark gets the same deposit | |
| b = performance.bench_value([1000.0, 1050.0, 1600.0], [100.0, 110.0, 110.0], ["d1", "d2", "d3"], {"d3": 500.0}) | |
| assert b == [1000.0, 1100.0, 1600.0] | |
| def test_rebuild_rolls_back_fills_and_deposits(): | |
| # now: 10 A + $100 cash. A was bought (10 @ 50) on d3 and B sold (5 @ 20) on d3; $200 deposited on d3. | |
| fills = [{"date": "d3", "symbol": "A", "side": "buy", "qty": 10.0, "price": 50.0}, | |
| {"date": "d3", "symbol": "B", "side": "sell", "qty": 5.0, "price": 20.0}] | |
| closes = {"A": {"d2": 48.0, "d3": 51.0}, "B": {"d2": 21.0, "d3": 20.0}} | |
| out = performance.rebuild(["d2", "d3"], 100.0, {"A": 10.0}, fills, {"d3": 200.0}, closes) | |
| # d2: cash 100 - 200 + 500 - 100 = 300, holding 5 B @ 21 β 405; d3: today's holdings at d3 closes β 610 | |
| assert out == {"d2": 405.0, "d3": 610.0} | |
| assert performance.rebuild(["d2"], 100.0, {"A": 10.0}, [], {}, {"A": {}}) == {} # missing close β skipped | |
| def test_series_fills_alpaca_history_gap_and_uses_live_equity(monkeypatch): | |
| from datetime import datetime as real_dt | |
| from zoneinfo import ZoneInfo | |
| ny = ZoneInfo("America/New_York") | |
| class FixedNow(real_dt): | |
| def now(cls, tz=None): | |
| return real_dt(2026, 10, 13, 11, 0, tzinfo=ny) | |
| class Fake: | |
| def portfolio_history(self): # Alpaca has posted 10-09 (all cash) but not 10-12 yet | |
| return {"timestamp": [int(real_dt(2026, 10, 9, 20, 0, tzinfo=ny).timestamp())], "equity": [1000.0]} | |
| def orders(self, after=None, status="all"): | |
| return [{"client_order_id": "qt3-20261012-A-buy", "created_at": "2026-10-12T14:30:00Z", "symbol": "A", | |
| "side": "buy", "filled_qty": "10", "filled_avg_price": "99", "filled_at": "2026-10-12T14:31:00Z"}] | |
| def account(self): | |
| return {"equity": 1015.0, "cash": 10.0} | |
| def cash_flows(self): | |
| return [] | |
| def positions(self): | |
| return {"A": {"qty": 10.0}} | |
| def daily_bars(self, sym, start, adjustment="all"): | |
| px = {"SPY": {"2026-10-09": 500.0, "2026-10-12": 505.0}, "A": {"2026-10-12": 100.0}}[sym] | |
| return [{"t": d, "c": c} for d, c in px.items()] | |
| def latest_prices(self, syms): | |
| return {"SPY": 510.0} | |
| monkeypatch.setattr(performance, "datetime", FixedNow) | |
| monkeypatch.setattr(config, "INCEPTION", "") | |
| s = performance.series(Fake()) | |
| assert s["dates"] == ["2026-10-09", "2026-10-12", "2026-10-13"] and s["src"] == ["alpaca", "rebuilt", "live"] | |
| assert s["value"] == [1000.0, 1010.0, 1015.0] # 10-12: $10 cash + 10 A @ 100 | |
| assert s["bench_value"] == [1000.0, 1010.0, 1020.0] and s["start"] == "2026-10-12" | |
| def test_ranking_endpoint_while_first_build_runs(monkeypatch, tmp_path): | |
| from fastapi.testclient import TestClient | |
| monkeypatch.setattr(config, "NDL_KEY", "") | |
| monkeypatch.setattr(config, "CACHE_DIR", str(tmp_path)) # no cached ranking yet | |
| import app as space | |
| r = TestClient(space.app).get("/api/ranking") | |
| assert r.status_code == 200 and r.json()["age_hours"] is None and "ranked" not in r.json() | |
| def test_history_replays_rebalances_from_orders(): | |
| from qt3 import history | |
| def o(day, sym, side, qty, px, at="T14:00:00Z"): | |
| return {"client_order_id": f"qt3-{day.replace('-', '')}-{sym}-{side}", "symbol": sym, "side": side, | |
| "filled_qty": str(qty), "filled_avg_price": str(px), "filled_at": day + at} | |
| orders = [o("2026-10-12", s, "buy", 1, 100) for s in "ABC"] + [ | |
| o("2026-11-02", "C", "sell", 1, 120, "T14:00:00Z"), o("2026-11-02", "D", "buy", 1, 110, "T14:05:00Z"), | |
| o("2026-11-02", "A", "buy", 0.05, 100, "T14:05:00Z"), o("2026-11-02", "B", "sell", 0.03, 100, "T14:00:00Z"), | |
| {"client_order_id": "manual-1", "symbol": "Z", "side": "buy", "filled_qty": "5", "filled_avg_price": "10"}] | |
| pos = {"A": {"market_value": 120.0}, "B": {"market_value": 90.0}, "D": {"market_value": 100.0}} | |
| h = history.build(orders, pos, {"2026-10-01": 10000.0, "2026-10-20": 1000.0}, {"C": "Cee Corp"}) | |
| new, first = h["events"] | |
| assert first["first"] and first["bought"] == ["A", "B", "C"] and first["added"] == 0 | |
| assert new["bought"] == ["D"] and new["kept"] == ["A", "B"] and new["added"] == 1000.0 | |
| assert [s["symbol"] for s in new["sold"]] == ["C"] and new["sold"][0]["return"] == pytest.approx(0.2) | |
| assert new["sold"][0]["days"] == 21 and new["sold"][0]["name"] == "Cee Corp" | |
| assert {r["symbol"]: r["value"] for r in new["resplit"]} == {"A": 5.0, "B": -3.0} | |
| op = {r["symbol"]: r for r in h["open"]} | |
| assert set(op) == {"A", "B", "D"} # the manual order is ignored | |
| assert op["A"]["return"] == pytest.approx(120 / 105 - 1) and op["B"]["return"] == pytest.approx(93 / 100 - 1) | |
| s = h["summary"] | |
| assert s["rebalances"] == 2 and s["swaps_per_rebalance"] == 1 and s["closed"] == 1 and s["win_rate"] == 1 | |
| def test_short_company_names(): | |
| import app as space | |
| assert space._short_name("Micron Technology, Inc. Common Stock") == "Micron Technology, Inc." | |
| assert space._short_name("Alphabet Inc. Class A Common Stock") == "Alphabet Inc." | |
| assert space._short_name("HF Sinclair Corporation") == "HF Sinclair Corporation" and space._short_name(None) is None | |