release 7 (a744da0941)
Browse files- CHANGELOG.md +40 -0
- fly_trader/agent/fly_live.py +9 -7
- fly_trader/agent/fly_session.py +31 -14
- fly_trader/chain/balances.py +7 -2
- fly_trader/db/schema.py +3 -0
- fly_trader/execution/broker_live.py +11 -2
- fly_trader/execution/ledger.py +26 -6
- fly_trader/execution/worker.py +14 -9
- fly_trader/kalshi/fly_replay.py +3 -2
- fly_trader/kalshi/fly_session.py +17 -12
- fly_trader/train/fly_governance.py +20 -5
- fly_trader/train/fly_replay.py +2 -2
CHANGELOG.md
CHANGED
|
@@ -3,6 +3,46 @@
|
|
| 3 |
Releases are identified by date. The Docker distribution (branch `distribution`, Hugging Face `bryceweiner/fly-trader`)
|
| 4 |
ships signed releases built from this development line.
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
## 2026-10-18, release 6: A restart never holds a sale back, and Kalshi orders go first
|
| 7 |
|
| 8 |
Published to Hugging Face as signed release 6. Fly #157 and selector #154 are unchanged, and so are the definitions they
|
|
|
|
| 3 |
Releases are identified by date. The Docker distribution (branch `distribution`, Hugging Face `bryceweiner/fly-trader`)
|
| 4 |
ships signed releases built from this development line.
|
| 5 |
|
| 6 |
+
## 2026-10-18, release 7: A live buy costs what its paper twin does, and the fly learns again
|
| 7 |
+
|
| 8 |
+
Published to Hugging Face as signed release 7. Fly #157 and selector #154 are unchanged, and so are the definitions they
|
| 9 |
+
were judged on: no model, seed or infrastructure file changes.
|
| 10 |
+
|
| 11 |
+
### Upgrading
|
| 12 |
+
|
| 13 |
+
- **Nothing to do on a Docker install:** its updater installs this release within the hour, as usual.
|
| 14 |
+
- **A server on the host updater** (`deploy/fly_update.py`) holds this code release for 24 hours, or applies it at once
|
| 15 |
+
with `fly_update.py approve 7`.
|
| 16 |
+
- **New columns are added on start:** `deposit_sol` and `deposit_refunded` on `positions`.
|
| 17 |
+
- **If your fly's learning froze** (the console's model page says learning is frozen), lift it with Resume learning once
|
| 18 |
+
this release runs.
|
| 19 |
+
|
| 20 |
+
### Live money
|
| 21 |
+
|
| 22 |
+
- **A live buy costs what its paper twin does.**
|
| 23 |
+
- The ~0.0015 SOL that a first buy of a token pays to open its token account is a deposit. The account's close
|
| 24 |
+
returns it.
|
| 25 |
+
- It was booked as part of the buy, so live buys looked 1–3 % bigger than paper and their entry prices 1–3 % higher.
|
| 26 |
+
- The deposit is now kept apart from the buy and counted in live wealth until the close returns it. Only the close's
|
| 27 |
+
fee is the position's.
|
| 28 |
+
|
| 29 |
+
### The fly
|
| 30 |
+
|
| 31 |
+
- **It learns again, and one strategy's trouble no longer stops the other's learning.**
|
| 32 |
+
- On 2026-10-13 the rollback checks froze all of the fly's learning after rolling ev back three times in three days.
|
| 33 |
+
- ev cannot learn (its learning rate is 0), so there was nothing to roll back. The IC check had tripped only because
|
| 34 |
+
the selector's own ev ranking had a bad day (IC −0.10 to −0.13).
|
| 35 |
+
- Capitulation, which was learning, stopped too. Five days of decay then erased what it had learned, and the
|
| 36 |
+
re-bootstrap the freeze asked for never ran.
|
| 37 |
+
- The checks now judge only what learning did. A strategy that has learned nothing trips none. The IC check compares
|
| 38 |
+
the fly with its frozen copy on the same rows instead of with zero, and the replays count it the same way.
|
| 39 |
+
- Reaching the rollback limit freezes that strategy's learning only.
|
| 40 |
+
- **One fly's operator commands reach it.**
|
| 41 |
+
- The Solana and Robinhood Chain books share one memory. Before, whichever book's minute ran first took a pause,
|
| 42 |
+
resume or rollback, and a resume could reach only the book that did not need it.
|
| 43 |
+
- The Robinhood Chain book also kept learning into the shared memory while the Solana book was frozen.
|
| 44 |
+
- Only the Solana book takes these commands now, and the Robinhood Chain book's learning follows its pause and freezes.
|
| 45 |
+
|
| 46 |
## 2026-10-18, release 6: A restart never holds a sale back, and Kalshi orders go first
|
| 47 |
|
| 48 |
Published to Hugging Face as signed release 6. Fly #157 and selector #154 are unchanged, and so are the definitions they
|
fly_trader/agent/fly_live.py
CHANGED
|
@@ -22,6 +22,7 @@ The paper book ``paper_fly`` keeps running as the mirror. Only after the handove
|
|
| 22 |
live trades, the mean return per trade trailing the paper mirror's same decisions by more than ``GAP_MAX`` pauses
|
| 23 |
entries and raises an alert;
|
| 24 |
- between minutes (``idle``), never in front of an order: the wealth mark — wallet SOL + open positions net of exit cost
|
|
|
|
| 25 |
→ ``wealth_marks`` (book 'live'), read once the worker has nothing queued or in flight (a balance read beside a
|
| 26 |
booking counts that trade twice or not at all) and not just before a boundary; the kill switch on its own peak; and
|
| 27 |
every ``SWEEP_S``, mid-minute, the written-off tokens are swept (what they fetch is booked onto their dead-bag).
|
|
@@ -251,17 +252,17 @@ class LiveMirror:
|
|
| 251 |
if mark is None:
|
| 252 |
return None
|
| 253 |
rebase = rails.kill_rebase_at(conn)
|
| 254 |
-
lamports, gross_v, exit_cost, exposure, n_open = mark
|
| 255 |
-
sol_now = lamports / config.LAMPORTS_PER_SOL; wealth = sol_now + gross_v - exit_cost
|
| 256 |
pk = conn.execute("SELECT max(wealth) AS pk FROM wealth_marks WHERE book = %s AND (%s::timestamptz IS NULL OR ts > %s)",
|
| 257 |
(BOOK, rebase, rebase)).fetchone(); peak = max(float(pk["pk"] or 0.0), wealth)
|
| 258 |
conn.execute("INSERT INTO wealth_marks (beat_id, book, ts, sol_free, positions_value, exit_cost, wealth, peak, drawdown, exposure, n_open) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) "
|
| 259 |
"ON CONFLICT (beat_id, book) DO NOTHING",
|
| 260 |
-
(beat_id, BOOK, m1, sol_now, gross_v - exit_cost, exit_cost, wealth, peak, (1.0 - wealth / peak) if peak > 0 else 0.0, exposure, n_open))
|
| 261 |
if config.VAULT_ENABLED: # deposits, withdrawals and payouts are not performance
|
| 262 |
from ..vault import nav as vault_nav, payout as vault_payout # trading wallet + treasury are one book: a top-up or sweep is no loss
|
| 263 |
-
vault_nav.mark(conn, m1, lamports +
|
| 264 |
-
int(round(exit_cost * config.LAMPORTS_PER_SOL)))
|
| 265 |
ix = vault_nav.status(conn, since=rebase)
|
| 266 |
killed = rails.check_drawdown(conn, ix["value"], ix["peak"], unit="index")
|
| 267 |
else:
|
|
@@ -271,7 +272,8 @@ class LiveMirror:
|
|
| 271 |
return self.last_mark
|
| 272 |
|
| 273 |
def _mark(self, conn, ctx) -> tuple | None:
|
| 274 |
-
"""(wallet lamports, open positions' gross value, their exit cost, exposure at cost, open positions
|
|
|
|
| 275 |
swap is between its fill and its booking (the worker's ``busy``): a balance read beside a booking in progress
|
| 276 |
counts that trade twice or not at all. None when the worker stayed busy ``MARK_WAIT_S``."""
|
| 277 |
if not self.worker.busy.acquire(timeout=MARK_WAIT_S):
|
|
@@ -288,7 +290,7 @@ class LiveMirror:
|
|
| 288 |
gross = int(p["qty"]) / 10 ** int(p.get("decimals") or 6) * px
|
| 289 |
gross_v += gross; exposure += float(p["cost_sol"])
|
| 290 |
exit_cost += gross * exit_cost_fraction(gross, ctx.resqs.get(p["mint"]) or ctx.last_resq(p["mint"]), ctx.mcap(p["mint"], px), p.get("program_label"), ctx.fee(p["mint"]))
|
| 291 |
-
return lamports, gross_v, exit_cost, exposure, len(opens)
|
| 292 |
finally:
|
| 293 |
self.worker.busy.release()
|
| 294 |
|
|
|
|
| 22 |
live trades, the mean return per trade trailing the paper mirror's same decisions by more than ``GAP_MAX`` pauses
|
| 23 |
entries and raises an alert;
|
| 24 |
- between minutes (``idle``), never in front of an order: the wealth mark — wallet SOL + open positions net of exit cost
|
| 25 |
+
+ the token accounts' deposits not yet returned (a first buy's rent, ledger.deposits_held)
|
| 26 |
→ ``wealth_marks`` (book 'live'), read once the worker has nothing queued or in flight (a balance read beside a
|
| 27 |
booking counts that trade twice or not at all) and not just before a boundary; the kill switch on its own peak; and
|
| 28 |
every ``SWEEP_S``, mid-minute, the written-off tokens are swept (what they fetch is booked onto their dead-bag).
|
|
|
|
| 252 |
if mark is None:
|
| 253 |
return None
|
| 254 |
rebase = rails.kill_rebase_at(conn)
|
| 255 |
+
lamports, gross_v, exit_cost, exposure, n_open, deposits = mark
|
| 256 |
+
sol_now = lamports / config.LAMPORTS_PER_SOL; wealth = sol_now + deposits + gross_v - exit_cost
|
| 257 |
pk = conn.execute("SELECT max(wealth) AS pk FROM wealth_marks WHERE book = %s AND (%s::timestamptz IS NULL OR ts > %s)",
|
| 258 |
(BOOK, rebase, rebase)).fetchone(); peak = max(float(pk["pk"] or 0.0), wealth)
|
| 259 |
conn.execute("INSERT INTO wealth_marks (beat_id, book, ts, sol_free, positions_value, exit_cost, wealth, peak, drawdown, exposure, n_open) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) "
|
| 260 |
"ON CONFLICT (beat_id, book) DO NOTHING",
|
| 261 |
+
(beat_id, BOOK, m1, sol_now, deposits + gross_v - exit_cost, exit_cost, wealth, peak, (1.0 - wealth / peak) if peak > 0 else 0.0, exposure, n_open))
|
| 262 |
if config.VAULT_ENABLED: # deposits, withdrawals and payouts are not performance
|
| 263 |
from ..vault import nav as vault_nav, payout as vault_payout # trading wallet + treasury are one book: a top-up or sweep is no loss
|
| 264 |
+
vault_nav.mark(conn, m1, lamports + int(round(deposits * config.LAMPORTS_PER_SOL)) + vault_payout.cached_balance(),
|
| 265 |
+
int(round(gross_v * config.LAMPORTS_PER_SOL)), int(round(exit_cost * config.LAMPORTS_PER_SOL)))
|
| 266 |
ix = vault_nav.status(conn, since=rebase)
|
| 267 |
killed = rails.check_drawdown(conn, ix["value"], ix["peak"], unit="index")
|
| 268 |
else:
|
|
|
|
| 272 |
return self.last_mark
|
| 273 |
|
| 274 |
def _mark(self, conn, ctx) -> tuple | None:
|
| 275 |
+
"""(wallet lamports, open positions' gross value, their exit cost, exposure at cost, open positions, the token
|
| 276 |
+
accounts' deposits held in SOL), read while no
|
| 277 |
swap is between its fill and its booking (the worker's ``busy``): a balance read beside a booking in progress
|
| 278 |
counts that trade twice or not at all. None when the worker stayed busy ``MARK_WAIT_S``."""
|
| 279 |
if not self.worker.busy.acquire(timeout=MARK_WAIT_S):
|
|
|
|
| 290 |
gross = int(p["qty"]) / 10 ** int(p.get("decimals") or 6) * px
|
| 291 |
gross_v += gross; exposure += float(p["cost_sol"])
|
| 292 |
exit_cost += gross * exit_cost_fraction(gross, ctx.resqs.get(p["mint"]) or ctx.last_resq(p["mint"]), ctx.mcap(p["mint"], px), p.get("program_label"), ctx.fee(p["mint"]))
|
| 293 |
+
return lamports, gross_v, exit_cost, exposure, len(opens), ledger.deposits_held(conn, BOOK)
|
| 294 |
finally:
|
| 295 |
self.worker.busy.release()
|
| 296 |
|
fly_trader/agent/fly_session.py
CHANGED
|
@@ -21,9 +21,10 @@ first (CLAUDE.md): ``on_minute`` runs before any other book's minute, ``after_mi
|
|
| 21 |
(the bootstrap, D = 0) from their scores of the minutes resolved in the last seven days (``train/fly_calibrate.py``).
|
| 22 |
|
| 23 |
Hourly: learning statistics (``fly_updates``), a snapshot of the plastic state (kind 'fly_plastic'; hourly for 7 days,
|
| 24 |
-
then one per day), the rollback checks (``train/fly_governance.py``
|
| 25 |
-
least a day old, or D = 0, pauses the checks a day, and the
|
| 26 |
-
|
|
|
|
| 27 |
at least the selector's). The state file ``data/brain/plastic/state.pt`` is written atomically every minute, before
|
| 28 |
tags are marked resolved: on restart, pending tags at or before its watermark count as applied.
|
| 29 |
"""
|
|
@@ -199,6 +200,7 @@ class FlyBook:
|
|
| 199 |
self.lines = {"plastic": dict(own), "frozen": dict(own)}
|
| 200 |
self.sizing = {"plastic": {k: list(v) for k, v in own_s.items()}, "frozen": {k: list(v) for k, v in own_s.items()}}
|
| 201 |
self.applied_through = -math.inf; self.learning_frozen = False; self.checks_paused_until = {k: 0.0 for k in self.names}
|
|
|
|
| 202 |
self.race_started_at: float | None = None; self.last_day: int | None = None; self.last_hour: int | None = None
|
| 203 |
self.rollbacks: dict = {k: [] for k in self.names}; self.nu_set = False; self.hour_stats = self._empty_stats(); self.last_checks: dict = {}
|
| 204 |
s = self._read_state()
|
|
@@ -213,6 +215,7 @@ class FlyBook:
|
|
| 213 |
self.nu_set = True
|
| 214 |
self.lines, self.sizing = s["lines"], s["sizing"]
|
| 215 |
self.applied_through, self.learning_frozen, self.checks_paused_until = s["applied_through"], s["learning_frozen"], s["checks_paused_until"]
|
|
|
|
| 216 |
self.race_started_at, self.last_day, self.last_hour, self.rollbacks = s["race_started_at"], s["last_day"], s["last_hour"], s["rollbacks"]
|
| 217 |
elif s:
|
| 218 |
self.race_started_at = s.get("race_started_at") # a new bootstrap keeps racing the same book
|
|
@@ -309,7 +312,7 @@ class FlyBook:
|
|
| 309 |
|
| 310 |
def _state(self) -> dict:
|
| 311 |
return {"bootstrap_id": self.boot_id, "cfg": self.cfg, "score": config.FLY_SCORE, "bank": self.bank.state() if self.brain is None else None, "lines": self.lines, "sizing": self.sizing,
|
| 312 |
-
"applied_through": self.applied_through, "learning_frozen": self.learning_frozen, "checks_paused_until": self.checks_paused_until,
|
| 313 |
"race_started_at": self.race_started_at, "last_day": self.last_day, "last_hour": self.last_hour, "rollbacks": self.rollbacks,
|
| 314 |
"saved_at": time.time()}
|
| 315 |
|
|
@@ -453,7 +456,9 @@ class FlyBook:
|
|
| 453 |
out = {"minute": ctx.m1.isoformat(), "eligible": len(ctx.mints), "picks": ph["picks"], "lines": self.lines["plastic"], "frozen_lines": self.lines["frozen"],
|
| 454 |
"line": self.lines["plastic"][self.names[0]], "frozen_line": self.lines["frozen"][self.names[0]],
|
| 455 |
"drift": max(drift.values()) if drift else 0.0, "drift_by_strategy": drift, "pending": len(self.pending), "learned": learned,
|
| 456 |
-
"learning_frozen": self.
|
|
|
|
|
|
|
| 457 |
"device": str(self.fly.net.dev)}
|
| 458 |
if st is not None:
|
| 459 |
out.update({k: v for k, v in st.items() if k != "entries"}); out["stage"] = "trading"
|
|
@@ -613,10 +618,17 @@ class FlyBook:
|
|
| 613 |
return stats
|
| 614 |
|
| 615 |
def _teach(self, tags, r: np.ndarray, now: float, pathway: str) -> dict | None:
|
| 616 |
-
"""One update of ``pathway`` (None
|
| 617 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
self.bank.decay_to(now)
|
| 619 |
return None
|
|
|
|
|
|
|
| 620 |
return self.bank.update(tags, torch.tensor(r, dtype=torch.float32), now, pathway=pathway)
|
| 621 |
|
| 622 |
def _teach_fill(self, tags, res: np.ndarray, now: float, stats: dict) -> None:
|
|
@@ -839,7 +851,7 @@ class FlyBook:
|
|
| 839 |
H = self.holds[name]; sc = g("score") - np.nan_to_num(g("fill_score"))
|
| 840 |
rp = gov.picks_returns(ts, mint, H, sc, g("line"), lab); rf = gov.picks_returns(ts, mint, H, g("frozen_score"), g("frozen_line"), lab)
|
| 841 |
recent = ts + H + LABEL_LAG_S > now - gov.IC_HOURS * 3600.0
|
| 842 |
-
out[name] = gov.check((rp, rf), (sc[recent], lab[recent]), self._drift(j))
|
| 843 |
else:
|
| 844 |
out[name] = gov.check(None, None, self._drift(j))
|
| 845 |
return out
|
|
@@ -857,7 +869,7 @@ class FlyBook:
|
|
| 857 |
a = np.array([r["fill_score"] for r in rows], dtype=np.float64); b = np.array([r["fill"] for r in rows], dtype=np.float64)
|
| 858 |
ic = float(np.corrcoef(a, b)[0, 1]) if len(rows) >= 3 and a.std() > 0 and b.std() > 0 else None
|
| 859 |
out[name] = {"n": len(rows), "ic": ic, "drift": float(self.bank.drift(j, pathway="fill")[0])}
|
| 860 |
-
if len(rows) >= FILL_IC_MIN and ic is not None and ic < 0 and not self.learning_frozen:
|
| 861 |
self.bank.reset_columns(self.bank.fill[j]); out[name]["reset"] = True
|
| 862 |
record_event("warning", "fly", f"{name} fill pathway back to its bootstrap: its fill predictions ran against the fills (IC {ic:+.2f} over {len(rows)})", out[name])
|
| 863 |
return out
|
|
@@ -879,7 +891,7 @@ class FlyBook:
|
|
| 879 |
good = not any(c["triggers"] for c in checks.values())
|
| 880 |
sid = self._snapshot(conn, now, good, checks)
|
| 881 |
for name, c in checks.items():
|
| 882 |
-
if c["triggers"] and now >= self.checks_paused_until.get(name, 0.0) and not self.learning_frozen:
|
| 883 |
self._rollback(conn, now, c, sid, name=name)
|
| 884 |
self._prune(conn, now)
|
| 885 |
activity.prune(activity_dir(), now - activity.KEEP_S)
|
|
@@ -895,7 +907,11 @@ class FlyBook:
|
|
| 895 |
(self.run_id, str(path), json.dumps(note, default=str))).fetchone()["id"])
|
| 896 |
|
| 897 |
def _commands(self, conn, now: float) -> None:
|
| 898 |
-
"""Operator commands from the console (``send_command``): pause / resume learning, roll back now.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
r = conn.execute("SELECT value FROM ui_settings WHERE key = %s", (COMMAND_KEY,)).fetchone()
|
| 900 |
if not r or not r["value"]:
|
| 901 |
return
|
|
@@ -912,7 +928,7 @@ class FlyBook:
|
|
| 912 |
if cmd == "pause":
|
| 913 |
self.learning_frozen = True; record_event("info", "fly", "learning paused by the operator")
|
| 914 |
elif cmd == "resume":
|
| 915 |
-
self.learning_frozen = False; self.rollbacks = {}; record_event("info", "fly", "learning resumed by the operator")
|
| 916 |
elif cmd == "rollback":
|
| 917 |
for j, name in enumerate(self.names):
|
| 918 |
self._rollback(conn, now, {"triggers": ["operator"], "drift": self._drift(j)}, None, count=False, name=name)
|
|
@@ -940,10 +956,11 @@ class FlyBook:
|
|
| 940 |
(f"{name}: " + ", ".join(checks["triggers"]), json.dumps(checks, default=str), from_sid, target["id"] if target is not None else None))
|
| 941 |
record_event("warning", "fly", f"{name} channel rolled back ({', '.join(checks['triggers'])})", {"to_snapshot": target["id"] if target is not None else "bootstrap (D = 0)", "checks": checks})
|
| 942 |
if count and len(self.rollbacks[name]) >= ROLLBACK_LIMIT:
|
| 943 |
-
self.
|
| 944 |
from ..train import pipeline
|
| 945 |
pipeline.request_run(fly=True, reason=f"{name}: {len(self.rollbacks[name])} rollbacks in 7 days")
|
| 946 |
-
record_event("error", "fly", f"learning frozen after {len(self.rollbacks[name])}
|
|
|
|
| 947 |
|
| 948 |
def _prune(self, conn, now: float) -> None:
|
| 949 |
rows = conn.execute("SELECT id, path, ts FROM brain_snapshots WHERE kind = 'fly_plastic' AND ts < %s ORDER BY ts DESC",
|
|
|
|
| 21 |
(the bootstrap, D = 0) from their scores of the minutes resolved in the last seven days (``train/fly_calibrate.py``).
|
| 22 |
|
| 23 |
Hourly: learning statistics (``fly_updates``), a snapshot of the plastic state (kind 'fly_plastic'; hourly for 7 days,
|
| 24 |
+
then one per day), the rollback checks (``train/fly_governance.py``: they judge what learning did, against the frozen
|
| 25 |
+
shadow; a rollback restores the newest good snapshot at least a day old, or D = 0, pauses the checks a day, and the
|
| 26 |
+
third within 7 days freezes that strategy's learning -- not the other strategies' -- and requests a re-bootstrap), and
|
| 27 |
+
the handover (``HANDOVER_DAYS`` of racing, at least ``HANDOVER_MIN_TRADES`` trades, realized P&L
|
| 28 |
at least the selector's). The state file ``data/brain/plastic/state.pt`` is written atomically every minute, before
|
| 29 |
tags are marked resolved: on restart, pending tags at or before its watermark count as applied.
|
| 30 |
"""
|
|
|
|
| 200 |
self.lines = {"plastic": dict(own), "frozen": dict(own)}
|
| 201 |
self.sizing = {"plastic": {k: list(v) for k, v in own_s.items()}, "frozen": {k: list(v) for k, v in own_s.items()}}
|
| 202 |
self.applied_through = -math.inf; self.learning_frozen = False; self.checks_paused_until = {k: 0.0 for k in self.names}
|
| 203 |
+
self.frozen: list[str] = [] # strategies whose learning the rollback limit froze (learning_frozen: the operator's pause, all)
|
| 204 |
self.race_started_at: float | None = None; self.last_day: int | None = None; self.last_hour: int | None = None
|
| 205 |
self.rollbacks: dict = {k: [] for k in self.names}; self.nu_set = False; self.hour_stats = self._empty_stats(); self.last_checks: dict = {}
|
| 206 |
s = self._read_state()
|
|
|
|
| 215 |
self.nu_set = True
|
| 216 |
self.lines, self.sizing = s["lines"], s["sizing"]
|
| 217 |
self.applied_through, self.learning_frozen, self.checks_paused_until = s["applied_through"], s["learning_frozen"], s["checks_paused_until"]
|
| 218 |
+
self.frozen = [k for k in s.get("frozen") or [] if k in self.names]
|
| 219 |
self.race_started_at, self.last_day, self.last_hour, self.rollbacks = s["race_started_at"], s["last_day"], s["last_hour"], s["rollbacks"]
|
| 220 |
elif s:
|
| 221 |
self.race_started_at = s.get("race_started_at") # a new bootstrap keeps racing the same book
|
|
|
|
| 312 |
|
| 313 |
def _state(self) -> dict:
|
| 314 |
return {"bootstrap_id": self.boot_id, "cfg": self.cfg, "score": config.FLY_SCORE, "bank": self.bank.state() if self.brain is None else None, "lines": self.lines, "sizing": self.sizing,
|
| 315 |
+
"applied_through": self.applied_through, "learning_frozen": self.learning_frozen, "frozen": self.frozen, "checks_paused_until": self.checks_paused_until,
|
| 316 |
"race_started_at": self.race_started_at, "last_day": self.last_day, "last_hour": self.last_hour, "rollbacks": self.rollbacks,
|
| 317 |
"saved_at": time.time()}
|
| 318 |
|
|
|
|
| 456 |
out = {"minute": ctx.m1.isoformat(), "eligible": len(ctx.mints), "picks": ph["picks"], "lines": self.lines["plastic"], "frozen_lines": self.lines["frozen"],
|
| 457 |
"line": self.lines["plastic"][self.names[0]], "frozen_line": self.lines["frozen"][self.names[0]],
|
| 458 |
"drift": max(drift.values()) if drift else 0.0, "drift_by_strategy": drift, "pending": len(self.pending), "learned": learned,
|
| 459 |
+
"learning_frozen": (self.brain or self).learning_frozen, "frozen_strategies": (self.brain or self).frozen, "channels": self.cfg,
|
| 460 |
+
"holds_min": {k: v / 60.0 for k, v in self.holds.items()},
|
| 461 |
+
"bootstrap": self.boot_id,
|
| 462 |
"device": str(self.fly.net.dev)}
|
| 463 |
if st is not None:
|
| 464 |
out.update({k: v for k, v in st.items() if k != "entries"}); out["stage"] = "trading"
|
|
|
|
| 618 |
return stats
|
| 619 |
|
| 620 |
def _teach(self, tags, r: np.ndarray, now: float, pathway: str) -> dict | None:
|
| 621 |
+
"""One update of ``pathway`` by the tags of strategies that may learn (None when none may: the memory only decays).
|
| 622 |
+
A strategy the rollback limit froze keeps decaying but learns nothing; the operator's pause freezes all. Another
|
| 623 |
+
chain's book teaches the Solana book's memory, so that book's freezes are the ones that hold."""
|
| 624 |
+
owner = self.brain if self.brain is not None else self
|
| 625 |
+
held = [self.names.index(k) for k in owner.frozen if k in self.names]
|
| 626 |
+
keep = ~np.isin(np.asarray(tags.s), held) if held else np.ones(len(tags), bool)
|
| 627 |
+
if owner.learning_frozen or not keep.any():
|
| 628 |
self.bank.decay_to(now)
|
| 629 |
return None
|
| 630 |
+
if not keep.all():
|
| 631 |
+
tags, r = tags.subset(keep), np.asarray(r)[keep]
|
| 632 |
return self.bank.update(tags, torch.tensor(r, dtype=torch.float32), now, pathway=pathway)
|
| 633 |
|
| 634 |
def _teach_fill(self, tags, res: np.ndarray, now: float, stats: dict) -> None:
|
|
|
|
| 851 |
H = self.holds[name]; sc = g("score") - np.nan_to_num(g("fill_score"))
|
| 852 |
rp = gov.picks_returns(ts, mint, H, sc, g("line"), lab); rf = gov.picks_returns(ts, mint, H, g("frozen_score"), g("frozen_line"), lab)
|
| 853 |
recent = ts + H + LABEL_LAG_S > now - gov.IC_HOURS * 3600.0
|
| 854 |
+
out[name] = gov.check((rp, rf), (sc[recent], lab[recent]), self._drift(j), ic_frozen_rows=(g("frozen_score")[recent], lab[recent]))
|
| 855 |
else:
|
| 856 |
out[name] = gov.check(None, None, self._drift(j))
|
| 857 |
return out
|
|
|
|
| 869 |
a = np.array([r["fill_score"] for r in rows], dtype=np.float64); b = np.array([r["fill"] for r in rows], dtype=np.float64)
|
| 870 |
ic = float(np.corrcoef(a, b)[0, 1]) if len(rows) >= 3 and a.std() > 0 and b.std() > 0 else None
|
| 871 |
out[name] = {"n": len(rows), "ic": ic, "drift": float(self.bank.drift(j, pathway="fill")[0])}
|
| 872 |
+
if len(rows) >= FILL_IC_MIN and ic is not None and ic < 0 and not self.learning_frozen and name not in self.frozen:
|
| 873 |
self.bank.reset_columns(self.bank.fill[j]); out[name]["reset"] = True
|
| 874 |
record_event("warning", "fly", f"{name} fill pathway back to its bootstrap: its fill predictions ran against the fills (IC {ic:+.2f} over {len(rows)})", out[name])
|
| 875 |
return out
|
|
|
|
| 891 |
good = not any(c["triggers"] for c in checks.values())
|
| 892 |
sid = self._snapshot(conn, now, good, checks)
|
| 893 |
for name, c in checks.items():
|
| 894 |
+
if c["triggers"] and now >= self.checks_paused_until.get(name, 0.0) and not self.learning_frozen and name not in self.frozen:
|
| 895 |
self._rollback(conn, now, c, sid, name=name)
|
| 896 |
self._prune(conn, now)
|
| 897 |
activity.prune(activity_dir(), now - activity.KEEP_S)
|
|
|
|
| 907 |
(self.run_id, str(path), json.dumps(note, default=str))).fetchone()["id"])
|
| 908 |
|
| 909 |
def _commands(self, conn, now: float) -> None:
|
| 910 |
+
"""Operator commands from the console (``send_command``): pause / resume learning, roll back now. They act on the
|
| 911 |
+
memory, which another chain's book shares with the Solana book: only the Solana book takes them (one key: whichever
|
| 912 |
+
book's minute ran first used to take a command, and a resume reached only the book that did not need it)."""
|
| 913 |
+
if self.brain is not None:
|
| 914 |
+
return
|
| 915 |
r = conn.execute("SELECT value FROM ui_settings WHERE key = %s", (COMMAND_KEY,)).fetchone()
|
| 916 |
if not r or not r["value"]:
|
| 917 |
return
|
|
|
|
| 928 |
if cmd == "pause":
|
| 929 |
self.learning_frozen = True; record_event("info", "fly", "learning paused by the operator")
|
| 930 |
elif cmd == "resume":
|
| 931 |
+
self.learning_frozen = False; self.frozen = []; self.rollbacks = {}; record_event("info", "fly", "learning resumed by the operator")
|
| 932 |
elif cmd == "rollback":
|
| 933 |
for j, name in enumerate(self.names):
|
| 934 |
self._rollback(conn, now, {"triggers": ["operator"], "drift": self._drift(j)}, None, count=False, name=name)
|
|
|
|
| 956 |
(f"{name}: " + ", ".join(checks["triggers"]), json.dumps(checks, default=str), from_sid, target["id"] if target is not None else None))
|
| 957 |
record_event("warning", "fly", f"{name} channel rolled back ({', '.join(checks['triggers'])})", {"to_snapshot": target["id"] if target is not None else "bootstrap (D = 0)", "checks": checks})
|
| 958 |
if count and len(self.rollbacks[name]) >= ROLLBACK_LIMIT:
|
| 959 |
+
self.frozen = sorted(set(self.frozen) | {name}) # this strategy's learning only: the others keep learning
|
| 960 |
from ..train import pipeline
|
| 961 |
pipeline.request_run(fly=True, reason=f"{name}: {len(self.rollbacks[name])} rollbacks in 7 days")
|
| 962 |
+
record_event("error", "fly", f"{name} learning frozen after {len(self.rollbacks[name])} rollbacks in 7 days; re-bootstrap requested",
|
| 963 |
+
{"rollbacks": self.rollbacks, "frozen": self.frozen})
|
| 964 |
|
| 965 |
def _prune(self, conn, now: float) -> None:
|
| 966 |
rows = conn.execute("SELECT id, path, ts FROM brain_snapshots WHERE kind = 'fly_plastic' AND ts < %s ORDER BY ts DESC",
|
fly_trader/chain/balances.py
CHANGED
|
@@ -19,25 +19,30 @@ class Snapshot:
|
|
| 19 |
tokens: dict[str, int]
|
| 20 |
ts: datetime
|
| 21 |
decimals: dict[str, int] = field(default_factory=dict)
|
|
|
|
| 22 |
|
| 23 |
def token(self, mint: str) -> int:
|
| 24 |
return int(self.tokens.get(mint, 0))
|
| 25 |
|
| 26 |
def to_json(self) -> dict:
|
| 27 |
return {"lamports": int(self.lamports), "tokens": {m: int(a) for m, a in self.tokens.items()},
|
| 28 |
-
"decimals": {m: int(d) for m, d in self.decimals.items()}, "
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
def snapshot_balances(rpc, pubkey) -> Snapshot:
|
| 32 |
lamports = int(rpc.get_balance(str(pubkey)))
|
| 33 |
tokens: dict[str, int] = {}
|
| 34 |
decimals: dict[str, int] = {}
|
|
|
|
| 35 |
for acct in rpc.get_token_accounts_by_owner(str(pubkey)):
|
| 36 |
mint = acct["mint"]
|
| 37 |
tokens[mint] = tokens.get(mint, 0) + int(acct["amount"])
|
| 38 |
if acct.get("decimals") is not None:
|
| 39 |
decimals[mint] = int(acct["decimals"])
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
def compute_delta(pre: Snapshot, post: Snapshot) -> dict:
|
|
|
|
| 19 |
tokens: dict[str, int]
|
| 20 |
ts: datetime
|
| 21 |
decimals: dict[str, int] = field(default_factory=dict)
|
| 22 |
+
deposits: dict[str, int] = field(default_factory=dict) # lamports held by the wallet's token accounts of each mint (their rent)
|
| 23 |
|
| 24 |
def token(self, mint: str) -> int:
|
| 25 |
return int(self.tokens.get(mint, 0))
|
| 26 |
|
| 27 |
def to_json(self) -> dict:
|
| 28 |
return {"lamports": int(self.lamports), "tokens": {m: int(a) for m, a in self.tokens.items()},
|
| 29 |
+
"decimals": {m: int(d) for m, d in self.decimals.items()}, "deposits": {m: int(v) for m, v in self.deposits.items()},
|
| 30 |
+
"ts": self.ts.isoformat()}
|
| 31 |
|
| 32 |
|
| 33 |
def snapshot_balances(rpc, pubkey) -> Snapshot:
|
| 34 |
lamports = int(rpc.get_balance(str(pubkey)))
|
| 35 |
tokens: dict[str, int] = {}
|
| 36 |
decimals: dict[str, int] = {}
|
| 37 |
+
deposits: dict[str, int] = {}
|
| 38 |
for acct in rpc.get_token_accounts_by_owner(str(pubkey)):
|
| 39 |
mint = acct["mint"]
|
| 40 |
tokens[mint] = tokens.get(mint, 0) + int(acct["amount"])
|
| 41 |
if acct.get("decimals") is not None:
|
| 42 |
decimals[mint] = int(acct["decimals"])
|
| 43 |
+
if not acct.get("is_native"): # a wrapped-SOL account's lamports are SOL, not a deposit
|
| 44 |
+
deposits[mint] = deposits.get(mint, 0) + int(acct.get("lamports") or 0)
|
| 45 |
+
return Snapshot(lamports=lamports, tokens=tokens, ts=config.utcnow(), decimals=decimals, deposits=deposits)
|
| 46 |
|
| 47 |
|
| 48 |
def compute_delta(pre: Snapshot, post: Snapshot) -> dict:
|
fly_trader/db/schema.py
CHANGED
|
@@ -131,6 +131,9 @@ BASE_DDL: list[str] = [
|
|
| 131 |
realized_sol double precision, fees_sol double precision NOT NULL DEFAULT 0,
|
| 132 |
status text NOT NULL DEFAULT 'open', forced_exit_kind text, last_swap_ts timestamptz, satiety double precision NOT NULL DEFAULT 0)""",
|
| 133 |
"CREATE UNIQUE INDEX IF NOT EXISTS positions_open_uniq ON positions (book, mint) WHERE status = 'open'",
|
|
|
|
|
|
|
|
|
|
| 134 |
"CREATE INDEX IF NOT EXISTS positions_book_status_idx ON positions (book, status)",
|
| 135 |
"""CREATE TABLE IF NOT EXISTS wealth_marks (
|
| 136 |
beat_id bigint NOT NULL, book text NOT NULL, ts timestamptz NOT NULL DEFAULT now(),
|
|
|
|
| 131 |
realized_sol double precision, fees_sol double precision NOT NULL DEFAULT 0,
|
| 132 |
status text NOT NULL DEFAULT 'open', forced_exit_kind text, last_swap_ts timestamptz, satiety double precision NOT NULL DEFAULT 0)""",
|
| 133 |
"CREATE UNIQUE INDEX IF NOT EXISTS positions_open_uniq ON positions (book, mint) WHERE status = 'open'",
|
| 134 |
+
# a live buy's token-account rent (the account's close returns it): a deposit kept apart from the position's cost
|
| 135 |
+
"ALTER TABLE positions ADD COLUMN IF NOT EXISTS deposit_sol double precision",
|
| 136 |
+
"ALTER TABLE positions ADD COLUMN IF NOT EXISTS deposit_refunded boolean NOT NULL DEFAULT false",
|
| 137 |
"CREATE INDEX IF NOT EXISTS positions_book_status_idx ON positions (book, status)",
|
| 138 |
"""CREATE TABLE IF NOT EXISTS wealth_marks (
|
| 139 |
beat_id bigint NOT NULL, book text NOT NULL, ts timestamptz NOT NULL DEFAULT now(),
|
fly_trader/execution/broker_live.py
CHANGED
|
@@ -45,6 +45,7 @@ from ..signer.policy import EXCLUDE_ROUTERS, PolicyError
|
|
| 45 |
from ..db.apilog import record_event
|
| 46 |
from ..db.connection import connect, transaction
|
| 47 |
from ..logging_setup import scrub
|
|
|
|
| 48 |
|
| 49 |
log = logging.getLogger(__name__)
|
| 50 |
|
|
@@ -73,9 +74,10 @@ class FillResult:
|
|
| 73 |
code: int | None
|
| 74 |
error: str | None
|
| 75 |
token_delta: int
|
| 76 |
-
lamports_delta: int
|
| 77 |
price_sol: float | None
|
| 78 |
attempts: int
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
def _int_or_none(v) -> int | None:
|
|
@@ -339,6 +341,10 @@ class LiveBroker:
|
|
| 339 |
log.warning("fill %s: snapshot deltas (%d, %d) differ from the transaction's (%d, %d); using the transaction",
|
| 340 |
signature[:12], lam, tok, exact[0], exact[1])
|
| 341 |
lam, tok = exact
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
# a confirmed sell that moved the tokens is a fill even at lam <= 0 (a rugged token's dust proceeds < fees)
|
| 343 |
verified = (lam < 0 and tok > 0) if side == "buy" else (tok < 0 and (lam > 0 or chain_status == "confirmed"))
|
| 344 |
jup_status = (ex or {}).get("status")
|
|
@@ -353,7 +359,7 @@ class LiveBroker:
|
|
| 353 |
order.get("platformFee"), verified_by, pre, post)
|
| 354 |
log.info("fill %s: %s %s token_delta=%d lamports_delta=%d price_sol=%s (%s)",
|
| 355 |
fill_id, side, mint, tok, lam, price, verified_by)
|
| 356 |
-
return FillResult(True, order_id, fill_id, signature, chain_status, code, error, tok, lam, price, attempt), False
|
| 357 |
if jup_status == "Success" or chain_status == "confirmed":
|
| 358 |
tx_info = None
|
| 359 |
try:
|
|
@@ -506,6 +512,9 @@ def close_empty_atas(rpc=None, signer=None, url: str | None = None, batch: int =
|
|
| 506 |
with transaction(url) as conn:
|
| 507 |
conn.execute("INSERT INTO wallet_events (kind, pubkey, detail) VALUES ('ata_closed', %s, %s)",
|
| 508 |
(owner_s, Jsonb(detail)))
|
|
|
|
|
|
|
|
|
|
| 509 |
except Exception as e:
|
| 510 |
log.warning("wallet_events insert failed: %s", type(e).__name__)
|
| 511 |
if status == "confirmed":
|
|
|
|
| 45 |
from ..db.apilog import record_event
|
| 46 |
from ..db.connection import connect, transaction
|
| 47 |
from ..logging_setup import scrub
|
| 48 |
+
from . import ledger
|
| 49 |
|
| 50 |
log = logging.getLogger(__name__)
|
| 51 |
|
|
|
|
| 74 |
code: int | None
|
| 75 |
error: str | None
|
| 76 |
token_delta: int
|
| 77 |
+
lamports_delta: int # the trade's SOL: the wallet's change less what went into (or came back from) the mint's token account
|
| 78 |
price_sol: float | None
|
| 79 |
attempts: int
|
| 80 |
+
deposit_lamports: int = 0 # SOL the swap put into the mint's token account (its rent, a first buy) or took back out (< 0)
|
| 81 |
|
| 82 |
|
| 83 |
def _int_or_none(v) -> int | None:
|
|
|
|
| 341 |
log.warning("fill %s: snapshot deltas (%d, %d) differ from the transaction's (%d, %d); using the transaction",
|
| 342 |
signature[:12], lam, tok, exact[0], exact[1])
|
| 343 |
lam, tok = exact
|
| 344 |
+
# the rent a first buy pays to open the mint's token account (~0.0015 SOL) is a deposit the account's close returns,
|
| 345 |
+
# not the trade: the trade's SOL, its price and the position's cost leave it out (it is kept on the position)
|
| 346 |
+
dep = int(post.deposits.get(mint, 0)) - int(pre.deposits.get(mint, 0))
|
| 347 |
+
lam += dep
|
| 348 |
# a confirmed sell that moved the tokens is a fill even at lam <= 0 (a rugged token's dust proceeds < fees)
|
| 349 |
verified = (lam < 0 and tok > 0) if side == "buy" else (tok < 0 and (lam > 0 or chain_status == "confirmed"))
|
| 350 |
jup_status = (ex or {}).get("status")
|
|
|
|
| 359 |
order.get("platformFee"), verified_by, pre, post)
|
| 360 |
log.info("fill %s: %s %s token_delta=%d lamports_delta=%d price_sol=%s (%s)",
|
| 361 |
fill_id, side, mint, tok, lam, price, verified_by)
|
| 362 |
+
return FillResult(True, order_id, fill_id, signature, chain_status, code, error, tok, lam, price, attempt, dep), False
|
| 363 |
if jup_status == "Success" or chain_status == "confirmed":
|
| 364 |
tx_info = None
|
| 365 |
try:
|
|
|
|
| 512 |
with transaction(url) as conn:
|
| 513 |
conn.execute("INSERT INTO wallet_events (kind, pubkey, detail) VALUES ('ata_closed', %s, %s)",
|
| 514 |
(owner_s, Jsonb(detail)))
|
| 515 |
+
if status == "confirmed": # their rent is back in the wallet: no longer a deposit the books hold
|
| 516 |
+
for m in sorted({a["mint"] for a in chunk}):
|
| 517 |
+
ledger.refund_deposits(conn, "live", m)
|
| 518 |
except Exception as e:
|
| 519 |
log.warning("wallet_events insert failed: %s", type(e).__name__)
|
| 520 |
if status == "confirmed":
|
fly_trader/execution/ledger.py
CHANGED
|
@@ -67,7 +67,9 @@ def _chain_of(book: str) -> str:
|
|
| 67 |
|
| 68 |
def open_position(conn, *, book: str, mint: str, pool: str | None, qty_raw: int, cost_sol: float,
|
| 69 |
entry_price: float, decision_id: int | None, fees_sol: float, ts: datetime | None = None,
|
| 70 |
-
hold_s: float | None = None, strategy: str | None = None) -> int:
|
|
|
|
|
|
|
| 71 |
ts = ts or datetime.now(timezone.utc)
|
| 72 |
existing = conn.execute("SELECT id, qty, cost_sol, entry_price FROM positions WHERE book=%s AND mint=%s AND status='open'",
|
| 73 |
(book, mint)).fetchone()
|
|
@@ -75,18 +77,36 @@ def open_position(conn, *, book: str, mint: str, pool: str | None, qty_raw: int,
|
|
| 75 |
q0, c0 = int(existing["qty"]), float(existing["cost_sol"])
|
| 76 |
q1, c1 = q0 + qty_raw, c0 + cost_sol
|
| 77 |
p1 = (float(existing["entry_price"] or entry_price) * q0 + entry_price * qty_raw) / max(q1, 1)
|
| 78 |
-
conn.execute("UPDATE positions SET qty=%s, cost_sol=%s, entry_price=%s, fees_sol=fees_sol+%s, last_mark_price=%s, last_mark_ts=%s
|
| 79 |
-
(
|
|
|
|
| 80 |
return int(existing["id"])
|
| 81 |
row = conn.execute(
|
| 82 |
"""INSERT INTO positions (book, mint, pool, opened_at, entry_decision_id, qty, cost_sol, entry_price, peak_price,
|
| 83 |
-
last_mark_price, last_mark_ts, fees_sol, status, last_swap_ts, hold_s, strategy, chain)
|
| 84 |
-
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'open',%s,%s,%s,%s) RETURNING id""",
|
| 85 |
-
(book, mint, pool, ts, decision_id, qty_raw, cost_sol, entry_price, entry_price, entry_price, ts, fees_sol, ts, hold_s, strategy, _chain_of(book)
|
|
|
|
| 86 |
).fetchone()
|
| 87 |
return int(row["id"])
|
| 88 |
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
def realize_partial(conn, *, position_id: int, qty_raw: int, cost_part: float, proceeds_sol: float, fees_sol: float,
|
| 91 |
price: float, ts: datetime | None = None) -> float | None:
|
| 92 |
"""Shrink an open position by qty_raw / cost_part and book proceeds − cost_part into realized_sol.
|
|
|
|
| 67 |
|
| 68 |
def open_position(conn, *, book: str, mint: str, pool: str | None, qty_raw: int, cost_sol: float,
|
| 69 |
entry_price: float, decision_id: int | None, fees_sol: float, ts: datetime | None = None,
|
| 70 |
+
hold_s: float | None = None, strategy: str | None = None, deposit_sol: float = 0.0) -> int:
|
| 71 |
+
"""``deposit_sol``: what the buy put into the mint's token account (its rent, a first live buy), kept apart from
|
| 72 |
+
``cost_sol``: the account's close returns it (``refund_deposits``)."""
|
| 73 |
ts = ts or datetime.now(timezone.utc)
|
| 74 |
existing = conn.execute("SELECT id, qty, cost_sol, entry_price FROM positions WHERE book=%s AND mint=%s AND status='open'",
|
| 75 |
(book, mint)).fetchone()
|
|
|
|
| 77 |
q0, c0 = int(existing["qty"]), float(existing["cost_sol"])
|
| 78 |
q1, c1 = q0 + qty_raw, c0 + cost_sol
|
| 79 |
p1 = (float(existing["entry_price"] or entry_price) * q0 + entry_price * qty_raw) / max(q1, 1)
|
| 80 |
+
conn.execute("UPDATE positions SET qty=%s, cost_sol=%s, entry_price=%s, fees_sol=fees_sol+%s, last_mark_price=%s, last_mark_ts=%s, "
|
| 81 |
+
"deposit_sol = NULLIF(COALESCE(deposit_sol, 0) + %s, 0) WHERE id=%s",
|
| 82 |
+
(q1, c1, p1, fees_sol, entry_price, ts, deposit_sol, existing["id"]))
|
| 83 |
return int(existing["id"])
|
| 84 |
row = conn.execute(
|
| 85 |
"""INSERT INTO positions (book, mint, pool, opened_at, entry_decision_id, qty, cost_sol, entry_price, peak_price,
|
| 86 |
+
last_mark_price, last_mark_ts, fees_sol, status, last_swap_ts, hold_s, strategy, chain, deposit_sol)
|
| 87 |
+
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'open',%s,%s,%s,%s,%s) RETURNING id""",
|
| 88 |
+
(book, mint, pool, ts, decision_id, qty_raw, cost_sol, entry_price, entry_price, entry_price, ts, fees_sol, ts, hold_s, strategy, _chain_of(book),
|
| 89 |
+
deposit_sol or None),
|
| 90 |
).fetchone()
|
| 91 |
return int(row["id"])
|
| 92 |
|
| 93 |
|
| 94 |
+
def refund_deposits(conn, book: str, mint: str) -> float:
|
| 95 |
+
"""The mint's token-account deposits not yet returned (paid by the buy that opened the account, whichever position
|
| 96 |
+
of the mint closes it), marked returned; their total in SOL. 0 when none was booked apart (a position bought before
|
| 97 |
+
deposits were: its cost holds the rent, so the whole refund is its own)."""
|
| 98 |
+
rows = conn.execute("UPDATE positions SET deposit_refunded = true WHERE book = %s AND mint = %s AND COALESCE(deposit_sol, 0) > 0 AND NOT deposit_refunded "
|
| 99 |
+
"RETURNING deposit_sol", (book, mint)).fetchall()
|
| 100 |
+
return sum(float(r["deposit_sol"]) for r in rows)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def deposits_held(conn, book: str) -> float:
|
| 104 |
+
"""SOL the book's token accounts hold as deposits (their rent) until their close returns it: part of its wealth."""
|
| 105 |
+
r = conn.execute("SELECT COALESCE(sum(deposit_sol), 0) AS s FROM positions WHERE book = %s AND COALESCE(deposit_sol, 0) > 0 AND NOT deposit_refunded",
|
| 106 |
+
(book,)).fetchone()
|
| 107 |
+
return float(r["s"])
|
| 108 |
+
|
| 109 |
+
|
| 110 |
def realize_partial(conn, *, position_id: int, qty_raw: int, cost_part: float, proceeds_sol: float, fees_sol: float,
|
| 111 |
price: float, ts: datetime | None = None) -> float | None:
|
| 112 |
"""Shrink an open position by qty_raw / cost_part and book proceeds − cost_part into realized_sol.
|
fly_trader/execution/worker.py
CHANGED
|
@@ -163,10 +163,13 @@ class ExecutionWorker:
|
|
| 163 |
price = fr.price_sol or (cost / max(fr.token_delta / (10 ** req.decimals), 1e-12))
|
| 164 |
pid = ledger.open_position(conn, book="live", mint=req.mint, pool=req.pool, qty_raw=fr.token_delta,
|
| 165 |
cost_sol=cost, entry_price=price, decision_id=req.decision_id,
|
| 166 |
-
fees_sol=0.0, ts=ts, hold_s=req.hold_s, strategy=req.strategy
|
|
|
|
| 167 |
rails.record_notional(conn, cost, "entry")
|
| 168 |
return ExecResult(req, True, fr.fill_id, fr.signature, fr.token_delta, fr.lamports_delta, price, position_id=pid)
|
| 169 |
else:
|
|
|
|
|
|
|
| 170 |
proceeds = fr.lamports_delta / config.LAMPORTS_PER_SOL
|
| 171 |
price = fr.price_sol if fr.price_sol is not None else (proceeds / max(abs(fr.token_delta) / (10 ** req.decimals), 1e-12))
|
| 172 |
if req.forced_kind == "dead_bag_sweep": # a written-off position's tokens: what they fetch is that position's
|
|
@@ -208,11 +211,12 @@ class ExecutionWorker:
|
|
| 208 |
return not req.retry and req.forced_kind != "dead_bag_sweep"
|
| 209 |
|
| 210 |
def _close_account(self, req: ExecRequest, res: ExecResult) -> None:
|
| 211 |
-
"""The sale left the live book without this mint: close its empty token account
|
| 212 |
-
to the wallet:
|
| 213 |
-
close
|
| 214 |
-
|
| 215 |
-
the account stays
|
|
|
|
| 216 |
from ..vault import walletlock
|
| 217 |
with self.lock:
|
| 218 |
if self.inflight.get(req.mint) != ["sell"]:
|
|
@@ -224,8 +228,9 @@ class ExecutionWorker:
|
|
| 224 |
with walletlock.maybe_exclusive(config.VAULT_ENABLED):
|
| 225 |
got = self.broker.close_token_account(req.mint)
|
| 226 |
if got is not None and res.position_id is not None:
|
| 227 |
-
with transaction() as conn:
|
| 228 |
-
|
| 229 |
-
|
|
|
|
| 230 |
except Exception as e:
|
| 231 |
log.warning("closing the token account of %s after its sale failed (the sale stands): %s: %s", req.mint, type(e).__name__, e)
|
|
|
|
| 163 |
price = fr.price_sol or (cost / max(fr.token_delta / (10 ** req.decimals), 1e-12))
|
| 164 |
pid = ledger.open_position(conn, book="live", mint=req.mint, pool=req.pool, qty_raw=fr.token_delta,
|
| 165 |
cost_sol=cost, entry_price=price, decision_id=req.decision_id,
|
| 166 |
+
fees_sol=0.0, ts=ts, hold_s=req.hold_s, strategy=req.strategy,
|
| 167 |
+
deposit_sol=max(fr.deposit_lamports, 0) / config.LAMPORTS_PER_SOL)
|
| 168 |
rails.record_notional(conn, cost, "entry")
|
| 169 |
return ExecResult(req, True, fr.fill_id, fr.signature, fr.token_delta, fr.lamports_delta, price, position_id=pid)
|
| 170 |
else:
|
| 171 |
+
if fr.deposit_lamports < 0: # the swap closed the token account itself: its deposit is back
|
| 172 |
+
ledger.refund_deposits(conn, "live", req.mint)
|
| 173 |
proceeds = fr.lamports_delta / config.LAMPORTS_PER_SOL
|
| 174 |
price = fr.price_sol if fr.price_sol is not None else (proceeds / max(abs(fr.token_delta) / (10 ** req.decimals), 1e-12))
|
| 175 |
if req.forced_kind == "dead_bag_sweep": # a written-off position's tokens: what they fetch is that position's
|
|
|
|
| 211 |
return not req.retry and req.forced_kind != "dead_bag_sweep"
|
| 212 |
|
| 213 |
def _close_account(self, req: ExecRequest, res: ExecResult) -> None:
|
| 214 |
+
"""The sale left the live book without this mint: close its empty token account. The rent its first buy paid comes
|
| 215 |
+
back to the wallet: the deposit booked apart on that buy's position is returned (``ledger.refund_deposits``) and only
|
| 216 |
+
the close's fee is the closing position's; a position bought before deposits were booked apart had the rent in its
|
| 217 |
+
cost, so the whole refund is its own. Not while a buy is queued behind the sale (it needs the account) or the
|
| 218 |
+
wallet still holds the token. A failure here never touches the sale: it is logged and the account stays (and its
|
| 219 |
+
deposit counts in wealth) for ``close-empty-atas``."""
|
| 220 |
from ..vault import walletlock
|
| 221 |
with self.lock:
|
| 222 |
if self.inflight.get(req.mint) != ["sell"]:
|
|
|
|
| 228 |
with walletlock.maybe_exclusive(config.VAULT_ENABLED):
|
| 229 |
got = self.broker.close_token_account(req.mint)
|
| 230 |
if got is not None and res.position_id is not None:
|
| 231 |
+
with transaction() as conn: # the account's deposit is back, less the close's fee: only the fee is the position's
|
| 232 |
+
pnl = got / config.LAMPORTS_PER_SOL - ledger.refund_deposits(conn, "live", req.mint)
|
| 233 |
+
ledger.credit(conn, res.position_id, pnl)
|
| 234 |
+
res.realized_sol = (res.realized_sol or 0.0) + pnl
|
| 235 |
except Exception as e:
|
| 236 |
log.warning("closing the token account of %s after its sale failed (the sale stands): %s: %s", req.mint, type(e).__name__, e)
|
fly_trader/kalshi/fly_replay.py
CHANGED
|
@@ -71,8 +71,9 @@ def _governance(ds, order, ts_o, res_o, upto, scores, lines, line_log, bank, now
|
|
| 71 |
for c in range(1, lines.shape[0]):
|
| 72 |
lc = R._line_at(line_log, (c, s_), ts_o[p3], float(lines[c, s_]))
|
| 73 |
rp = gov.picks_returns(ds.ts[r3], ds.mint[r3], ds.hold_s[r3], np.nan_to_num(scores[c, s_, p3], nan=-np.inf), lc, y[r3])
|
| 74 |
-
s1 = scores[c, s_, p1]; has = np.isfinite(s1)
|
| 75 |
-
out = gov.check((rp[np.isfinite(rp)], rf[np.isfinite(rf)]), (s1[has], ds.outcome[r1][has]), drift[c]
|
|
|
|
| 76 |
counts[(c, s_)]["checks"] += 1
|
| 77 |
for t in out["triggers"]:
|
| 78 |
counts[(c, s_)][t] += 1
|
|
|
|
| 71 |
for c in range(1, lines.shape[0]):
|
| 72 |
lc = R._line_at(line_log, (c, s_), ts_o[p3], float(lines[c, s_]))
|
| 73 |
rp = gov.picks_returns(ds.ts[r3], ds.mint[r3], ds.hold_s[r3], np.nan_to_num(scores[c, s_, p3], nan=-np.inf), lc, y[r3])
|
| 74 |
+
s1 = scores[c, s_, p1]; f1 = scores[0, s_, p1]; has = np.isfinite(s1) & np.isfinite(f1)
|
| 75 |
+
out = gov.check((rp[np.isfinite(rp)], rf[np.isfinite(rf)]), (s1[has], ds.outcome[r1][has]), drift[c],
|
| 76 |
+
ic_frozen_rows=(f1[has], ds.outcome[r1][has])) # as live: against the frozen shadow
|
| 77 |
counts[(c, s_)]["checks"] += 1
|
| 78 |
for t in out["triggers"]:
|
| 79 |
counts[(c, s_)][t] += 1
|
fly_trader/kalshi/fly_session.py
CHANGED
|
@@ -21,8 +21,8 @@ minute's live part, ``after_minute`` the rest, once the live orders are out.
|
|
| 21 |
maker label the return of a bid one tick inside the ask when a later minute's ask reached it).
|
| 22 |
|
| 23 |
Hourly: learning statistics (``kalshi_fly_updates``), a snapshot of the plastic state (kind 'kalshi_fly_plastic'), the
|
| 24 |
-
rollback checks (train/fly_governance.py
|
| 25 |
-
re-bootstrap). State: ``data/brain/plastic_kalshi/state.pt``; commands ``ui_settings['kalshi_fly_command']``; status
|
| 26 |
``ui_settings['kalshi_fly_status']``; activity for the 3D view in ``data/brain/activity_kalshi/``.
|
| 27 |
"""
|
| 28 |
from __future__ import annotations
|
|
@@ -141,6 +141,7 @@ class KalshiFlyBook:
|
|
| 141 |
self.lines = {"plastic": dict(self.fly.lines), "frozen": dict(self.fly.lines)}
|
| 142 |
self.sizing = {"plastic": {k: list(v) for k, v in self.fly.sizings.items()}, "frozen": {k: list(v) for k, v in self.fly.sizings.items()}}
|
| 143 |
self.applied_through = -math.inf; self.learning_frozen = False; self.checks_paused_until = {k: 0.0 for k in self.names}
|
|
|
|
| 144 |
self.last_day: int | None = None; self.last_hour: int | None = None
|
| 145 |
self.rollbacks: dict = {k: [] for k in self.names}; self.nu_set = False; self.hour_stats = self._empty_stats(); self.last_checks: dict = {}
|
| 146 |
s = self._read_state()
|
|
@@ -148,6 +149,7 @@ class KalshiFlyBook:
|
|
| 148 |
self.bank.load_state(s["bank"]); self.nu_set = True
|
| 149 |
self.lines, self.sizing = s["lines"], s["sizing"]
|
| 150 |
self.applied_through, self.learning_frozen, self.checks_paused_until = s["applied_through"], s["learning_frozen"], s["checks_paused_until"]
|
|
|
|
| 151 |
self.last_day, self.last_hour, self.rollbacks = s["last_day"], s["last_hour"], s["rollbacks"]
|
| 152 |
elif s:
|
| 153 |
with transaction() as conn:
|
|
@@ -164,7 +166,8 @@ class KalshiFlyBook:
|
|
| 164 |
|
| 165 |
def _state(self) -> dict:
|
| 166 |
return {"bootstrap_id": self.boot_id, "cfg": self.cfg, "bank": self.bank.state(), "lines": self.lines, "sizing": self.sizing, "applied_through": self.applied_through,
|
| 167 |
-
"learning_frozen": self.learning_frozen, "
|
|
|
|
| 168 |
"rollbacks": self.rollbacks, "saved_at": time.time()}
|
| 169 |
|
| 170 |
def _write_state(self) -> None:
|
|
@@ -267,7 +270,7 @@ class KalshiFlyBook:
|
|
| 267 |
self.last_day = day; self._calibrate(conn, ctx.m1_epoch)
|
| 268 |
drift = {k: float(self.bank.drift(j)[0]) if self.bank.learn.shape[0] > 1 else float(self.bank.drift()[0]) for j, k in enumerate(self.names)}
|
| 269 |
out.update({"lines": self.lines["plastic"], "frozen_lines": self.lines["frozen"], "arms": self.arms, "drift": max(drift.values()) if drift else 0.0,
|
| 270 |
-
"drift_by_strategy": drift, "pending": len(self.pending), "learned": learned, "learning_frozen": self.learning_frozen, "channels": self.cfg,
|
| 271 |
"bootstrap": self.boot_id, "device": str(self.fly.net.dev)})
|
| 272 |
hour = int(ctx.m1_epoch // 3600)
|
| 273 |
if self.last_hour is None:
|
|
@@ -386,12 +389,13 @@ class KalshiFlyBook:
|
|
| 386 |
self.pending.push(ss.keys, ss.ts, now + REARM_S, ss.y_dn, ss.u0, ss.k, ss.w, s=j)
|
| 387 |
rearmed = [(datetime.fromtimestamp(now + REARM_S, timezone.utc), datetime.fromtimestamp(t, timezone.utc), tk, side, name) for t, tk, side, name in sub.keys]
|
| 388 |
stats = {"n": int(keep.sum()), "unknown": int(sum(1 for k in out_keys if k[1] == "unknown")), "rearmed": rearmed, "keys": out_keys, "waiting": len(rearm)}
|
|
|
|
|
|
|
| 389 |
if keep.any():
|
| 390 |
-
|
| 391 |
-
if self.learning_frozen:
|
| 392 |
self.bank.decay_to(now)
|
| 393 |
else:
|
| 394 |
-
st = self.bank.update(
|
| 395 |
stats.update(mean_delta=st["mean_delta"][0], mean_abs_delta=st["mean_abs_delta"][0], step=st["step"][0], capped=bool(st["capped"][0]))
|
| 396 |
h = self.hour_stats; h["n"] += stats["n"]; h["sum_delta"] += st["mean_delta"][0] * stats["n"]; h["sum_abs"] += st["mean_abs_delta"][0] * stats["n"]
|
| 397 |
h["step"] += st["step"][0]; h["capped"] += int(st["capped"][0])
|
|
@@ -512,7 +516,7 @@ class KalshiFlyBook:
|
|
| 512 |
g = lambda c: np.array([r[c] for r in rows], dtype=np.float64)
|
| 513 |
rp = gov.picks_returns(ts, mint, hold, g("score"), g("line"), lab); rf = gov.picks_returns(ts, mint, hold, g("frozen_score"), g("frozen_line"), lab)
|
| 514 |
recent = np.array([r["resolved_at"].timestamp() for r in rows]) > now - gov.IC_HOURS * 3600.0
|
| 515 |
-
out[name] = gov.check((rp, rf), (g("score")[recent], lab[recent]), self._drift(j))
|
| 516 |
else:
|
| 517 |
out[name] = gov.check(None, None, self._drift(j))
|
| 518 |
return out
|
|
@@ -528,7 +532,7 @@ class KalshiFlyBook:
|
|
| 528 |
good = not any(c["triggers"] for c in checks.values())
|
| 529 |
sid = self._snapshot(conn, now, good, checks)
|
| 530 |
for name, c in checks.items():
|
| 531 |
-
if c["triggers"] and now >= self.checks_paused_until.get(name, 0.0) and not self.learning_frozen:
|
| 532 |
self._rollback(conn, now, c, sid, name=name)
|
| 533 |
self._prune(conn, now)
|
| 534 |
activity.prune(kalshi_activity_dir(), now - activity.KEEP_S)
|
|
@@ -558,7 +562,7 @@ class KalshiFlyBook:
|
|
| 558 |
if cmd == "pause":
|
| 559 |
self.learning_frozen = True; record_event("info", NAME, "learning paused by the operator")
|
| 560 |
elif cmd == "resume":
|
| 561 |
-
self.learning_frozen = False; self.rollbacks = {}; record_event("info", NAME, "learning resumed by the operator")
|
| 562 |
elif cmd == "rollback":
|
| 563 |
for j, name in enumerate(self.names):
|
| 564 |
self._rollback(conn, now, {"triggers": ["operator"], "drift": self._drift(j)}, None, count=False, name=name)
|
|
@@ -585,10 +589,11 @@ class KalshiFlyBook:
|
|
| 585 |
(f"{name}: " + ", ".join(checks["triggers"]), json.dumps(checks, default=str), from_sid, target["id"] if target is not None else None))
|
| 586 |
record_event("warning", NAME, f"{name} channel rolled back ({', '.join(checks['triggers'])})", {"to_snapshot": target["id"] if target is not None else "bootstrap (D = 0)", "checks": checks})
|
| 587 |
if count and len(self.rollbacks[name]) >= ROLLBACK_LIMIT:
|
| 588 |
-
self.
|
| 589 |
from . import pipeline
|
| 590 |
pipeline.request_run(fly=True, reason=f"{name}: {len(self.rollbacks[name])} rollbacks in 7 days")
|
| 591 |
-
record_event("error", NAME, f"learning frozen after {len(self.rollbacks[name])}
|
|
|
|
| 592 |
|
| 593 |
def _prune(self, conn, now: float) -> None:
|
| 594 |
rows = conn.execute("SELECT id, path, ts FROM brain_snapshots WHERE kind = %s AND ts < %s ORDER BY ts DESC",
|
|
|
|
| 21 |
maker label the return of a bid one tick inside the ask when a later minute's ask reached it).
|
| 22 |
|
| 23 |
Hourly: learning statistics (``kalshi_fly_updates``), a snapshot of the plastic state (kind 'kalshi_fly_plastic'), the
|
| 24 |
+
rollback checks (train/fly_governance.py: they judge what learning did, against the frozen shadow; a third rollback of a
|
| 25 |
+
strategy in 7 days freezes that strategy's learning and asks kalshi/pipeline.py for a re-bootstrap). State: ``data/brain/plastic_kalshi/state.pt``; commands ``ui_settings['kalshi_fly_command']``; status
|
| 26 |
``ui_settings['kalshi_fly_status']``; activity for the 3D view in ``data/brain/activity_kalshi/``.
|
| 27 |
"""
|
| 28 |
from __future__ import annotations
|
|
|
|
| 141 |
self.lines = {"plastic": dict(self.fly.lines), "frozen": dict(self.fly.lines)}
|
| 142 |
self.sizing = {"plastic": {k: list(v) for k, v in self.fly.sizings.items()}, "frozen": {k: list(v) for k, v in self.fly.sizings.items()}}
|
| 143 |
self.applied_through = -math.inf; self.learning_frozen = False; self.checks_paused_until = {k: 0.0 for k in self.names}
|
| 144 |
+
self.frozen: list[str] = [] # strategies whose learning the rollback limit froze (learning_frozen: the operator's pause, all)
|
| 145 |
self.last_day: int | None = None; self.last_hour: int | None = None
|
| 146 |
self.rollbacks: dict = {k: [] for k in self.names}; self.nu_set = False; self.hour_stats = self._empty_stats(); self.last_checks: dict = {}
|
| 147 |
s = self._read_state()
|
|
|
|
| 149 |
self.bank.load_state(s["bank"]); self.nu_set = True
|
| 150 |
self.lines, self.sizing = s["lines"], s["sizing"]
|
| 151 |
self.applied_through, self.learning_frozen, self.checks_paused_until = s["applied_through"], s["learning_frozen"], s["checks_paused_until"]
|
| 152 |
+
self.frozen = [k for k in s.get("frozen") or [] if k in self.names]
|
| 153 |
self.last_day, self.last_hour, self.rollbacks = s["last_day"], s["last_hour"], s["rollbacks"]
|
| 154 |
elif s:
|
| 155 |
with transaction() as conn:
|
|
|
|
| 166 |
|
| 167 |
def _state(self) -> dict:
|
| 168 |
return {"bootstrap_id": self.boot_id, "cfg": self.cfg, "bank": self.bank.state(), "lines": self.lines, "sizing": self.sizing, "applied_through": self.applied_through,
|
| 169 |
+
"learning_frozen": self.learning_frozen, "frozen": self.frozen, "checks_paused_until": self.checks_paused_until, "last_day": self.last_day,
|
| 170 |
+
"last_hour": self.last_hour,
|
| 171 |
"rollbacks": self.rollbacks, "saved_at": time.time()}
|
| 172 |
|
| 173 |
def _write_state(self) -> None:
|
|
|
|
| 270 |
self.last_day = day; self._calibrate(conn, ctx.m1_epoch)
|
| 271 |
drift = {k: float(self.bank.drift(j)[0]) if self.bank.learn.shape[0] > 1 else float(self.bank.drift()[0]) for j, k in enumerate(self.names)}
|
| 272 |
out.update({"lines": self.lines["plastic"], "frozen_lines": self.lines["frozen"], "arms": self.arms, "drift": max(drift.values()) if drift else 0.0,
|
| 273 |
+
"drift_by_strategy": drift, "pending": len(self.pending), "learned": learned, "learning_frozen": self.learning_frozen, "frozen_strategies": self.frozen, "channels": self.cfg,
|
| 274 |
"bootstrap": self.boot_id, "device": str(self.fly.net.dev)})
|
| 275 |
hour = int(ctx.m1_epoch // 3600)
|
| 276 |
if self.last_hour is None:
|
|
|
|
| 389 |
self.pending.push(ss.keys, ss.ts, now + REARM_S, ss.y_dn, ss.u0, ss.k, ss.w, s=j)
|
| 390 |
rearmed = [(datetime.fromtimestamp(now + REARM_S, timezone.utc), datetime.fromtimestamp(t, timezone.utc), tk, side, name) for t, tk, side, name in sub.keys]
|
| 391 |
stats = {"n": int(keep.sum()), "unknown": int(sum(1 for k in out_keys if k[1] == "unknown")), "rearmed": rearmed, "keys": out_keys, "waiting": len(rearm)}
|
| 392 |
+
held = [self.names.index(k) for k in self.frozen if k in self.names]
|
| 393 |
+
learn = keep & ~np.isin(np.asarray(tags.s), held) if held else keep # a strategy the rollback limit froze learns nothing
|
| 394 |
if keep.any():
|
| 395 |
+
if self.learning_frozen or not learn.any():
|
|
|
|
| 396 |
self.bank.decay_to(now)
|
| 397 |
else:
|
| 398 |
+
st = self.bank.update(tags.subset(learn), torch.tensor(labels[learn], dtype=torch.float32), now)
|
| 399 |
stats.update(mean_delta=st["mean_delta"][0], mean_abs_delta=st["mean_abs_delta"][0], step=st["step"][0], capped=bool(st["capped"][0]))
|
| 400 |
h = self.hour_stats; h["n"] += stats["n"]; h["sum_delta"] += st["mean_delta"][0] * stats["n"]; h["sum_abs"] += st["mean_abs_delta"][0] * stats["n"]
|
| 401 |
h["step"] += st["step"][0]; h["capped"] += int(st["capped"][0])
|
|
|
|
| 516 |
g = lambda c: np.array([r[c] for r in rows], dtype=np.float64)
|
| 517 |
rp = gov.picks_returns(ts, mint, hold, g("score"), g("line"), lab); rf = gov.picks_returns(ts, mint, hold, g("frozen_score"), g("frozen_line"), lab)
|
| 518 |
recent = np.array([r["resolved_at"].timestamp() for r in rows]) > now - gov.IC_HOURS * 3600.0
|
| 519 |
+
out[name] = gov.check((rp, rf), (g("score")[recent], lab[recent]), self._drift(j), ic_frozen_rows=(g("frozen_score")[recent], lab[recent]))
|
| 520 |
else:
|
| 521 |
out[name] = gov.check(None, None, self._drift(j))
|
| 522 |
return out
|
|
|
|
| 532 |
good = not any(c["triggers"] for c in checks.values())
|
| 533 |
sid = self._snapshot(conn, now, good, checks)
|
| 534 |
for name, c in checks.items():
|
| 535 |
+
if c["triggers"] and now >= self.checks_paused_until.get(name, 0.0) and not self.learning_frozen and name not in self.frozen:
|
| 536 |
self._rollback(conn, now, c, sid, name=name)
|
| 537 |
self._prune(conn, now)
|
| 538 |
activity.prune(kalshi_activity_dir(), now - activity.KEEP_S)
|
|
|
|
| 562 |
if cmd == "pause":
|
| 563 |
self.learning_frozen = True; record_event("info", NAME, "learning paused by the operator")
|
| 564 |
elif cmd == "resume":
|
| 565 |
+
self.learning_frozen = False; self.frozen = []; self.rollbacks = {}; record_event("info", NAME, "learning resumed by the operator")
|
| 566 |
elif cmd == "rollback":
|
| 567 |
for j, name in enumerate(self.names):
|
| 568 |
self._rollback(conn, now, {"triggers": ["operator"], "drift": self._drift(j)}, None, count=False, name=name)
|
|
|
|
| 589 |
(f"{name}: " + ", ".join(checks["triggers"]), json.dumps(checks, default=str), from_sid, target["id"] if target is not None else None))
|
| 590 |
record_event("warning", NAME, f"{name} channel rolled back ({', '.join(checks['triggers'])})", {"to_snapshot": target["id"] if target is not None else "bootstrap (D = 0)", "checks": checks})
|
| 591 |
if count and len(self.rollbacks[name]) >= ROLLBACK_LIMIT:
|
| 592 |
+
self.frozen = sorted(set(self.frozen) | {name}) # this strategy's learning only: the others keep learning
|
| 593 |
from . import pipeline
|
| 594 |
pipeline.request_run(fly=True, reason=f"{name}: {len(self.rollbacks[name])} rollbacks in 7 days")
|
| 595 |
+
record_event("error", NAME, f"{name} learning frozen after {len(self.rollbacks[name])} rollbacks in 7 days; re-bootstrap requested",
|
| 596 |
+
{"rollbacks": self.rollbacks, "frozen": self.frozen})
|
| 597 |
|
| 598 |
def _prune(self, conn, now: float) -> None:
|
| 599 |
rows = conn.execute("SELECT id, path, ts FROM brain_snapshots WHERE kind = %s AND ts < %s ORDER BY ts DESC",
|
fly_trader/train/fly_governance.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
| 1 |
"""The checks that roll the fly's plasticity back (live, hourly) — and are counted in the replay to measure false alarms.
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
- shadow: over the last ``SHADOW_DAYS`` days of resolved picks (each arm at its own line, one position per token), the
|
| 5 |
plastic fly's mean net return per trade is more than one standard error below its frozen shadow's (the bootstrap fly,
|
| 6 |
D = 0, scoring the same minutes), with at least ``SHADOW_MIN_TRADES`` trades on each side;
|
| 7 |
-
- ic:
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
- drift: ‖D‖_F / ‖w0‖_F above ``DRIFT_MAX``.
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
|
@@ -17,6 +22,8 @@ from .decisions import rank_corr, taken_idx
|
|
| 17 |
SHADOW_DAYS, SHADOW_MIN_TRADES = 3, 30
|
| 18 |
IC_HOURS, IC_MIN_ROWS, IC_SAMPLE = 24, 5000, 50_000
|
| 19 |
DRIFT_MAX = 0.5
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def shadow_check(r_plastic: np.ndarray, r_frozen: np.ndarray, min_trades: int = SHADOW_MIN_TRADES) -> tuple[bool, dict]:
|
|
@@ -50,8 +57,11 @@ def picks_returns(ts: np.ndarray, mint: np.ndarray, horizon_s: float, scores: np
|
|
| 50 |
return np.asarray(rets)[taken_idx(ts, mint, horizon_s, idx)]
|
| 51 |
|
| 52 |
|
| 53 |
-
def check(shadow: tuple[np.ndarray, np.ndarray] | None, ic_rows: tuple[np.ndarray, np.ndarray] | None, drift: float
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
| 55 |
out = {"triggers": [], "drift": float(drift)}
|
| 56 |
if shadow is not None:
|
| 57 |
t, d = shadow_check(*shadow); out["shadow"] = d
|
|
@@ -59,8 +69,13 @@ def check(shadow: tuple[np.ndarray, np.ndarray] | None, ic_rows: tuple[np.ndarra
|
|
| 59 |
out["triggers"].append("shadow")
|
| 60 |
if ic_rows is not None:
|
| 61 |
t, ic = ic_check(*ic_rows); out["ic"] = ic
|
|
|
|
|
|
|
|
|
|
| 62 |
if t:
|
| 63 |
out["triggers"].append("ic")
|
| 64 |
if drift_check(drift):
|
| 65 |
out["triggers"].append("drift")
|
|
|
|
|
|
|
| 66 |
return out
|
|
|
|
| 1 |
"""The checks that roll the fly's plasticity back (live, hourly) — and are counted in the replay to measure false alarms.
|
| 2 |
|
| 3 |
+
They judge what learning did, so a channel that has learned nothing (drift at most ``DRIFT_MIN``: a learning rate of 0,
|
| 4 |
+
or a memory decayed back to its bootstrap) trips none: there is nothing for a rollback to undo. Otherwise any one trips
|
| 5 |
+
a rollback:
|
| 6 |
- shadow: over the last ``SHADOW_DAYS`` days of resolved picks (each arm at its own line, one position per token), the
|
| 7 |
plastic fly's mean net return per trade is more than one standard error below its frozen shadow's (the bootstrap fly,
|
| 8 |
D = 0, scoring the same minutes), with at least ``SHADOW_MIN_TRADES`` trades on each side;
|
| 9 |
+
- ic: over the last ``IC_HOURS`` hours (at least ``IC_MIN_ROWS`` resolved minutes), the rank correlation of its scores
|
| 10 |
+
with the realized returns trails its frozen shadow's on the same rows by more than ``IC_MARGIN`` -- learning ranked
|
| 11 |
+
worse than its bootstrap. Not merely below 0: a fly that scores as its teacher would then be rolled back for the
|
| 12 |
+
teacher's own bad day (2026-10-11..13: ev, learning rate 0, rolled back three times at IC -0.10..-0.13, which froze
|
| 13 |
+
learning). Without the shadow's rows, below 0;
|
| 14 |
- drift: ‖D‖_F / ‖w0‖_F above ``DRIFT_MAX``.
|
| 15 |
"""
|
| 16 |
from __future__ import annotations
|
|
|
|
| 22 |
SHADOW_DAYS, SHADOW_MIN_TRADES = 3, 30
|
| 23 |
IC_HOURS, IC_MIN_ROWS, IC_SAMPLE = 24, 5000, 50_000
|
| 24 |
DRIFT_MAX = 0.5
|
| 25 |
+
DRIFT_MIN = 1e-3 # at most this far from its bootstrap, a channel has learned nothing a rollback could undo
|
| 26 |
+
IC_MARGIN = 0.02 # how far the plastic fly's IC must trail its frozen shadow's to count as learning's harm
|
| 27 |
|
| 28 |
|
| 29 |
def shadow_check(r_plastic: np.ndarray, r_frozen: np.ndarray, min_trades: int = SHADOW_MIN_TRADES) -> tuple[bool, dict]:
|
|
|
|
| 57 |
return np.asarray(rets)[taken_idx(ts, mint, horizon_s, idx)]
|
| 58 |
|
| 59 |
|
| 60 |
+
def check(shadow: tuple[np.ndarray, np.ndarray] | None, ic_rows: tuple[np.ndarray, np.ndarray] | None, drift: float,
|
| 61 |
+
ic_frozen_rows: tuple[np.ndarray, np.ndarray] | None = None) -> dict:
|
| 62 |
+
"""All three checks; ``triggers`` lists the ones that tripped. ``ic_frozen_rows``: the frozen shadow's scores and the
|
| 63 |
+
labels of the same rows as ``ic_rows``. A channel at or below ``DRIFT_MIN`` trips nothing (what would have tripped is
|
| 64 |
+
``not_counted``)."""
|
| 65 |
out = {"triggers": [], "drift": float(drift)}
|
| 66 |
if shadow is not None:
|
| 67 |
t, d = shadow_check(*shadow); out["shadow"] = d
|
|
|
|
| 69 |
out["triggers"].append("shadow")
|
| 70 |
if ic_rows is not None:
|
| 71 |
t, ic = ic_check(*ic_rows); out["ic"] = ic
|
| 72 |
+
if ic_frozen_rows is not None:
|
| 73 |
+
_, icf = ic_check(*ic_frozen_rows); out["ic_frozen"] = icf
|
| 74 |
+
t = ic is not None and icf is not None and ic < icf - IC_MARGIN
|
| 75 |
if t:
|
| 76 |
out["triggers"].append("ic")
|
| 77 |
if drift_check(drift):
|
| 78 |
out["triggers"].append("drift")
|
| 79 |
+
if float(drift) <= DRIFT_MIN and out["triggers"]:
|
| 80 |
+
out["not_counted"], out["triggers"] = out["triggers"], []
|
| 81 |
return out
|
fly_trader/train/fly_replay.py
CHANGED
|
@@ -324,10 +324,10 @@ def _governance(ds, order, ts_o, scores, lines, line_log, bank, now, lags, holds
|
|
| 324 |
sc3 = np.where(other3, np.nan, sc3)
|
| 325 |
sc3 = np.nan_to_num(sc3, nan=-np.inf)
|
| 326 |
rp = gov.picks_returns(ds.ts[r3], ds.mint[r3], H * 60.0, sc3, lc, y[r3])
|
| 327 |
-
s1 = scores[c, s_, a1:b1]; has = np.isfinite(s1)
|
| 328 |
if ci is not None:
|
| 329 |
has = has & (slots.cidx_o[a1:b1] == ci)
|
| 330 |
-
out = gov.check((rp, rf), (s1[has], y[r1][has]), drift[c])
|
| 331 |
counts[(c, s_)]["checks"] += 1
|
| 332 |
for t in out["triggers"]:
|
| 333 |
counts[(c, s_)][t] += 1
|
|
|
|
| 324 |
sc3 = np.where(other3, np.nan, sc3)
|
| 325 |
sc3 = np.nan_to_num(sc3, nan=-np.inf)
|
| 326 |
rp = gov.picks_returns(ds.ts[r3], ds.mint[r3], H * 60.0, sc3, lc, y[r3])
|
| 327 |
+
s1 = scores[c, s_, a1:b1]; f1 = scores[0, s_, a1:b1]; has = np.isfinite(s1) & np.isfinite(f1)
|
| 328 |
if ci is not None:
|
| 329 |
has = has & (slots.cidx_o[a1:b1] == ci)
|
| 330 |
+
out = gov.check((rp, rf), (s1[has], y[r1][has]), drift[c], ic_frozen_rows=(f1[has], y[r1][has])) # as live: against the frozen shadow
|
| 331 |
counts[(c, s_)]["checks"] += 1
|
| 332 |
for t in out["triggers"]:
|
| 333 |
counts[(c, s_)][t] += 1
|