File size: 12,055 Bytes
0f038be
 
 
 
 
411abf3
0f038be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411abf3
0f038be
 
 
 
 
 
 
 
 
 
 
411abf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c0b4bb4
 
411abf3
 
 
 
 
 
 
 
 
c0b4bb4
 
411abf3
 
 
 
0f038be
 
 
 
 
 
 
 
 
 
 
 
 
 
411abf3
0f038be
 
 
 
 
 
411abf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""The minute engine: readiness on the stream, minute aggregation, the full feature vector built once per mint whatever
the number of books, the training eligibility rules, and each book trading in its own savepoint."""
import json
import math
import time
from datetime import datetime, timedelta, timezone

import numpy as np

from fly_trader.agent import minute_engine as me
from fly_trader.db.connection import transaction
from fly_trader.train.decisions import X_COLS


def _engine(books=()):
    return me.MinuteEngine(list(books))


def test_ready_through_waits_for_the_stream(db_conn):
    s = _engine()
    m0 = math.floor(time.time() / 60) * 60 - 60
    with db_conn.cursor() as cur:
        cur.execute("INSERT INTO ui_settings (key, value) VALUES ('pumpstream_status', %s) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",
                    (json.dumps({"flushed_through": datetime.fromtimestamp(m0 - 60, timezone.utc).isoformat()}),))
    db_conn.commit()
    assert s.ready_through(m0 + 60) == m0                        # minute m0 is not written yet: it waits, however late
    with db_conn.cursor() as cur:
        cur.execute("UPDATE ui_settings SET value = %s WHERE key = 'pumpstream_status'", (json.dumps({"flushed_through": datetime.fromtimestamp(m0, timezone.utc).isoformat()}),))
    db_conn.commit()
    assert s.ready_through(m0 + 60) == m0 + 60


def test_scale_break_blocks_the_mint_like_training(db_conn):
    s = _engine(); t = 1_800_000_000.0
    a = {"open": 1.0, "high": 1.0, "low": 1.0, "close": 1.0, "buy": 1.0, "sell": 0.0, "nb": 1, "ns": 0, "n_traders": 1, "resq": 50.0, "pool": "P", "program_label": "Pump.fun Amm"}
    assert s._features(db_conn, "Bkpump", a, t)[1]["broken"] is False
    assert s._features(db_conn, "Bkpump", {**a, "close": 80.0}, t + 60)[1]["broken"] is True
    assert s._features(db_conn, "Bkpump", a, t + 120)[1]["broken"] is True          # from that minute on, as in decisions.build


def test_aggregate_and_full_feature_vector(db_conn):
    s = _engine()
    m0 = datetime(2026, 9, 1, 12, 0, tzinfo=timezone.utc)
    with db_conn.cursor() as cur:
        cur.execute("DELETE FROM pump_minutes WHERE mint = 'Zpump'")
        cur.execute("INSERT INTO pump_minutes (mint, ts, pool_id, open, high, low, close, buy_sol, sell_sol, n_buys, n_sells, n_traders, resq_sol) VALUES "
                    "('Zpump', %s, 'POOL', 1.0, 1.2, 0.9, 1.1, 6.0, 2.0, 3, 1, 4, 55.0)", (m0,))
    db_conn.commit()
    agg = s._aggregate(db_conn, m0, m0.replace(minute=1))
    assert list(agg) == ["Zpump"] and agg["Zpump"]["resq"] == 55.0 and agg["Zpump"]["n_traders"] == 4
    x, info = s._features(db_conn, "Zpump", agg["Zpump"], m0.timestamp() + 60)
    assert x.shape == (len(X_COLS),) and np.isfinite(x).all()
    assert info["resq"] == 55.0 and info["price"] == 1.1 and info["open"] == 1.0 and info["ec"] > 0
    assert x[X_COLS.index("n_trades_1m")] == 4.0 and x[X_COLS.index("traders_15m")] == 4.0
    assert x[X_COLS.index("meta_known")] == 0.0 and x[X_COLS.index("age_known")] == 0.0


def test_graduation_time_comes_from_corpus_meta(db_conn):
    s = _engine()
    g = datetime(2026, 9, 1, 10, 0, tzinfo=timezone.utc)
    with db_conn.cursor() as cur:
        cur.execute("DELETE FROM corpus_meta WHERE mint = 'Ypump'")
        cur.execute("INSERT INTO corpus_meta (mint, graduated_at, ttg_min, dev_sol) VALUES ('Ypump', %s, 45.0, 1.5)", (g,))
    db_conn.commit()
    a = {"open": 1.0, "high": 1.0, "low": 1.0, "close": 1.0, "buy": 6.0, "sell": 0.0, "nb": 2, "ns": 0, "n_traders": 2, "resq": 60.0, "pool": "P", "program_label": "Pump.fun Amm"}
    x, info = s._features(db_conn, "Ypump", a, g.timestamp() + 2 * 3600)
    assert info["age_h"] == 2.0
    assert x[X_COLS.index("age_known")] == 1.0 and x[X_COLS.index("meta_known")] == 1.0
    assert x[X_COLS.index("ttg_min")] == 45.0 and x[X_COLS.index("dev_sol")] == 1.5
    with db_conn.cursor() as cur:
        cur.execute("DELETE FROM corpus_meta WHERE mint = 'Ypump'")
    db_conn.commit()


class _Book:
    def __init__(self, name, fail=False):
        self.name, self.fail, self.done, self.seen = name, fail, False, None

    def on_bars(self, t, bars):
        self.bars = bars

    def on_minute(self, ctx):
        ctx.conn.execute("INSERT INTO events (level, source, message) VALUES ('info', 'engine_test', %s)", (self.name,))
        self.seen = (ctx.X.shape, list(ctx.mints)); self.lag = ctx.lag_s
        if self.fail:
            raise RuntimeError("boom")
        return {"ok": True}

    def open_mints(self, conn):
        return set()

    def finish(self):
        pass


class _LiveBook(_Book):
    """A book that trades live money: its order goes out, committed, in the live phase."""
    LIVE = True

    def __init__(self, name, calls):
        super().__init__(name); self.calls = calls

    def on_minute(self, ctx):
        self.calls.append(("live", self.name))
        ctx.conn.execute("INSERT INTO events (level, source, message) VALUES ('info', 'engine_test', 'live order')")
        return {"live": True}

    def after_minute(self, ctx):
        self.calls.append(("after", self.name))
        return {"ok": True}


class _PaperBook(_Book):
    def __init__(self, name, calls):
        super().__init__(name); self.calls = calls

    def on_minute(self, ctx):
        self.calls.append(("paper", self.name))
        with transaction() as other:                     # another connection: the live phase is committed already
            self.saw = other.execute("SELECT count(*) AS n FROM events WHERE source = 'engine_test' AND message = 'live order'").fetchone()["n"]
        return {"ok": True}


def test_live_books_trade_and_commit_before_any_paper_book(db_conn, monkeypatch):
    """Live money first: the live book's minute runs and commits before any other book's, whatever the books' order;
    its paper trades, learning and bookkeeping (after_minute) come after the paper books."""
    m0 = datetime(2026, 9, 3, 12, 0, tzinfo=timezone.utc); calls = []
    with transaction() as conn:
        conn.execute("DELETE FROM events WHERE source = 'engine_test'")
    paper, live = _PaperBook("selector", calls), _LiveBook("fly", calls)
    e = _engine([paper, live]); monkeypatch.setattr(e, "stream_fresh", lambda m0_epoch: True)
    out = e.run_minute(m0.timestamp() + 60)
    with transaction() as conn:
        conn.execute("DELETE FROM events WHERE source = 'engine_test'")
    assert calls == [("live", "fly"), ("paper", "selector"), ("after", "fly")] and paper.saw == 1
    assert out["fly"] == {"ok": True} and out["selector"] == {"ok": True}


def test_every_chains_live_phase_runs_before_any_chains_paper_phase(monkeypatch):
    """Live money first across chains: older minutes are caught up whole, then every chain's newest minute's live phase
    (the chains at once), and only then any chain's paper phase."""
    calls, engines = [], []
    for name in ("sol", "rh"):
        e = _engine([_Book(f"book_{name}")]); e.last_minute = 600.0
        monkeypatch.setattr(e, "ready_through", lambda m1: 720.0)
        monkeypatch.setattr(e, "run_minute", lambda m, trade=True, n=name: calls.append((n, "whole", m)) or {})
        monkeypatch.setattr(e, "live_phase", lambda m, trade=True, n=name: calls.append((n, "live", m)) or (None, {"minute": m}))
        monkeypatch.setattr(e, "paper_phase", lambda ctx, out, n=name: calls.append((n, "paper")) or out)
        engines.append(e)
    ran = me.run_ready(engines, 720.0)
    assert calls[:2] == [("sol", "whole", 660.0), ("rh", "whole", 660.0)] and calls[4:] == [("sol", "paper"), ("rh", "paper")]
    assert sorted(calls[2:4]) == [("rh", "live", 720.0), ("sol", "live", 720.0)]       # side by side, either first
    assert [(m, st) for _, m, st in ran] == [(720, {"minute": 720.0})] * 2 and all(e.last_minute == 720 for e in engines)
    assert me.run_ready(engines, 720.0) == []                                       # nothing new: the loop goes idle


def test_books_share_one_feature_pass_and_trade_in_their_own_savepoints(db_conn, monkeypatch):
    m0 = datetime(2026, 9, 2, 12, 0, tzinfo=timezone.utc)
    with db_conn.cursor() as cur:
        cur.execute("DELETE FROM pump_minutes WHERE mint = 'Wpump'"); cur.execute("DELETE FROM events WHERE source = 'engine_test'")
        cur.execute("INSERT INTO pump_minutes (mint, ts, pool_id, open, high, low, close, buy_sol, sell_sol, n_buys, n_sells, n_traders, resq_sol) VALUES "
                    "('Wpump', %s, 'POOL', 1.0, 1.2, 0.9, 1.1, 6.0, 2.0, 3, 1, 4, 55.0)", (m0,))
    db_conn.commit()
    bad, good = _Book("bad", fail=True), _Book("good")
    e = _engine([bad, good]); calls = []
    real = e._features
    monkeypatch.setattr(e, "_features", lambda *a: calls.append(a[1]) or real(*a))
    monkeypatch.setattr(e, "stream_fresh", lambda m0_epoch: True)
    out = e.run_minute(m0.timestamp() + 60)
    assert calls == ["Wpump"]                                                      # one feature pass per mint, two books
    assert good.lag > 86400                                                         # how long after its end the minute was traded (a replayed past one)
    assert good.seen == ((1, len(X_COLS)), ["Wpump"]) and good.bars["Wpump"][0] == m0.timestamp()
    assert "good" in out and "bad" not in out
    with transaction() as conn:
        left = [r["message"] for r in conn.execute("SELECT message FROM events WHERE source = 'engine_test'").fetchall()]
        conn.execute("DELETE FROM events WHERE source = 'engine_test'"); conn.execute("DELETE FROM pump_minutes WHERE mint = 'Wpump'")
    assert left == ["good"]                                                        # the failing book's writes were rolled back alone


def test_a_quiet_minute_is_charged_the_mints_last_known_fee(db_conn):
    """A held coin that did not trade this minute reports no fee: its sale and its mark pay the last one known (the
    engine's own minutes, else the stream's newest row with one; RH: else the pool's measured hook fee), not the cost
    model's generic fallback (on RH ~1 % against hook fees of 2.8-5 %)."""
    from fly_trader import markets
    m0 = datetime(2026, 9, 3, 12, 0, tzinfo=timezone.utc); s = _engine(); rh = me.MinuteEngine(market=markets.RH)
    tok, pool = "0x" + "f1" * 20, "0x" + "f2" * 32

    def ctx(engine, fees):
        return me.Minute(conn=db_conn, m0=m0, m1=m0, m1_epoch=m0.timestamp(), agg={}, X=np.zeros((0, len(X_COLS)), np.float32), infos=[], mints=[], bars={},
                         prices={}, resqs={}, fees=fees, fresh=True, trade=True, engine=engine)
    with db_conn.cursor() as cur:
        cur.execute("DELETE FROM pump_minutes WHERE mint IN ('Fpump', 'Npump')"); cur.execute("DELETE FROM rh_pools WHERE pool_id = %s", (pool,))
        cur.execute("INSERT INTO pump_minutes (mint, ts, pool_id, close, resq_sol, fee_rate) VALUES ('Fpump', %s, 'P', 1.0, 50.0, 0.0115), ('Fpump', %s, 'P', 1.0, 50.0, NULL)",
                    (m0, m0 + timedelta(minutes=1)))
        cur.execute("INSERT INTO rh_pools (pool_id, token, currency0, currency1, is_pons, hook_fee) VALUES (%s, %s, %s, %s, true, 0.029)", (pool, tok, "0x" + "00" * 20, tok))
    db_conn.commit()
    try:
        c = ctx(s, {"Gpump": 0.01, "Fpump": None})
        assert c.fee("Gpump") == 0.01 and c.fee("Fpump") == 0.0115 and c.fee("Npump") is None
        a = {"open": 1.0, "high": 1.0, "low": 1.0, "close": 1.0, "buy": 1.0, "sell": 0.0, "nb": 1, "ns": 0, "n_traders": 1, "resq": 50.0, "pool": "P",
             "program_label": "Pump.fun Amm", "fee_rate": 0.0095}
        s._features(db_conn, "Fpump", a, m0.timestamp() + 180)
        assert c.fee("Fpump") == 0.0095                                                # the engine's own newer minute
        assert ctx(rh, {}).fee(tok) == 0.029 and c.cost_label("Fpump", "Pump.fun Amm") == "Pump.fun Amm"
    finally:
        with db_conn.cursor() as cur:
            cur.execute("DELETE FROM pump_minutes WHERE mint IN ('Fpump', 'Npump')"); cur.execute("DELETE FROM rh_pools WHERE pool_id = %s", (pool,))
        db_conn.commit()