cinque punti della revisione 09/09: usde_watch a 1000 trade + finestra 24h, cuscino_watch (equity USDC, riconverte da solo), PREVDAY-01 kill/veto cablati, SCALA-01 al 2027-02-28, versamento 04/09 dichiarato
Decisi dall'operatore il 2026-09-10, verificati da revisione fable (15 segnalazioni, 12 applicate). - usde_watch: TRADE_LIMIT 1000 (count max Deribit) e trade_copertura(): lista troncata o ultima lettura oltre la finestra di 24h del gateway => somma NON leggibile, reward non attribuito (P12). Riga del 07/09 corretta nel log con campo `correzione` (400 USDE erano acquisti, non reward). - cuscino_watch.py (cron :53, monitor_health): equity USDC contro cuscino derivato da usde.cuscino_richiesto_usd (formula spostata in src/live/usde.py, usde_convert la importa); OK/PREAVVISO/SCOPERTO/BLIND; sotto zero lancia usde_convert --quota quota_ripristino(0.20)=0.64 --esegui con guardie (execution_enabled, depeg_warn, 1 tentativo/6h). Primo giro: PREAVVISO, +$26. - usde_convert: il tetto del venue vale solo in ACQUISTO (bloccava la vendita). - paper_prevday: GATE PREVDAY-01 cablato (2027-06-21, kill Sharpe giornaliero < -0,50 su >=180 g attivi, veto >=80% barre ricostruibili + divergenze non crescenti con soglia materiale). Oggi: +0,95 su 81 g, 1942/1943 ricostruibili, kill NON MATURO. - CLAUDE.md: arming 20/06 (TP01) / 23/06 (BOOK); piano EUR 5.000 chiuso col versamento 04/09 ($2.414,68 dal balance, dichiarato); SCALA-01 non prima del 2027-02-28 (A2 dal 01/09); PREVDAY-01 con data, kill, veto; §5.17 riparato con i limiti dichiarati (ratchet, slack zero a 0,70). - test: 1062 (+31): test_cuscino_watch (16), test_paper_prevday_gate (8), test_usde_watch (+5). Fixes #2 Fixes #3 Fixes #4 Fixes #5 Fixes #6 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""cuscino_watch: il cuscino USDC di regolamento e' sorvegliato, e sotto zero si riconverte.
|
||||
|
||||
Blindano: (a) il cuscino e' DERIVATO da config + book (P1), la stessa formula di usde_convert;
|
||||
(b) i tre stati e la soglia di preavviso; (c) la transizione non si consuma se l'invio fallisce
|
||||
(debito §5.2); (d) le guardie della riconversione automatica — interruttore del libro, depeg,
|
||||
cadenza — e la quota di ripristino che lascia margine."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live import usde as U # noqa: E402
|
||||
|
||||
|
||||
def _carica(nome):
|
||||
sp = importlib.util.spec_from_file_location(nome, ROOT / "scripts" / "live" / f"{nome}.py")
|
||||
m = importlib.util.module_from_spec(sp)
|
||||
sp.loader.exec_module(m)
|
||||
return m
|
||||
|
||||
|
||||
W = _carica("cuscino_watch")
|
||||
|
||||
|
||||
# ------------------------------ il cuscino e' derivato, non ridichiarato ------------------------------
|
||||
|
||||
def test_cuscino_derivato_da_config_e_book():
|
||||
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
||||
from src.live import book as B
|
||||
f, come = U.frazione_cuscino()
|
||||
assert abs(f - 2 * float(B.WEIGHT) * float(cfg["disaster_sl_pct"])) < 1e-12
|
||||
usd, _ = U.cuscino_richiesto_usd(1000.0)
|
||||
assert abs(usd - 1000.0 * f) < 1e-9
|
||||
assert "SL" in come
|
||||
|
||||
|
||||
def test_usde_convert_usa_la_stessa_formula():
|
||||
C = _carica("usde_convert")
|
||||
assert C.cuscino_richiesto_usd(4469.46) == U.cuscino_richiesto_usd(4469.46)
|
||||
|
||||
|
||||
def test_quota_ripristino_lascia_margine_sul_cuscino():
|
||||
f, _ = U.frazione_cuscino()
|
||||
q0, q10 = U.quota_ripristino(0.0), U.quota_ripristino(0.10)
|
||||
assert abs(q0 - (1 - f)) < 1e-12 # a margine zero e' il complemento del cuscino
|
||||
assert q10 < q0 # col margine si vende UN PO' di piu'
|
||||
# dopo la riconversione a q10 lo slack e' esattamente il 10% del cuscino
|
||||
E = 4000.0
|
||||
usdc_dopo = E * (1 - q10)
|
||||
assert abs((usdc_dopo - E * f) - 0.10 * E * f) < 1e-9
|
||||
|
||||
|
||||
# ------------------------------ i tre stati ------------------------------
|
||||
|
||||
def test_stati_ok_preavviso_scoperto():
|
||||
assert W.giudica(1500.0, 4000.0, 1200.0)["stato"] == "OK" # slack 300 > 120
|
||||
g = W.giudica(1300.0, 4000.0, 1200.0)
|
||||
assert g["stato"] == "PREAVVISO" and abs(g["slack"] - 100.0) < 1e-9 and g["preavviso_usd"] == 120.0
|
||||
assert W.giudica(1199.0, 4000.0, 1200.0)["stato"] == "SCOPERTO"
|
||||
|
||||
|
||||
def test_non_vedo_e_blind_non_ok():
|
||||
assert W.giudica(None, 4000.0, 1200.0)["stato"] == "BLIND"
|
||||
assert W.giudica(1500.0, None, None)["stato"] == "BLIND"
|
||||
|
||||
|
||||
# ------------------------------ transizione e trasporto ------------------------------
|
||||
|
||||
def test_ok_non_allerta_mai():
|
||||
assert not W.transizione(None, "OK") and not W.transizione({"stato": "SCOPERTO"}, "OK")
|
||||
|
||||
|
||||
def test_transizione_alla_prima_e_al_cambio_di_stato():
|
||||
assert W.transizione(None, "PREAVVISO")
|
||||
assert W.transizione({"stato": "OK", "allertato": None}, "PREAVVISO")
|
||||
assert W.transizione({"stato": "PREAVVISO", "allertato": True}, "SCOPERTO")
|
||||
assert not W.transizione({"stato": "PREAVVISO", "allertato": True}, "PREAVVISO")
|
||||
|
||||
|
||||
def test_un_invio_fallito_non_consuma_la_transizione():
|
||||
"""Debito §5.2: il marcatore 'gia' detto' e' l'esito dell'invio."""
|
||||
assert W.transizione({"stato": "SCOPERTO", "allertato": False}, "SCOPERTO")
|
||||
assert W.transizione({"stato": "SCOPERTO", "allertato": None}, "SCOPERTO")
|
||||
|
||||
|
||||
# ------------------------------ guardie della riconversione ------------------------------
|
||||
|
||||
def test_disarmare_il_libro_disarma_la_riconversione():
|
||||
ok, m = W.puo_riconvertire([], 0, False, 1.0, 0.99)
|
||||
assert not ok and "execution_enabled" in m
|
||||
|
||||
|
||||
def test_sotto_depeg_non_si_vende_al_buio():
|
||||
ok, m = W.puo_riconvertire([], 0, True, 0.985, 0.99)
|
||||
assert not ok and "depeg" in m
|
||||
ok, m = W.puo_riconvertire([], 0, True, None, 0.99)
|
||||
assert not ok and "leggibile" in m
|
||||
|
||||
|
||||
def test_un_tentativo_ogni_sei_ore():
|
||||
now = 1_789_000_000_000
|
||||
prev = [{"ts": now - 2 * 3_600_000, "riconversione": {"tentata": True}}]
|
||||
ok, m = W.puo_riconvertire(prev, now, True, 1.0, 0.99)
|
||||
assert not ok and "non si insiste" in m
|
||||
prev = [{"ts": now - 7 * 3_600_000, "riconversione": {"tentata": True}}]
|
||||
assert W.puo_riconvertire(prev, now, True, 1.0, 0.99)[0]
|
||||
# un record SENZA riconversione non conta come tentativo
|
||||
assert W.puo_riconvertire([{"ts": now - 1000, "riconversione": None}], now, True, 1.0, 0.99)[0]
|
||||
|
||||
|
||||
def test_riconverti_lancia_lo_script_vero_con_esegui(monkeypatch):
|
||||
chiamate = []
|
||||
|
||||
class R:
|
||||
returncode, stdout, stderr = 0, " → piano valido\n ESITO: 10 USDE\n", ""
|
||||
|
||||
def finto_run(cmd, **kw):
|
||||
chiamate.append(cmd)
|
||||
return R()
|
||||
|
||||
monkeypatch.setattr(W.subprocess, "run", finto_run)
|
||||
e = W.riconverti(0.67)
|
||||
assert e["ok"] and chiamate and chiamate[0][1].endswith("usde_convert.py")
|
||||
assert "--esegui" in chiamate[0] and "0.6700" in chiamate[0]
|
||||
assert e["coda"][-1].startswith(" ESITO")
|
||||
|
||||
|
||||
def test_il_monitor_e_registrato_in_monitor_health():
|
||||
from src.live.monitor_health import MONITORS
|
||||
spec = [m for m in MONITORS if m.name == "cuscino_watch"]
|
||||
assert spec and spec[0].cadence_h == 1.0 and spec[0].open_labeled is False
|
||||
|
||||
|
||||
def test_un_non_tentativo_non_consuma_il_budget():
|
||||
"""Prezzo illeggibile, --secco, interruttore spento: `tentata=False` non blocca il giro dopo."""
|
||||
now = 1_789_000_000_000
|
||||
prev = [{"ts": now - 3_600_000, "riconversione": {"tentata": False, "motivo": "--secco"}}]
|
||||
assert W.puo_riconvertire(prev, now, True, 1.0, 0.99)[0]
|
||||
|
||||
|
||||
def test_dopo_il_ripristino_lo_stato_e_OK_non_preavviso():
|
||||
"""Il margine di ripristino deve superare la soglia di preavviso, o ogni 🚨 e' seguito da un ⚠️
|
||||
per costruzione (P14). Include il floor di 1 USDE e la tolleranza di quota di usde_convert."""
|
||||
assert W.MARGINE_RIPRISTINO > W.PREAVVISO_FRAC
|
||||
f, _ = U.frazione_cuscino()
|
||||
E = 4450.0
|
||||
q = U.quota_ripristino(W.MARGINE_RIPRISTINO) + 0.005 # usde_convert si ferma entro TOLL
|
||||
usdc_dopo = E * (1 - q) - 1.0 # e vende fino a 1 USDE in meno
|
||||
assert W.giudica(usdc_dopo, E, E * f)["stato"] == "OK"
|
||||
|
||||
|
||||
def test_cron_installato_e_dichiarato_fuori_dal_tondo_e_dopo_il_book():
|
||||
"""Legge la crontab VIVA (come test_book_cadenza), non un commento nello .sh (P1)."""
|
||||
import shutil
|
||||
import subprocess
|
||||
s = (ROOT / "scripts" / "cron_cuscino.sh").read_text()
|
||||
assert "cuscino_watch.py --quiet" in s
|
||||
if not shutil.which("crontab"):
|
||||
return # SALTATO: distinto da 'assente'
|
||||
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
|
||||
if out.returncode != 0 or "PythagorasGoal" not in out.stdout:
|
||||
return # altra macchina / altro utente
|
||||
righe = [ln for ln in out.stdout.splitlines() if "cron_cuscino.sh" in ln and not ln.startswith("#")]
|
||||
assert len(righe) == 1, "cron_cuscino.sh deve essere installato UNA volta"
|
||||
minuto, ora = righe[0].split()[:2]
|
||||
assert minuto == "53" and ora == "*" # orario, dopo il book (:47)
|
||||
@@ -0,0 +1,126 @@
|
||||
"""GATE PREVDAY-01 cablato nel monitor (issue #4): kill, veto d'integrita', data.
|
||||
|
||||
Prima del 10/09 il gate esisteva solo nel testo (RESULTS-0822 §54) e la revisione settimanale lo
|
||||
ha letto come «senza data». Qui si blinda che: (a) le costanti sono quelle scritte il 23/08;
|
||||
(b) il kill non e' leggibile sotto 180 giorni attivi e scatta solo sotto -0,50; (c) il veto
|
||||
blocca sotto l'80% di barre ricostruibili e su una crescita MATERIALE delle divergenze — non su
|
||||
una barra sola (P14); (d) la ricostruzione riproduce una serie generata con la stessa aritmetica
|
||||
di `advance`; (e) la lente del kill e' GIORNALIERA."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
sp = importlib.util.spec_from_file_location("paper_prevday", ROOT / "scripts" / "live" / "paper_prevday.py")
|
||||
PP = importlib.util.module_from_spec(sp)
|
||||
sp.loader.exec_module(PP)
|
||||
|
||||
|
||||
def test_le_costanti_sono_quelle_del_gate_scritto_il_23_08():
|
||||
assert PP.GATE_DATE == "2027-06-21"
|
||||
assert PP.KILL_SHARPE == -0.50 and PP.KILL_MIN_ACTIVE_DAYS == 180
|
||||
assert PP.MIN_RECON_FRAC == 0.80
|
||||
# e CLAUDE.md §4 cita la stessa data (era assente fino al 10/09)
|
||||
assert "2027-06-21" in (ROOT / "CLAUDE.md").read_text()
|
||||
|
||||
|
||||
def _serie(n_giorni: int, mu_h: float, sd_h: float, seed: int = 0, pos: float = 0.3) -> pd.DataFrame:
|
||||
rng = np.random.default_rng(seed)
|
||||
n = n_giorni * 24
|
||||
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
|
||||
x = rng.normal(mu_h, sd_h, n)
|
||||
return pd.DataFrame(dict(ts=ts, dt=pd.to_datetime(ts, unit="ms", utc=True), net_modeled=x,
|
||||
net_real=x, pos_btc=pos, pos_eth=pos))
|
||||
|
||||
|
||||
def test_kill_non_leggibile_sotto_180_giorni_attivi():
|
||||
r = _serie(81, -0.001, 0.004)
|
||||
sh, n = PP.sharpe_giornaliero(r)
|
||||
assert n == 81 and sh < PP.KILL_SHARPE # Sharpe pessimo...
|
||||
stato, motivo = PP.kill_check(sh, PP.giorni_attivi(r), pd.Timestamp("2026-09-10", tz="UTC"))
|
||||
assert stato == "NON_MATURO" and "99" in motivo # ...ma il kill non e' leggibile
|
||||
|
||||
|
||||
def test_kill_scatta_sopra_180_giorni_solo_sotto_la_soglia():
|
||||
r_male = _serie(200, -0.001, 0.004)
|
||||
sh, _ = PP.sharpe_giornaliero(r_male)
|
||||
assert PP.kill_check(sh, PP.giorni_attivi(r_male), pd.Timestamp("2027-01-01", tz="UTC"))[0] == "KILL"
|
||||
oggi = pd.Timestamp("2027-01-01", tz="UTC")
|
||||
assert PP.kill_check(-0.49, 200, oggi)[0] == "SUPERATO" # la soglia e' STRETTA: -0,49 passa
|
||||
assert PP.kill_check(-0.51, 200, oggi)[0] == "KILL"
|
||||
assert PP.kill_check(float("nan"), 200, oggi)[0] == "NON_MISURABILE"
|
||||
|
||||
|
||||
def test_i_giorni_flat_non_contano_come_attivi():
|
||||
r = _serie(200, 0.0, 0.004)
|
||||
r.loc[r.index[: 50 * 24], ["pos_btc", "pos_eth"]] = 0.0
|
||||
assert PP.giorni_attivi(r) == 150
|
||||
|
||||
|
||||
def test_la_lente_del_kill_e_giornaliera_non_oraria():
|
||||
"""§54: sulla lente oraria lo stesso forward vale +2,04 contro +1,56. Con autocorrelazione
|
||||
positiva intragiornaliera la lente oraria SOVRASTIMA: il kill usa la giornaliera."""
|
||||
rng = np.random.default_rng(3)
|
||||
n = 120 * 24
|
||||
base = np.repeat(rng.normal(0.0004, 0.004, 120), 24) # shock comune al giorno
|
||||
x = base + rng.normal(0, 0.0005, n)
|
||||
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
|
||||
r = pd.DataFrame(dict(ts=ts, dt=pd.to_datetime(ts, unit="ms", utc=True), net_modeled=x,
|
||||
net_real=x, pos_btc=0.3, pos_eth=0.3))
|
||||
sh_g, _ = PP.sharpe_giornaliero(r)
|
||||
assert PP._sharpe_orario(r) > sh_g * 2 # la lente oraria gonfia; il kill non la usa
|
||||
|
||||
|
||||
def test_veto_sotto_80_pct_ricostruibili():
|
||||
ok, m = PP.veto_check(0.79, 0.5, 0.5, 10)
|
||||
assert not ok and "79.0%" in m
|
||||
assert PP.veto_check(0.80, 0.5, 0.5, 10)[0]
|
||||
|
||||
|
||||
def test_veto_su_crescita_materiale_non_su_una_barra(monkeypatch):
|
||||
"""Il 10/09 la serie reale aveva UNA barra divergente su 1943 (26/08 00:00, il giorno del fix
|
||||
di advance()) e la prima stesura del veto la leggeva come 'crescita' (0,03/g contro 0,01/g)."""
|
||||
assert PP.veto_check(0.999, 0.012, 0.032, 1)[0] # 1 barra: non e' crescita
|
||||
ok, m = PP.veto_check(0.95, 0.10, 0.50, 12) # 12 recenti, tasso 5x: crescita
|
||||
assert not ok and "crescita" in m
|
||||
assert PP.veto_check(0.95, 0.30, 0.50, 12)[0] # tasso < 2x: no
|
||||
|
||||
|
||||
def test_ricostruzione_riproduce_una_serie_generata_come_advance(monkeypatch):
|
||||
"""Feed sintetico, target congelato deterministico (monkeypatch su prevday_target): la serie
|
||||
registrata con l'aritmetica di advance si ricostruisce al 100%; una barra manomessa no."""
|
||||
n = 24 * 10
|
||||
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
|
||||
rng = np.random.default_rng(7)
|
||||
dfs = {}
|
||||
for a, p0 in (("BTC", 60000.0), ("ETH", 2000.0)):
|
||||
close = p0 * np.cumprod(1 + rng.normal(0, 0.003, n))
|
||||
dfs[a] = pd.DataFrame(dict(timestamp=ts, datetime=pd.to_datetime(ts, unit="ms", utc=True), close=close))
|
||||
tgt = {a: np.where((np.arange(n) // 24) % 2 == 0, 0.3, -0.3) for a in dfs}
|
||||
monkeypatch.setattr(PP, "prevday_target",
|
||||
lambda df: tgt["BTC"] if float(df["close"].iloc[0]) > 10000 else tgt["ETH"])
|
||||
start = int(ts[23])
|
||||
# registrazione con la stessa aritmetica di advance (ribilanciamento continuo)
|
||||
pos = {a: float(tgt[a][23]) for a in dfs}
|
||||
rows = []
|
||||
for i in range(24, n):
|
||||
net = 0.0
|
||||
for a in dfs:
|
||||
c = dfs[a]["close"].values
|
||||
rr = c[i] / c[i - 1] - 1
|
||||
net += PP.WEIGHT * (pos[a] * rr - PP.FEE_SIDE * abs(tgt[a][i] - pos[a]))
|
||||
pos[a] = float(tgt[a][i])
|
||||
rows.append(dict(ts=int(ts[i]), net_modeled=round(net, 6), pos_btc=pos["BTC"], pos_eth=pos["ETH"]))
|
||||
r = pd.DataFrame(rows); r["dt"] = pd.to_datetime(r["ts"], unit="ms", utc=True)
|
||||
rc = PP.ricostruzione(r, dfs, start)
|
||||
assert rc["ricostruibili"] == rc["n"] == n - 24 and rc["assenti_nel_feed"] == 0
|
||||
r2 = r.copy(); r2.loc[r2.index[-1], "net_modeled"] += 0.01
|
||||
rc2 = PP.ricostruzione(r2, dfs, start)
|
||||
assert rc2["ricostruibili"] == rc2["n"] - 1 and rc2["div_recenti"] == 1
|
||||
@@ -205,3 +205,64 @@ def test_la_quota_si_decide_da_lunedi_e_solo_se_IDONEO():
|
||||
assert q("2026-09-30", "IDONEO") is True, "smette di chiedere: la decisione si dimentica"
|
||||
assert q("2026-08-31", "IN_ATTESA") is False, "chiede la quota su un conto non idoneo"
|
||||
assert q("2026-08-31", "NON_IDONEO") is False
|
||||
|
||||
|
||||
# ------------------------------ lettura dei trade: i due limiti del canale (issue #2) ------------------------------
|
||||
# Il 07/09 il lettore chiedeva limit=50, la sonda del 06/09 aveva fatto 54 ordini: 400 USDE di
|
||||
# acquisti sono finiti nel «reward». Il lettore ora chiede il massimo del venue (1000) e, se la
|
||||
# lista puo' essere incompleta — troncata al limite, o ultima lettura oltre la finestra di 24h
|
||||
# che il gateway espone — dichiara «non leggibile» invece di sommare (P12).
|
||||
|
||||
class _ClientFinto:
|
||||
def __init__(self, rows):
|
||||
self.rows, self.limit_chiesto = rows, None
|
||||
|
||||
def trade_history(self, instrument, limit=20):
|
||||
self.limit_chiesto = limit
|
||||
return self.rows[:limit]
|
||||
|
||||
|
||||
def _ordini(n, t0_ms, amt=100.0, passo_ms=15_000):
|
||||
return [{"timestamp": t0_ms + i * passo_ms, "direction": "buy", "amount": amt} for i in range(n)]
|
||||
|
||||
|
||||
def test_controllo_positivo_54_ordini_si_sommano_tutti():
|
||||
"""Il caso del 06-07/09: 54 ordini in 24h. Con il vecchio limit=50 ne mancavano 4."""
|
||||
prev, now = 1_788_698_101_000, 1_788_784_501_000 # 06/09 12:35Z -> 07/09 12:35Z
|
||||
c = _ClientFinto(_ordini(54, prev + 30_000_000))
|
||||
tot, n, motivo = W._trades_spot_da(c, prev, now)
|
||||
assert motivo is None and n == 54 and abs(tot - 5400.0) < 1e-9
|
||||
assert c.limit_chiesto == W.TRADE_LIMIT >= 54
|
||||
|
||||
|
||||
def test_lista_al_limite_del_venue_non_e_leggibile():
|
||||
prev, now = 1_788_698_101_000, 1_788_784_501_000
|
||||
c = _ClientFinto(_ordini(W.TRADE_LIMIT + 5, prev + 1000))
|
||||
tot, n, motivo = W._trades_spot_da(c, prev, now)
|
||||
assert tot is None and n is None and "troncata" in motivo
|
||||
# e il reward NON si attribuisce (P12)
|
||||
an = W.analizza({"eq_usde": 1000.0}, 1400.0, tot)
|
||||
assert an["reward_stimato"] is None and not an["reward_rilevato"]
|
||||
|
||||
|
||||
def test_lettura_oltre_la_finestra_del_gateway_non_e_leggibile():
|
||||
"""Giro saltato: 48h dall'ultima lettura. I trade fra 48h e 24h fa sono invisibili."""
|
||||
prev = 1_788_698_101_000
|
||||
now = prev + 48 * 3_600_000
|
||||
c = _ClientFinto(_ordini(3, now - 3_600_000))
|
||||
tot, n, motivo = W._trades_spot_da(c, prev, now)
|
||||
assert tot is None and "finestra" in motivo
|
||||
|
||||
|
||||
def test_la_cadenza_del_cron_sta_dentro_la_tolleranza():
|
||||
"""24h + qualche secondo (il cron delle 12:35 di due giorni consecutivi) e' coperta."""
|
||||
prev = 1_788_698_101_000
|
||||
assert W.trade_copertura(10, prev, prev + 24 * 3_600_000 + 2_000) is None
|
||||
assert W.trade_copertura(10, prev, prev + 24 * 3_600_000 + W.TOLL_FINESTRA_MS + 1) is not None
|
||||
|
||||
|
||||
def test_i_trade_prima_della_lettura_precedente_non_contano():
|
||||
prev, now = 1_788_698_101_000, 1_788_784_501_000
|
||||
rows = _ordini(2, prev - 60_000) + [{"timestamp": prev + 1, "direction": "sell", "amount": 40.0}]
|
||||
tot, n, motivo = W._trades_spot_da(_ClientFinto(rows), prev, now)
|
||||
assert motivo is None and n == 1 and abs(tot + 40.0) < 1e-9
|
||||
|
||||
Reference in New Issue
Block a user