usde: l'USDC NON frutta sul nostro conto — misurato, e §6 di stasera era sbagliata
Domanda dell'operatore: "verifica se l'USDC frutta davvero sul nostro conto". Sembrava dover aspettare: il gateway non espone il Transaction Log (get_transaction_log, get_settlement_history, get_deposits, get_transfers, get_interest_history: tutti 404 — debito #11) e l'unica serie storica e' trades.db.equity, oraria ma arrotondata a 2 decimali e sporcata dal P&L non realizzato (+-$2-8/ora a posizioni aperte), dove $0,13/giorno sparisce. La fonte ufficiale Deribit ha spostato il problema dal rumore al calendario: i reward USDC si pagano UNA VOLTA AL MESE ("paid out as a single monthly payment early in the following month"), accrual alle 00:00 UTC sulla minima equity — non ogni giorno come l'USDE. Ecco perche' una sorveglianza giornaliera non poteva vederli. E un accredito mensile da qualche dollaro si vede benissimo anche a 2 decimali, purche' il libro sia FLAT: allora equity == balance == USDC e ogni scalino e' un accredito. MISURA, due confini di mese entrambi a libro flat: 29/06 -> 08/07 equity 598,06 costante, 237/237 ore ferme, 0 scalini (attesi $0,45) 31/07 -> 03/08 equity 596,92 costante per 4 giorni pieni (attesi $1,72) Il secondo e' decisivo: $1,72 contro una risoluzione di $0,01, 172x. Zero MISURATO, non zero sotto soglia. => La premessa originale del gate USDE-01 e' RESTAURATA: il guadagno di tenere USDE e' il tasso pieno ~4,1%, non lo spread 0,71 punti che avevo scritto poche ore fa. Sui $644: ~$26/anno, non ~$4,6. Cosa avevo sbagliato: ho letto `apr: 3.4` in public/get_currencies e l'ho trattato come una proprieta' del NOSTRO CONTO. Era un listino del VENUE. La "conferma indiretta" che invocavo provava che il listino e' reale per l'USDE e non diceva nulla sull'idoneita' dell'USDC. Un tasso pubblicato non e' un tasso incassato: si verifica sul conto — ed e' costato una query su una serie che avevamo gia'. Esclusione plausibile ma NON verificata: MiCA (USDC e' e-money token, USDe no); Deribit dice solo "eligibility is based on their location". Il fatto misurato e' lo zero, non il suo motivo. Nuovo: scripts/live/balance_watch.py + scripts/cron_balance.sh (orario al minuto :42, libero fra :25/:35/:47, sola lettura). Registra il BALANCE a 8 decimali per valuta — la serie che mancava — col conteggio dei fill dall'ultimo campione e il nozionale lordo, cosi' una finestra sporca si riconosce invece di essere mediata dentro. Etichetta PULITA le finestre a 0 fill e libro flat, dove il delta USDC E' l'interesse, e ne stampa l'APR implicita. Suite: 795 passati. L'ERROR di teardown e' il falso positivo dichiarato dalla guardia stessa: il cron :47 ha scritto un fill reale (ETH BUY $+9, 23:47:19) mentre la suite girava. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Campionamento ORARIO del BALANCE per valuta (USDC/USDE) — v2.0.0+.
|
||||
# Serve a rispondere a UNA domanda: l'APR USDC pubblicata da Deribit (3,4000%) e' accreditata
|
||||
# sul nostro conto? Il balance e' l'unica serie dove si vede: l'equity e' arrotondata a 2
|
||||
# decimali in trades.db e contiene il P&L non realizzato, e il gateway non espone il
|
||||
# Transaction Log (debito #11). Cadenza oraria per STRINGERE la finestra intorno all'accredito
|
||||
# (i reward USDE arrivano in un colpo verso le 12:00 UTC), non per campionare di piu'.
|
||||
# Minuto :42 = libero fra :25 (catena), :35 (usde), :47 (book); fuori dal minuto tondo
|
||||
# (rate-limit per-IP). SOLA LETTURA: nessun ordine, nessuna scrittura sul percorso soldi.
|
||||
export PATH="/home/adriano/.local/bin:$PATH"
|
||||
cd /opt/docker/PythagorasGoal || exit 1
|
||||
mkdir -p logs
|
||||
{
|
||||
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_balance ====="
|
||||
uv run python scripts/live/balance_watch.py || true
|
||||
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||
} >> logs/cron_balance.log 2>&1
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python
|
||||
"""balance_watch.py — registra il BALANCE esatto per valuta. SOLA LETTURA, nessun ordine.
|
||||
|
||||
PERCHE' ESISTE. Il 2026-08-30 e' emerso che Deribit pubblica una APR anche per l'USDC
|
||||
(3,4000%, contro 4,1071% dell'USDE): se fosse accreditata, il guadagno del passaggio a USDE
|
||||
non sarebbe il tasso ma lo SPREAD (0,71 punti), e la pista USDE varrebbe ~$4,6/anno invece di
|
||||
~$21. La domanda "l'USDC frutta davvero SUL NOSTRO conto?" non era rispondibile, perche':
|
||||
|
||||
* il gateway NON espone il Transaction Log (provati get_transaction_log,
|
||||
get_settlement_history, get_deposits, get_transfers, get_interest_history: tutti 404 —
|
||||
e' il debito #11, si ha solo cio' che il gateway espone);
|
||||
* l'unica serie storica del conto e' `trades.db.equity`, che e' EQUITY **arrotondata a 2
|
||||
decimali**: $0,13/giorno di interesse atteso ci sparisce dentro, e l'equity contiene
|
||||
comunque il P&L non realizzato, che vale mille volte tanto.
|
||||
|
||||
Il `balance` no: cambia solo per P&L REALIZZATO, fee, funding, movimenti di fondi e interessi.
|
||||
Su USDE (nessuna posizione denominata in USDE) e' gia' stato il rivelatore dei reward. Questa
|
||||
serie fa lo stesso per l'USDC — ed e' il dato che mancava, non un'analisi nuova.
|
||||
|
||||
COME SI SEPARA L'INTERESSE DAL RESTO (P7: il numero si etichetta con la sua configurazione):
|
||||
* P&L realizzato e fee -> noti ESATTAMENTE da `trades.db.fills`; ogni record porta il
|
||||
conteggio dei fill dall'ultimo campione, cosi' una finestra sporca si riconosce invece di
|
||||
essere mediata dentro;
|
||||
* funding -> proporzionale al NOZIONALE, quindi **zero quando il libro e' FLAT**. Il libro e'
|
||||
flat il 28% dei giorni: la lettura pulita arriva da sola, non va costruita;
|
||||
* interesse -> proporzionale al BALANCE.
|
||||
Una finestra con 0 fill e nozionale 0 misura l'interesse e basta. Le altre si etichettano.
|
||||
|
||||
MISURATO GIA' ORA (30/08, tre letture a 8 decimali durante le conversioni USDE): il balance USDC
|
||||
si e' mosso **esattamente** dell'importo degli ordini (1555.84720621 - 10.002 = 1545.84520621,
|
||||
poi -134.0268 = 1411.81840621). Su ~10 minuti con $568 di posizioni aperte: **ne' funding ne'
|
||||
interesse arrivano in continuo sul balance**. Se l'interesse esiste, e' un accredito PERIODICO —
|
||||
come i reward USDE, che compaiono in un colpo intorno alle 12:00 UTC. Percio' la cadenza e' ORARIA:
|
||||
serve a stringere la finestra intorno all'accredito, non a campionare di piu'.
|
||||
|
||||
uv run python scripts/live/balance_watch.py # un campione + lettura della serie
|
||||
uv run python scripts/live/balance_watch.py --report # solo la lettura, nessun campione
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live.deribit import DeribitRead # noqa: E402
|
||||
|
||||
STATE = ROOT / "data" / "live" / "balance_watch.jsonl"
|
||||
TRADES_DB = ROOT / "data" / "live" / "trades.db"
|
||||
VALUTE = ("USDC", "USDE")
|
||||
APR_ATTESE = {"USDC": 0.0340, "USDE": 0.041071} # public/get_currencies, lette il 2026-08-30
|
||||
|
||||
|
||||
def fills_da(ts_iso: str | None) -> int | None:
|
||||
"""Quanti fill dopo `ts_iso` (None se il DB non e' leggibile: non si inventa 0)."""
|
||||
if ts_iso is None:
|
||||
return None
|
||||
try:
|
||||
con = sqlite3.connect(f"file:{TRADES_DB}?mode=ro", uri=True)
|
||||
try:
|
||||
return con.execute("select count(*) from fills where ts_utc > ?", (ts_iso,)).fetchone()[0]
|
||||
finally:
|
||||
con.close()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def campiona(c: DeribitRead) -> dict:
|
||||
"""Un campione: balance per valuta (8 decimali), nozionale lordo, posizioni. Mai solleva."""
|
||||
rec = {"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "valute": {}}
|
||||
for cur in VALUTE:
|
||||
try:
|
||||
s = c.account_summary(cur)
|
||||
rec["valute"][cur] = {"balance": float(s["balance"]), "equity": float(s["equity"])}
|
||||
except Exception as e:
|
||||
rec["valute"][cur] = {"errore": f"{type(e).__name__}"}
|
||||
try:
|
||||
pos = c.positions("USDC")
|
||||
rec["nozionale_lordo"] = round(sum(abs(float(p.get("size") or 0)) for p in pos), 2)
|
||||
rec["n_posizioni"] = sum(1 for p in pos if abs(float(p.get("size") or 0)) > 1)
|
||||
except Exception:
|
||||
rec["nozionale_lordo"], rec["n_posizioni"] = None, None
|
||||
return rec
|
||||
|
||||
|
||||
def serie() -> list[dict]:
|
||||
if not STATE.exists():
|
||||
return []
|
||||
out = []
|
||||
for line in STATE.read_text().splitlines():
|
||||
try:
|
||||
out.append(json.loads(line))
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def leggi(righe: list[dict]) -> None:
|
||||
print(f" campioni: {len(righe)}"
|
||||
+ (f" da {righe[0]['ts']} a {righe[-1]['ts']}" if righe else ""))
|
||||
if len(righe) < 2:
|
||||
print(" → serve almeno un secondo campione: la prima differenza e' la prima misura.")
|
||||
return
|
||||
print(f"\n {'finestra':>34s} {'ore':>5s} {'Δ USDC':>13s} {'Δ USDE':>13s} {'fill':>5s} {'lordo':>9s} lettura")
|
||||
pulite = []
|
||||
for a, b in zip(righe, righe[1:]):
|
||||
ba, bb = a["valute"].get("USDC", {}), b["valute"].get("USDC", {})
|
||||
ea, eb = a["valute"].get("USDE", {}), b["valute"].get("USDE", {})
|
||||
if "balance" not in ba or "balance" not in bb:
|
||||
continue
|
||||
t0 = datetime.strptime(a["ts"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
||||
t1 = datetime.strptime(b["ts"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
||||
ore = (t1 - t0).total_seconds() / 3600
|
||||
d_usdc = bb["balance"] - ba["balance"]
|
||||
d_usde = (eb.get("balance", 0) - ea.get("balance", 0)) if "balance" in eb and "balance" in ea else None
|
||||
nf = b.get("fills_da_ultimo")
|
||||
lordo = b.get("nozionale_lordo")
|
||||
pulita = (nf == 0) and (lordo is not None and lordo < 1)
|
||||
nota = "PULITA (0 fill, libro flat) → il Δ USDC E' interesse" if pulita else (
|
||||
"sporca: fill nella finestra" if nf else
|
||||
"0 fill ma libro esposto → Δ = funding + interesse" if nf == 0 else "fill ignoti")
|
||||
if pulita and ore > 0:
|
||||
pulite.append((d_usdc, ba["balance"], ore))
|
||||
print(f" {a['ts'][5:16]}→{b['ts'][5:16]:>16s} {ore:5.1f} {d_usdc:+13.8f} "
|
||||
f"{(f'{d_usde:+13.8f}' if d_usde is not None else ' n/d')} "
|
||||
f"{('' if nf is None else nf):>5} {('' if lordo is None else f'${lordo:,.0f}'):>9} {nota}")
|
||||
print()
|
||||
if pulite:
|
||||
tot_d = sum(d for d, _, _ in pulite)
|
||||
tot_h = sum(h for _, _, h in pulite)
|
||||
bal = sum(b * h for _, b, h in pulite) / tot_h
|
||||
apr = (tot_d / bal) * (8760 / tot_h) if bal and tot_h else float("nan")
|
||||
print(f" ⇒ FINESTRE PULITE: {len(pulite)}, {tot_h:.1f} ore, Δ totale {tot_d:+.8f} USDC "
|
||||
f"su balance medio ${bal:,.2f}")
|
||||
print(f" APR implicita {apr:+.4%} (attesa da public/get_currencies: "
|
||||
f"{APR_ATTESE['USDC']:.4%})")
|
||||
print(f" verdetto: {'FRUTTA' if tot_d > 1e-6 else 'NON frutta (Δ nullo su finestra pulita)'}")
|
||||
else:
|
||||
print(" ⇒ nessuna finestra PULITA ancora (serve il libro FLAT e 0 fill: succede il 28% dei giorni).")
|
||||
print(" Nel frattempo le finestre a 0 fill misurano funding+interesse insieme.")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--report", action="store_true", help="solo lettura, nessun campione nuovo")
|
||||
a = ap.parse_args()
|
||||
righe = serie()
|
||||
if not a.report:
|
||||
rec = campiona(DeribitRead())
|
||||
rec["fills_da_ultimo"] = fills_da(righe[-1]["ts"] if righe else None)
|
||||
STATE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with STATE.open("a") as f:
|
||||
f.write(json.dumps(rec) + "\n")
|
||||
righe.append(rec)
|
||||
u = rec["valute"].get("USDC", {}); e = rec["valute"].get("USDE", {})
|
||||
print(f" campione {rec['ts']}: USDC balance {u.get('balance')} · USDE {e.get('balance')}"
|
||||
f" · lordo ${rec.get('nozionale_lordo')}")
|
||||
print("=" * 78)
|
||||
print(" BALANCE WATCH — l'USDC frutta sul nostro conto?")
|
||||
print("=" * 78)
|
||||
leggi(righe)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user