merge lavoro/goldrake-u10 (secondo giro 27/09)
This commit is contained in:
@@ -285,6 +285,39 @@ def test_la_testata_porta_i_numeri_che_si_leggono_dalla_notifica():
|
||||
t = A.testata(_voce_piena())
|
||||
assert "$637.36" in t and "+2.36" in t and "+39.30" in t
|
||||
assert "BTC $+116" in t and "leva 0.41x" in t and "1 fill" in t
|
||||
# (#35 C8) il cron gira alle 00:30 sul giorno CHIUSO: non e' "oggi", e' "nel giorno".
|
||||
assert "nel giorno" in t and "oggi" not in t
|
||||
|
||||
|
||||
def test_testata_con_movimento_mostra_trading_giorno_non_il_grezzo():
|
||||
"""(#35 C8) Un versamento di $1.399 non deve leggersi come P&L: con mov_giorno != 0 e
|
||||
trading_giorno calcolabile, la cifra mostrata e' trading_giorno, non delta_equity."""
|
||||
voce = _voce_piena()
|
||||
voce["pnl"] = dict(voce["pnl"], delta_equity=1401.36, mov_giorno=1399.0, trading_giorno=2.36)
|
||||
t = A.testata(voce)
|
||||
assert "+2.36 nel giorno" in t
|
||||
assert "di cui +1399.00 movimenti" in t
|
||||
assert "+1401.36" not in t # il grezzo non compare piu' in testata
|
||||
|
||||
|
||||
def test_testata_con_movimento_ma_trading_giorno_none_resta_sul_grezzo():
|
||||
"""(#35 C8) Se trading_giorno non e' calcolabile (None) si resta su delta_equity, ma con
|
||||
l'etichetta nuova, non "oggi"."""
|
||||
voce = _voce_piena()
|
||||
voce["pnl"] = dict(voce["pnl"], delta_equity=1401.36, mov_giorno=1399.0, trading_giorno=None)
|
||||
t = A.testata(voce)
|
||||
assert "+1401.36 nel giorno" in t
|
||||
assert "di cui" not in t
|
||||
|
||||
|
||||
def test_testata_senza_movimento_mostra_il_grezzo_con_etichetta_nuova():
|
||||
"""(#35 C8) mov_giorno assente o 0: nessun movimento da scorporare, stesso trattamento di
|
||||
journal.scorporo (bool())."""
|
||||
voce = _voce_piena()
|
||||
voce["pnl"] = dict(voce["pnl"], mov_giorno=0.0, trading_giorno=2.36)
|
||||
t = A.testata(voce)
|
||||
assert "+2.36 nel giorno" in t
|
||||
assert "di cui" not in t
|
||||
|
||||
|
||||
def test_il_messaggio_sfugge_l_HTML_e_converte_il_grassetto():
|
||||
@@ -312,6 +345,21 @@ def test_il_taglio_a_4096_si_DICHIARA():
|
||||
assert "tagliato" in m and "docs/journal/2026-08-23.md" in m
|
||||
|
||||
|
||||
def test_taglio_a_cavallo_di_entita_e_tag_resta_HTML_valido():
|
||||
"""(#35 C13) Il taglio e' sul GREZZO, prima di _b/html.escape e prima di **-><b>: un '&' o
|
||||
un '**grassetto**' a cavallo del punto di taglio non deve produrre un'entita' spezzata
|
||||
(es. '&am') ne' un <b> senza chiusura -- Telegram rifiuta l'INTERO messaggio altrimenti
|
||||
("can't parse entities"). Il pattern si ripete per tutta la lunghezza, cosi' il punto di
|
||||
taglio (qualunque sia) cade sempre dentro o a cavallo di un '&' o di '**...**'."""
|
||||
pattern = "parola & altra parola **in grassetto** ancora testo "
|
||||
analisi = pattern * 200 # ben oltre qualunque budget qui
|
||||
m = A.per_telegram(_voce_piena(), analisi, "ok", [])
|
||||
assert "tagliato" in m
|
||||
assert re.search(r"&(?!amp;|lt;|gt;)", m) is None # nessuna entita' spezzata o '&' nudo
|
||||
assert m.count("<b>") == m.count("</b>") # ogni <b> aperto e' chiuso
|
||||
assert m.count("<i>") == m.count("</i>")
|
||||
|
||||
|
||||
def test_un_messaggio_normale_non_viene_toccato():
|
||||
m = A.per_telegram(_voce_piena(), "Analisi breve.", "ok", [])
|
||||
assert "tagliato" not in m and m.endswith("Analisi breve.")
|
||||
|
||||
@@ -132,6 +132,18 @@ def test_telegram_porta_la_sintesi_e_dichiara_il_taglio():
|
||||
assert "[… sintesi tagliata" in lunga and len(lunga) < 4096
|
||||
|
||||
|
||||
def test_taglio_a_cavallo_di_una_entita_resta_HTML_valido():
|
||||
"""(#35 C13) Un '&' esattamente al taglio non deve spezzarsi in un'entita' a meta' (es.
|
||||
'&am'): il taglio e' sul GREZZO, prima di html.escape, quindi il carattere e' incluso o
|
||||
escluso per intero — mai spezzato a meta' entita'."""
|
||||
sintesi = "a" * (R.LIMITE_TELEGRAM - 1) + "&resto che verra' tagliato via di sicuro"
|
||||
tg = R.per_telegram(_revisione(sintesi=sintesi), None, "ok", ADESSO, Path("p.md"))
|
||||
assert "[… sintesi tagliata" in tg
|
||||
corpo = tg.split("\n\n", 1)[1].split("\n[…", 1)[0]
|
||||
assert corpo == "a" * (R.LIMITE_TELEGRAM - 1) + "&" # il '&' e' incluso PER INTERO
|
||||
assert "&am;" not in tg and "resto che verra'" not in tg # niente entita' a meta', niente coda
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- orchestrazione: cosa scrive, cosa no
|
||||
|
||||
def _albero(root: Path) -> set[str]:
|
||||
|
||||
@@ -7,6 +7,7 @@ e non invia niente in dry-run.
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -21,8 +22,33 @@ telegram_daily = pytest.importorskip("telegram_daily")
|
||||
def test_build_non_solleva_e_ha_le_sezioni():
|
||||
txt = telegram_daily.build()
|
||||
assert isinstance(txt, str) and len(txt) > 50
|
||||
for sezione in ("Conto", "Perche' non opera", "Ordini", "Monitor in osservazione"):
|
||||
for sezione in ("Conto", "Ordini", "Monitor in osservazione"):
|
||||
assert sezione in txt, f"sezione mancante: {sezione}"
|
||||
# (#35 C14) l'header sul trend e' condizionale: esattamente uno dei due, non piu' fisso.
|
||||
assert ("Perche' non opera" in txt) != ("Stato trend" in txt)
|
||||
|
||||
|
||||
def test_header_perche_non_opera_solo_se_il_libro_e_flat(monkeypatch):
|
||||
"""(#35 C14) L'header fisso mentiva quando il libro NON e' fermo (posizioni o target != 0):
|
||||
con flat=True resta "Perche' non opera"."""
|
||||
monkeypatch.setattr(telegram_daily, "book_state", lambda: ([" equity $1.00"], True))
|
||||
assert "Perche' non opera" in telegram_daily.build()
|
||||
|
||||
|
||||
def test_header_neutro_se_il_libro_non_e_flat(monkeypatch):
|
||||
monkeypatch.setattr(telegram_daily, "book_state",
|
||||
lambda: ([" BTC: pos $300 → target $300"], False))
|
||||
txt = telegram_daily.build()
|
||||
assert "Stato trend" in txt and "Perche' non opera" not in txt
|
||||
|
||||
|
||||
def test_header_neutro_se_il_libro_non_e_leggibile(monkeypatch):
|
||||
"""Se book_state non sa dire se e' flat (eccezione -> None), niente header "non opera"
|
||||
dichiarato senza saperlo davvero: si usa il neutro."""
|
||||
monkeypatch.setattr(telegram_daily, "book_state",
|
||||
lambda: ([" ⚠️ stato libro non leggibile: Errore"], None))
|
||||
txt = telegram_daily.build()
|
||||
assert "Stato trend" in txt and "Perche' non opera" not in txt
|
||||
|
||||
|
||||
def test_nessuna_scrittura_ne_ordine():
|
||||
@@ -52,3 +78,77 @@ def test_convenzione_trend_e_documentata():
|
||||
"""La convenzione media-dei-segni (non frazione) e' la ragione per cui TP01 e' 0 con 1/3 su:
|
||||
se sparisce dal codice, il report mentirebbe sul 'quanto manca'."""
|
||||
assert "media dei SEGNI" in telegram_daily.trend_state.__doc__
|
||||
|
||||
|
||||
def test_soglia_accensione_2_negativi_1_positivo():
|
||||
"""2 negativi + 1 positivo: basta 1 flip -> soglia = il riferimento negativo PIU' BASSO."""
|
||||
r = telegram_daily._soglia_accensione([-1, -1, 1], [100.0, 200.0])
|
||||
assert r == 100.0
|
||||
|
||||
|
||||
def test_soglia_accensione_3_negativi():
|
||||
"""3 negativi: servono 2 flip -> soglia = il SECONDO riferimento negativo piu' basso, non il
|
||||
minimo (il bug di #35 C3: superare solo il minimo lascia la media <= 0)."""
|
||||
r = telegram_daily._soglia_accensione([-1, -1, -1], [100.0, 200.0, 300.0])
|
||||
assert r == 200.0
|
||||
|
||||
|
||||
def test_soglia_accensione_1_negativo_2_positivi_gia_acceso():
|
||||
"""1 negativo + 2 positivi: la media e' gia' positiva (TP01 > 0) -> nessuna soglia da dare."""
|
||||
assert telegram_daily._soglia_accensione([-1, 1, 1], [100.0]) is None
|
||||
|
||||
|
||||
def test_soglia_accensione_nessun_negativo():
|
||||
"""Tutti positivi: nessun riferimento negativo, nessuna soglia."""
|
||||
assert telegram_daily._soglia_accensione([1, 1, 1], []) is None
|
||||
|
||||
|
||||
def test_soglia_accensione_servono_tutti_i_flip():
|
||||
"""2 negativi su 2 (nessun positivo): servono ENTRAMBI i flip -> k == len(neg_refs), soglia
|
||||
= il piu' alto dei due (l'ultimo a bastare), non il minimo."""
|
||||
r = telegram_daily._soglia_accensione([-1, -1], [200.0, 100.0])
|
||||
assert r == 200.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- (#35 C14) gate: data, formato
|
||||
|
||||
|
||||
def test_gate_date_legge_da_monitor_health():
|
||||
"""La data non e' una seconda dichiarazione: viene da monitor_health.MONITORS (`gate`),
|
||||
la stessa tabella che gli alert dei sorveglianti citano come "METTE A RISCHIO il gate"."""
|
||||
from src.live.monitor_health import MONITORS
|
||||
m = next(x for x in MONITORS if x.name == "paper_statarb")
|
||||
assert m.gate[:10] == "2026-09-27"
|
||||
assert telegram_daily._gate_date("paper_statarb", date(2030, 1, 1)) == date(2026, 9, 27)
|
||||
m2 = next(x for x in MONITORS if x.name == "paper_xsr")
|
||||
assert telegram_daily._gate_date("paper_xsr", date(2030, 1, 1)) == date.fromisoformat(m2.gate[:10])
|
||||
|
||||
|
||||
def test_gate_date_usa_la_cablata_se_il_monitor_non_esiste():
|
||||
"""Rete di sicurezza: se il nome non e' in MONITORS (o cambia), non si inventa una data."""
|
||||
assert telegram_daily._gate_date("non_esiste_piu'", date(2030, 1, 1)) == date(2030, 1, 1)
|
||||
|
||||
|
||||
def test_fmt_giorni_tre_casi_mai_il_doppio_segno():
|
||||
"""(#35 C14) giorni>0 -> '-Ng'; 0 -> 'oggi'; <0 -> 'scaduto da Ng'. Mai '(−-1g)'."""
|
||||
assert telegram_daily._fmt_giorni(5) == "-5g"
|
||||
assert telegram_daily._fmt_giorni(0) == "oggi"
|
||||
assert telegram_daily._fmt_giorni(-1) == "scaduto da 1g"
|
||||
assert "-1g" not in telegram_daily._fmt_giorni(-1).replace("scaduto da 1g", "") # niente doppio segno
|
||||
|
||||
|
||||
def test_monitors_formatta_i_tre_casi_del_giorno(monkeypatch, tmp_path):
|
||||
"""I rami di monitors() (barra assente qui: file inesistenti in tmp_path) usano tutti
|
||||
_fmt_giorni, non il vecchio '(−{giorni}g)' cablato: futuro, oggi e passato si distinguono."""
|
||||
from datetime import timedelta
|
||||
oggi = date.today()
|
||||
monkeypatch.setattr(telegram_daily, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(telegram_daily, "GATES",
|
||||
[("FUTURO", oggi + timedelta(days=10), "data/nonexiste1/r.jsonl", "k"),
|
||||
("OGGI", oggi, "data/nonexiste2/r.jsonl", "k"),
|
||||
("PASSATO", oggi - timedelta(days=3), "data/nonexiste3/r.jsonl", "k")])
|
||||
righe = telegram_daily.monitors()
|
||||
assert "(-10g)" in righe[0]
|
||||
assert "(oggi)" in righe[1]
|
||||
assert "(scaduto da 3g)" in righe[2]
|
||||
assert "−-" not in "".join(righe) and "(−" not in "".join(righe) # mai il doppio segno
|
||||
|
||||
Reference in New Issue
Block a user