Download fly_trader/execution/ledger.py from bryceweiner/fly-trader: direct link, hf CLI and curl.
- Browser
- Download file 12.5 kB
-
https://huggingface.co/bryceweiner/fly-trader/resolve/main/fly_trader/execution/ledger.py
- Command line
-
hf download hf://bryceweiner/fly-trader/fly_trader/execution/ledger.py
-
curl -L -o ledger.py https://huggingface.co/bryceweiner/fly-trader/resolve/main/fly_trader/execution/ledger.py
12.5 kB
| """Positions and fills bookkeeping per book (live, paper_selector). | |
| Paper cash is derived, never stored: cash = CAPITAL_SOL + Σ realized_sol(all) − Σ cost_sol(open). | |
| realized_sol accumulates per position: each partial sale books proceeds − the cost it retires (cost_sol | |
| shrinks by that part) and the close adds proceeds − the remaining cost, so an open position may carry | |
| realized P&L from partial sales. | |
| Live cash is the on-chain SOL balance. Every open/close writes positions rows; every fill writes a | |
| fills row (verified_by = 'balance_delta' for live, 'model' for paper). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| from datetime import datetime, timezone | |
| from .. import config | |
| log = logging.getLogger(__name__) | |
| def open_positions(conn, book: str) -> list[dict]: | |
| """Open positions of ``book`` with what trading them needs; ``decision_ts`` is the entry decision's minute (its ``ts``, | |
| the minute end a paper position opens at): a live position's hold runs from it, not from its fill 10–20 s later.""" | |
| rows = conn.execute( | |
| """SELECT p.*, COALESCE(t.decimals, CASE WHEN p.chain = 'rh' THEN 18 END) AS decimals, t.graduated_at, | |
| COALESCE(wp.program_label, CASE WHEN p.chain = 'rh' THEN 'Pons v4' END) AS program_label, | |
| COALESCE(p.hold_s, (d.detail->>'hold_s')::float) AS hold_s, COALESCE(p.strategy, d.detail->>'strategy') AS strategy, | |
| d.ts AS decision_ts | |
| FROM positions p LEFT JOIN tokens t ON t.mint = p.mint | |
| LEFT JOIN watch_pools wp ON wp.pool = p.pool | |
| LEFT JOIN decisions d ON d.id = p.entry_decision_id | |
| WHERE p.book = %s AND p.status = 'open' ORDER BY p.opened_at""", | |
| (book,), | |
| ).fetchall() | |
| return [dict(r) for r in rows] | |
| def paper_cash(conn, book: str) -> float: | |
| r = conn.execute( | |
| """SELECT COALESCE(sum(realized_sol), 0) AS realized, | |
| COALESCE(sum(cost_sol) FILTER (WHERE status = 'open'), 0) AS open_cost | |
| FROM positions WHERE book = %s""", | |
| (book,), | |
| ).fetchone() | |
| from ..markets import for_book | |
| return for_book(book).capital() + float(r["realized"]) - float(r["open_cost"]) # the book's own market's capital (SOL / ETH) | |
| def record_fill(conn, *, order_id: int | None, book: str, mint: str, side: str, token_delta: int, | |
| sol_delta_lamports: int, price_sol: float, fee_lamports: int, verified_by: str, | |
| signature: str | None = None, slot: int | None = None, ts: datetime | None = None, | |
| pre: dict | None = None, post: dict | None = None) -> int: | |
| row = conn.execute( | |
| """INSERT INTO fills (order_id, book, ts, signature, slot, mint, side, token_delta, sol_delta_lamports, | |
| price_sol, fee_lamports, verified_by, pre_snapshot, post_snapshot) | |
| VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING id""", | |
| (order_id, book, ts or datetime.now(timezone.utc), signature, slot, mint, side, token_delta, sol_delta_lamports, | |
| price_sol, fee_lamports, verified_by, json.dumps(pre) if pre else None, json.dumps(post) if post else None), | |
| ).fetchone() | |
| return int(row["id"]) | |
| def _chain_of(book: str) -> str: | |
| from ..markets import for_book | |
| return for_book(book).chain | |
| def open_position(conn, *, book: str, mint: str, pool: str | None, qty_raw: int, cost_sol: float, | |
| entry_price: float, decision_id: int | None, fees_sol: float, ts: datetime | None = None, | |
| hold_s: float | None = None, strategy: str | None = None, deposit_sol: float = 0.0) -> int: | |
| """``deposit_sol``: what the buy put into the mint's token account (its rent, a first live buy), kept apart from | |
| ``cost_sol``: the account's close returns it (``refund_deposits``).""" | |
| ts = ts or datetime.now(timezone.utc) | |
| existing = conn.execute("SELECT id, qty, cost_sol, entry_price FROM positions WHERE book=%s AND mint=%s AND status='open'", | |
| (book, mint)).fetchone() | |
| if existing: # add to an open position: average the entry | |
| q0, c0 = int(existing["qty"]), float(existing["cost_sol"]) | |
| q1, c1 = q0 + qty_raw, c0 + cost_sol | |
| p1 = (float(existing["entry_price"] or entry_price) * q0 + entry_price * qty_raw) / max(q1, 1) | |
| 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, " | |
| "deposit_sol = NULLIF(COALESCE(deposit_sol, 0) + %s, 0) WHERE id=%s", | |
| (q1, c1, p1, fees_sol, entry_price, ts, deposit_sol, existing["id"])) | |
| return int(existing["id"]) | |
| row = conn.execute( | |
| """INSERT INTO positions (book, mint, pool, opened_at, entry_decision_id, qty, cost_sol, entry_price, peak_price, | |
| last_mark_price, last_mark_ts, fees_sol, status, last_swap_ts, hold_s, strategy, chain, deposit_sol) | |
| VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'open',%s,%s,%s,%s,%s) RETURNING id""", | |
| (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), | |
| deposit_sol or None), | |
| ).fetchone() | |
| return int(row["id"]) | |
| def refund_deposits(conn, book: str, mint: str) -> float: | |
| """The mint's token-account deposits not yet returned (paid by the buy that opened the account, whichever position | |
| of the mint closes it), marked returned; their total in SOL. 0 when none was booked apart (a position bought before | |
| deposits were: its cost holds the rent, so the whole refund is its own).""" | |
| 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 " | |
| "RETURNING deposit_sol", (book, mint)).fetchall() | |
| return sum(float(r["deposit_sol"]) for r in rows) | |
| def deposits_held(conn, book: str) -> float: | |
| """SOL the book's token accounts hold as deposits (their rent) until their close returns it: part of its wealth.""" | |
| 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", | |
| (book,)).fetchone() | |
| return float(r["s"]) | |
| def book_account_close(conn, book: str, mint: str, lamports: int, closed: bool = True) -> tuple[float, int | None]: | |
| """A close of the mint's token account moved ``lamports`` into the wallet (its rent less its share of the fee; only | |
| the fee, < 0, when it failed and ``closed`` is False): the deposit the buy that opened it booked apart comes back | |
| (``refund_deposits``) and the rest -- the fee, or the whole rent of an account opened before deposits were booked | |
| apart -- is the realized P&L of the mint's latest closed position, whose sale emptied it. Returns (SOL credited, that | |
| position); no position when the book never held the mint (an account opened for the wallet by someone else).""" | |
| rest = lamports / config.LAMPORTS_PER_SOL - (refund_deposits(conn, book, mint) if closed else 0.0) | |
| r = conn.execute("SELECT id FROM positions WHERE book = %s AND mint = %s AND status = 'closed' ORDER BY closed_at DESC NULLS LAST, id DESC LIMIT 1", | |
| (book, mint)).fetchone() | |
| if r is None: | |
| return rest, None | |
| credit(conn, int(r["id"]), rest) | |
| return rest, int(r["id"]) | |
| def realize_partial(conn, *, position_id: int, qty_raw: int, cost_part: float, proceeds_sol: float, fees_sol: float, | |
| price: float, ts: datetime | None = None) -> float | None: | |
| """Shrink an open position by qty_raw / cost_part and book proceeds − cost_part into realized_sol. | |
| Returns this sale's realized P&L, or None when the position is no longer open (nothing booked).""" | |
| ts = ts or datetime.now(timezone.utc) | |
| realized = proceeds_sol - cost_part | |
| r = conn.execute( | |
| """UPDATE positions SET qty = qty - %s, cost_sol = cost_sol - %s, realized_sol = COALESCE(realized_sol, 0) + %s, | |
| fees_sol = fees_sol + %s, last_mark_price=%s, last_mark_ts=%s WHERE id=%s AND status='open' RETURNING id""", | |
| (qty_raw, cost_part, realized, fees_sol, price, ts, position_id)).fetchone() | |
| return realized if r else None | |
| def close_position(conn, *, position_id: int, exit_price: float, proceeds_sol: float, fees_sol: float, | |
| decision_id: int | None, forced_kind: str | None, ts: datetime | None = None) -> float | None: | |
| """Close an open position; realized_sol accumulates (earlier partial sales + proceeds − remaining cost). | |
| Returns this sale's realized P&L, or None when the position was not open (already closed: nothing booked).""" | |
| ts = ts or datetime.now(timezone.utc) | |
| r = conn.execute( | |
| """UPDATE positions SET status='closed', closed_at=%s, exit_decision_id=%s, exit_price=%s, | |
| realized_sol=COALESCE(realized_sol, 0) + %s - cost_sol, fees_sol=fees_sol+%s, forced_exit_kind=%s, | |
| last_mark_price=%s, last_mark_ts=%s WHERE id=%s AND status='open' RETURNING %s - cost_sol AS realized""", | |
| (ts, decision_id, exit_price, proceeds_sol, fees_sol, forced_kind, exit_price, ts, position_id, proceeds_sol), | |
| ).fetchone() | |
| if r is None: | |
| log.warning("close_position: position %s is not open; nothing booked", position_id) | |
| return None | |
| return float(r["realized"]) | |
| def credit(conn, position_id: int, sol: float) -> None: | |
| """Book ``sol`` that reached the wallet after a position closed (its token account's rent refunded, a dead-bag's tokens | |
| swept) onto that position's realized P&L.""" | |
| conn.execute("UPDATE positions SET realized_sol = COALESCE(realized_sol, 0) + %s WHERE id = %s", (sol, position_id)) | |
| def last_dead_bag(conn, book: str, mint: str) -> int | None: | |
| """The most recent position of ``mint`` written off as a dead-bag in ``book`` (its later sweeps' proceeds are its).""" | |
| r = conn.execute("SELECT id FROM positions WHERE book = %s AND mint = %s AND forced_exit_kind = 'dead_bag' ORDER BY closed_at DESC NULLS LAST, id DESC LIMIT 1", | |
| (book, mint)).fetchone() | |
| return int(r["id"]) if r else None | |
| def mark_positions(conn, book: str, prices: dict[str, float], last_swaps: dict[str, float], ts: datetime, | |
| dt_s: float = 0.0) -> None: | |
| """Mark to the last price; track the peak; accrue satiety = Σ max(unrealized return, 0) × dt while in profit.""" | |
| rows = conn.execute("SELECT id, mint, peak_price, entry_price, satiety FROM positions WHERE book=%s AND status='open'", (book,)).fetchall() | |
| for r in rows: | |
| p = prices.get(r["mint"]) | |
| if p is None: | |
| continue | |
| peak = max(float(r["peak_price"] or 0.0), p) | |
| entry = float(r["entry_price"] or 0.0) | |
| u = (p / entry - 1.0) if entry > 0 else 0.0 | |
| sat = float(r["satiety"] or 0.0) + max(u, 0.0) * dt_s | |
| ls = last_swaps.get(r["mint"]) | |
| conn.execute("UPDATE positions SET last_mark_price=%s, last_mark_ts=%s, peak_price=%s, satiety=%s, " | |
| "last_swap_ts=COALESCE(%s, last_swap_ts) WHERE id=%s", | |
| (p, ts, peak, sat, datetime.fromtimestamp(ls, tz=timezone.utc) if ls else None, r["id"])) | |
| def verify_fills() -> None: | |
| """Reconcile live positions against on-chain token balances; prints mismatches, writes wallet_events.""" | |
| from ..chain.rpc import HttpSolanaRpc | |
| from ..db.connection import transaction | |
| from ..signer import client | |
| rpc = HttpSolanaRpc() | |
| pub = client.get().call("pubkeys")["trading"] | |
| accounts = rpc.get_token_accounts_by_owner(pub) | |
| onchain = {a["mint"]: int(a["amount"]) for a in accounts} | |
| with transaction() as conn: | |
| pos = open_positions(conn, "live") | |
| mismatches = [] | |
| for p in pos: | |
| have = onchain.get(p["mint"], 0) | |
| if abs(have - int(p["qty"])) > max(1, int(p["qty"]) // 1000): | |
| mismatches.append({"mint": p["mint"], "db_qty": int(p["qty"]), "onchain": have}) | |
| stray = [m for m, a in onchain.items() if a > 0 and m not in {p["mint"] for p in pos} and m != config.WSOL_MINT] | |
| conn.execute("INSERT INTO wallet_events (kind, pubkey, detail) VALUES ('verify_fills', %s, %s)", | |
| (pub, json.dumps({"mismatches": mismatches, "stray_mints": stray, "n_positions": len(pos)}))) | |
| print(json.dumps({"positions": len(pos), "mismatches": mismatches, "stray_mints": stray}, indent=1)) | |