bryceweiner's picture
release 8 (48a468826e)
4afe541 verified
Raw History Blame Contribute Delete
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))