fly-trader distribution: code, fly #62, selector #59, connectome, seed (branch distribution @ 5cb1652)
0f038be verified Download tests/test_fix_train.py from bryceweiner/fly-trader: direct link, hf CLI and curl.
- Browser
- Download file 13.6 kB
-
https://huggingface.co/bryceweiner/fly-trader/resolve/main/tests/test_fix_train.py
- Command line
-
hf download hf://bryceweiner/fly-trader/tests/test_fix_train.py
-
curl -L -o test_fix_train.py https://huggingface.co/bryceweiner/fly-trader/resolve/main/tests/test_fix_train.py
13.6 kB
| """Review fixes in the feature engine and the train pipeline (compaction, window highs, exit-cost size, mature | |
| graduation refresh, causal replay band, backtest exits and baseline, corpus part commits, selector folds).""" | |
| import math | |
| import random | |
| from datetime import date, datetime, timedelta, timezone | |
| import duckdb | |
| import numpy as np | |
| import pandas as pd | |
| import pyarrow as pa | |
| import pyarrow.parquet as pq | |
| import pytest | |
| from fly_trader import config | |
| from fly_trader.market.exit_cost import exit_cost_fraction | |
| from fly_trader.market.features import FEATURE_VERSION, FEATURES, FIDX, TokenMeta, TokenState | |
| from fly_trader.train import corpus_backtest as cb | |
| from fly_trader.train import corpus_features as cf | |
| from fly_trader.train import mature, selector | |
| from fly_trader.train.decisions import DecisionSet | |
| from fly_trader.train.replay_assemble import band_amm | |
| # ---------------------------------------------------------------- market/features.py | |
| def test_compaction_keeps_window_start_price(): | |
| st = TokenState("M"); t = 0.0 | |
| for i in range(20500): | |
| st.append(t, 1.0, 0.1, True, f"s{i % 50}", 100.0); t += 1.0 | |
| t += 3 * 3600 + 300 | |
| st.append(t, 2.0, 0.1, True, "new", 100.0) # triggers compaction | |
| assert len(st.ts) == 2 # the last pre-window trade survives | |
| f, mask = st.features(t, TokenMeta("M")) | |
| assert f[FIDX["ret_3h"]] == pytest.approx(math.log(2.0)) and mask & (1 << FIDX["ret_3h"]) | |
| assert f[FIDX["logsigners_1h"]] == pytest.approx(math.log1p(1)) | |
| assert st.cum_vol[-1] == pytest.approx(0.2) | |
| def test_window_highs_exact_on_long_windows_and_after_compaction(): | |
| random.seed(1); st = TokenState("M"); t = 0.0; lp = 0.0; compacted = False | |
| for i in range(50000): | |
| lp += random.gauss(0, 0.01); st.append(t, math.exp(lp), 0.1, i % 2 == 0, None, 100.0); t += 0.5 | |
| compacted |= st.n_since_compact == 0 | |
| if i % 4999 == 0 and i: | |
| f, _ = st.features(t, TokenMeta("M")) | |
| for k, w in (("1h", 3600.0), ("3h", 10800.0)): | |
| j = next(n for n, x in enumerate(st.ts) if x >= t - w) | |
| assert f[FIDX[f"dd_{k}"]] == pytest.approx(st.logp[-1] - max(st.logp[j:]), abs=1e-12) | |
| assert compacted | |
| def test_exit_cost_uses_fixed_size(monkeypatch): | |
| monkeypatch.setattr(config, "MAX_POSITION_SOL", 7.0) | |
| st = TokenState("M"); st.append(0.0, 1.0, 1.0, True, None, 30.0) | |
| f, _ = st.features(60.0, TokenMeta("M", graduated_at=-3600.0, program_label="Pump.fun Amm")) | |
| assert f[FIDX["exit_cost_0p1"]] == pytest.approx(exit_cost_fraction(0.1, 30.0, 1.0 * 1e9, "Pump.fun Amm")) # market cap = price × supply | |
| # ---------------------------------------------------------------- train/mature.py | |
| def _candles(path, day: date, mints): | |
| t0 = datetime(day.year, day.month, day.day, 12, tzinfo=timezone.utc); rows = [] | |
| for m in mints: | |
| for k in range(30): | |
| rows.append({"mint": m, "ts": t0 + timedelta(minutes=k), "open": 1.0, "high": 1.0, "low": 1.0, "close": 1.0 + k / 100, "buy_sol": 1.0, "sell_sol": 0.5, | |
| "n_buys": 2, "n_sells": 1, "n_traders": 2, "resq_sol": 50.0}) | |
| pq.write_table(pa.Table.from_pylist(rows), path) | |
| def test_mature_part_records_known_graduations_and_refreshes(tmp_path, monkeypatch): | |
| monkeypatch.setattr(mature, "MATURE_DIR", tmp_path / "mature"); monkeypatch.setattr(mature, "MATURE_FEAT_DIR", tmp_path / "feat") | |
| (tmp_path / "mature").mkdir(); d = date(2026, 9, 2) | |
| _candles(tmp_path / "mature" / "2026-09-01.parquet", date(2026, 9, 1), ["A", "B"]); _candles(tmp_path / "mature" / "2026-09-02.parquet", d, ["A", "B"]) | |
| grads = {"A": datetime(2026, 8, 30, tzinfo=timezone.utc)} | |
| assert mature.build_day(d, grads=grads) > 0 | |
| part = tmp_path / "feat" / "2026-09-02" / "part.parquet" | |
| assert mature.part_version(part) == FEATURE_VERSION and mature.part_known(part) == 1 | |
| t = pq.read_table(part, columns=["mint", "age_h"]).to_pandas() | |
| assert t.loc[t["mint"] == "A", "age_h"].notna().all() and t.loc[t["mint"] == "B", "age_h"].isna().all() | |
| more = {**grads, "B": datetime(2026, 8, 31, tzinfo=timezone.utc)} | |
| assert not mature._knows_more(part, grads) | |
| assert not mature._knows_more(part, more) # one more dated mint is below the rebuild bar (no rebuild storm) | |
| monkeypatch.setattr(mature, "KNOWN_REBUILD_MIN", 1) | |
| assert mature._knows_more(part, more) | |
| # a replaced part stays readable until its successor lands; the old one is kept aside | |
| monkeypatch.setattr(mature, "STALE_DIR", tmp_path / "stale") | |
| mature._archive(part, "feat_2026-09-02") | |
| assert part.exists() and len(list((tmp_path / "stale").glob("feat_2026-09-02_v*.parquet"))) == 1 | |
| # a part from before the metadata key: the known count comes from its age_h column | |
| legacy = tmp_path / "legacy.parquet"; tab = pq.read_table(part) | |
| pq.write_table(tab.replace_schema_metadata({b"fly_version": str(FEATURE_VERSION).encode()}), legacy) | |
| assert mature.part_known(legacy) == 1 | |
| # ---------------------------------------------------------------- train/replay_assemble.py | |
| def test_replay_band_is_causal(): | |
| con = duckdb.connect() | |
| con.execute("""CREATE TEMP TABLE t0 AS SELECT 'M' AS mint, TIMESTAMP '2026-09-01' + to_seconds(i) AS ts, i AS slot, 'pump-amm' AS pool, | |
| CASE WHEN i < 10 THEN 1.0 ELSE exp(ln(10000.0) * (i - 9) / 291.0) END AS price FROM range(300) r(i)""") | |
| con.execute("INSERT INTO t0 VALUES ('M', TIMESTAMP '2026-09-01' + to_seconds(150), 150, 'pump-amm', 1e9), " | |
| "('M', TIMESTAMP '2026-08-31 23:50', 0, 'pump', 1e-9)") | |
| band_amm(con, "t0", "t") | |
| assert con.execute("SELECT count(*) FROM t WHERE price = 1.0").fetchone()[0] == 10 # a later 50x+ run no longer deletes graduation-time legs | |
| assert con.execute("SELECT count(*) FROM t WHERE price >= 1e9").fetchone()[0] == 0 # the off-scale leg is dropped | |
| assert con.execute("SELECT count(*) FROM t WHERE pool = 'pump'").fetchone()[0] == 1 # curve legs are not banded | |
| assert con.execute("SELECT count(*) FROM t").fetchone()[0] == 301 | |
| # ---------------------------------------------------------------- train/corpus_backtest.py | |
| def _bt_frame(minutes, closes, t0, resq=1e12, broken=None): | |
| ts = [t0 + timedelta(minutes=m) for m in minutes] | |
| df = pd.DataFrame({"mint": "A", "ts": pd.to_datetime(ts, utc=True), "close": closes, "resq": resq, "has_trades": False}) | |
| df["grad_day"] = df["ts"].dt.date.astype(str); df["broken"] = False if broken is None else broken | |
| return df | |
| def test_time_stop_between_traded_minutes_and_entry_day_fold(): | |
| t0 = datetime(2026, 9, 1, 23, 58, tzinfo=timezone.utc) | |
| df = _bt_frame([0, 1, 2, 200], [1.0, 1.1, 1.2, 5.0], t0) | |
| cand = np.array([True, False, False, False]) | |
| tr = cb.simulate(df, cand, 0.0, 1.0, 1.0, 30) | |
| assert tr.shape == (1, 2) | |
| assert tr[0, 0] == date(2026, 9, 1).toordinal() # entry day, although the exit is on the 2nd | |
| assert tr[0, 1] == pytest.approx(0.2, abs=1e-6) # last close before the 30-minute stop, not minute 200's 5.0 | |
| df = _bt_frame([0, 10, 30, 31], [1.0, 1.1, 1.3, 2.0], t0) | |
| assert cb.simulate(df, cand, 0.0, 1.0, 1.0, 30)[0, 1] == pytest.approx(0.3, abs=1e-6) # a traded minute right at the stop exits there | |
| def test_random_baseline_respects_gates(monkeypatch): | |
| t0 = datetime(2026, 9, 1, 0, 0, tzinfo=timezone.utc); frames = [] | |
| for j in range(12): | |
| f = _bt_frame(list(range(200)), list(1.0 + 0.001 * np.arange(200)), t0 + timedelta(days=j), resq=np.where(np.arange(200) % 2, 50.0, 5.0), | |
| broken=np.arange(200) >= 150) | |
| f["mint"] = f"M{j}"; frames.append(f) | |
| df = pd.concat(frames, ignore_index=True) | |
| gate = (df["resq"].to_numpy() >= cb.MIN_RESQ_SOL) & ~df["broken"].to_numpy(); seen = [] | |
| real = cb.simulate | |
| monkeypatch.setattr(cb, "simulate", lambda d, cand, *a, **k: (seen.append(cand.copy()), real(d, cand, *a, **k))[1]) | |
| monkeypatch.setattr(cb, "STRATEGIES", {"S9 all": lambda d: [("all", np.ones(len(d), bool))]}) | |
| cb.run(df, 0.003, min_trades=1) | |
| assert len(seen) == len(cb.EXITS) + 5 | |
| assert all(not (c & ~gate).any() for c in seen) # strategy and random entries only where the gate allows | |
| assert seen[-1].any() | |
| # ---------------------------------------------------------------- train/corpus_features.py | |
| class _Tx: | |
| def __init__(self, fail=False, pending=()): | |
| self.fail, self.pending, self.rows = fail, list(pending), None | |
| def __call__(self): | |
| return self | |
| def __enter__(self): | |
| return self | |
| def __exit__(self, *a): | |
| return False | |
| def cursor(self): | |
| return self | |
| def execute(self, *a): | |
| return self | |
| def fetchall(self): | |
| return self.pending | |
| def executemany(self, sql, rows): | |
| if self.fail: | |
| raise RuntimeError("db down") | |
| self.rows = rows | |
| def _fake_rows(mint): | |
| g = datetime(2026, 9, 1, tzinfo=timezone.utc).timestamp() | |
| return [cf._row(mint, g + 60 * k, g, 1.0, 1.0, 1.0, 1.0, 1.0, 50.0, False, [0.0] * len(FEATURES), 0, {}) for k in range(3)], 0 | |
| def test_corpus_features_failures_and_atomic_commit(tmp_path, monkeypatch): | |
| g = datetime(2026, 9, 1, tzinfo=timezone.utc) | |
| todo = [{"mint": m, "graduated_at": g, "candle_path": None, "trade_path": None} for m in ("GOOD", "BAD")] | |
| monkeypatch.setattr(config, "CORPUS_FEATURES_DIR", tmp_path); monkeypatch.setattr(cf, "_failed", set()) | |
| monkeypatch.setattr(cf, "build_token", lambda m, *a: (_ for _ in ()).throw(ValueError("corrupt")) if m == "BAD" else _fake_rows(m)) | |
| monkeypatch.setattr(cf, "_pending", lambda limit: todo) | |
| monkeypatch.setattr(cf, "transaction", _Tx(fail=True)) | |
| with pytest.raises(RuntimeError): | |
| cf.build_batch() | |
| assert not [p for p in tmp_path.rglob("*") if p.is_file()] # failed insert: no part left behind, so no duplicate later | |
| tx = _Tx(); monkeypatch.setattr(cf, "transaction", tx) | |
| assert cf.build_batch() == 2 | |
| assert [r[0] for r in tx.rows] == ["GOOD"] and "BAD" in cf._failed # the failure is not registered | |
| files = [p for p in tmp_path.rglob("*") if p.is_file()] | |
| assert len(files) == 1 and files[0].name.startswith("part-") and files[0].suffix == ".parquet" | |
| assert pq.read_table(files[0]).num_rows == 3 and tx.rows[0][4] == str(files[0]) | |
| monkeypatch.undo(); monkeypatch.setattr(cf, "_failed", {"BAD"}) | |
| monkeypatch.setattr(cf, "transaction", _Tx(pending=[{"mint": "BAD"}, {"mint": "X"}, {"mint": "Y"}])) | |
| assert [r["mint"] for r in cf._pending(1)] == ["X"] # skipped in this process | |
| # ---------------------------------------------------------------- train/selector.py | |
| def _ds(n_days=5, per_day=400, seed=0): | |
| rng = np.random.default_rng(seed); n = n_days * per_day | |
| X = np.c_[rng.normal(size=(n, 3)), np.zeros((n, 2))].astype(np.float32); y = (X[:, 0] + 0.3 * rng.normal(size=n) > 0).astype(np.int8) | |
| day = np.array([date(2026, 9, 1) + timedelta(days=k) for k in range(n_days) for _ in range(per_day)], dtype=object) | |
| ts = np.array([datetime(d.year, d.month, d.day, tzinfo=timezone.utc).timestamp() + 60 * (i % per_day) for i, d in enumerate(day)]) | |
| fwd = np.where(y == 1, 0.05, -0.02).astype(np.float32) | |
| return DecisionSet(X=X, y=y, fwd=fwd, fwd_pess=fwd, day=day, ts=ts, mint=np.array([f"M{i % 7}" for i in range(n)]), | |
| cols=["a", "b", "c", "age_known", "log_age_h"], horizon_s=1800.0) # age unknown: graduated before the archive, tradable | |
| def test_selector_fold_guards(): | |
| ds = _ds(); days = ds.days | |
| fo = selector.fold(ds, days[-1], min_train=100, min_test=10) | |
| pick = fo.test & (fo.scores >= selector.MIN_EV) | |
| assert fo is not None and fo.ic is not None and fo.ic > 0.5 and pick.any() and np.isnan(fo.scores[~fo.test]).all() | |
| assert selector.fold(ds, days[0], min_train=100, min_test=10) is None # empty train | |
| assert selector.fold(ds, days[-1]) is None # below the default sizes | |
| from fly_trader.train import selector_eval # importable: runs only as a script | |
| assert selector_eval.fold is selector.fold | |
| def test_selector_trades_only_aged_tokens(): | |
| ds = _ds(); days = ds.days; young = (ds.mint == "M0") | |
| ds.X[young, 3] = 1.0; ds.X[young, 4] = np.log1p(2.0) # M0: known graduation, 2 h old | |
| fo = selector.fold(ds, days[-1], min_train=100, min_test=10); pick = fo.test & (fo.scores >= selector.MIN_EV) | |
| assert pick.any() and not (pick & young).any() and not (fo.test & young).any() # never a test row or a pick | |
| assert list(selector.in_universe(np.array([[0, 0, 0, 1, np.log1p(30.0)], [0, 0, 0, 1, np.log1p(5.0)], [0, 0, 0, 0, 0]]), ds.cols)) == [True, False, True] | |
| assert fo.model.threshold == selector.MIN_EV and fo.model.scaler is not None | |
| def test_buy_line_is_chosen_by_total_profit(): | |
| ds = _ds(); rows = np.ones(len(ds.y), bool) | |
| scores = ds.fwd_pess.astype(np.float64) - 0.01 # good rows +4 %, bad rows -3 % | |
| line, table = selector.choose_line(ds, scores, rows) | |
| assert line in selector.LINE_CANDIDATES and [t["line"] for t in table] == list(selector.LINE_CANDIDATES) | |
| best = max((t for t in table if t["trades"] >= selector.MIN_LINE_TRADES), key=lambda t: t["total"]) | |
| assert line == best["line"] and best["mean"] > 0 | |