Bond Strategy Lab

Manuale tecnico — formule, algoritmi, esempi
← Torna all'app

CAPITOLO 1Panoramica

1.1 — Cos'è Bond Strategy Lab

Bond Strategy Lab è un'applicazione web vanilla-JS che gira interamente nel browser dell'utente, senza server, senza backend e senza salvataggi cloud. Il suo scopo è aiutare un investitore obbligazionario italiano a costruire, analizzare e confrontare strategie su un universo di obbligazioni quotate su MOT ed EuroTLX.

L'app riceve in input fino a 5 file CSV (3 obbligatori: anagrafica titoli, flussi di cassa, rendimenti EOD; 2 opzionali: rendimenti intraday, storico prezzi), elabora le strategie in 7 sezioni numerate (§01§07) e mostra metriche tecniche: IRR netto, YTM residuo, duration, Z-spread, attrito di switch, breakeven, copertura mensile delle cedole.

Caratteristiche tecniche:

  • Tutto il calcolo è lato client: nessuna informazione esce dal browser (eccetto la fetch opzionale di prezzi live da Borsa Italiana via CORS proxy)
  • Lo stato (file, selezione, override) può essere esportato in un singolo JSON e re-importato successivamente per riprendere il lavoro
  • 7 temi visivi alternativi (Editorial, Terminal, Swiss, Brutalist, Italia, Modernist, TradeView)
  • Mobile-responsive con layout dedicato sotto i 600px

1.2 — Per chi è

Il pubblico target è l'investitore retail sofisticato italiano che:

  • Investe direttamente in obbligazioni (BTP, BOT, corporate, governativi esteri) tramite intermediari italiani
  • Conosce il vocabolario tecnico: YTM, duration, Z-spread, rateo, capital gain, compensazione fiscale
  • Vuole valutare scenari di switch (vendere A, comprare B) tenendo conto non solo del Δ-rendimento ma anche dell'attrito complessivo
  • Apprezza il dettaglio e la verificabilità più della "magia" automatica

L'app non è pensata per:

  • Investitori principianti che hanno bisogno di guida pedagogica
  • Trading ad alta frequenza (i dati sono daily / intraday delayed)
  • Mercati esteri non quotati su Borsa Italiana
  • Strumenti diversi dalle obbligazioni (no azioni, ETF, derivati)

1.3 — Fonti dati

L'app non genera dati di mercato: legge file CSV prodotti da una fonte esterna. La fonte ufficiale è:

URL fonte dati
https://www.simpletoolsforinvestors.eu/documentivari.php
sezione "Data export"

Questo sito pubblica quotidianamente 5 file scaricabili come CSV (alcuni compressi in .csv.zip). I link hanno hash che cambia ogni giorno, quindi sono URL non bookmarkabili.

L'app supporta due modalità di acquisizione:

Modalità 1: caricamento manuale

L'utente scarica i 5 file dal sito sorgente, li decomprime se necessario, e li trascina nelle 5 caselle della §01 Sorgenti. Validazione automatica del contenuto e degli header.

Modalità 2: auto-download

Il sesto tile della §01 ("Scarica da web") esegue automaticamente:

  1. Fetch della pagina documentivari.php tramite CORS proxy
  2. Estrazione dei 5 URL via regex stabile
  3. Download dei 5 file via proxy
  4. Decompressione dei .zip via libreria JSZip caricata da CDN
  5. Iniezione nei loader come se fossero stati caricati a mano
⚠ ATTENZIONE

L'auto-download usa servizi CORS proxy di terze parti (corsproxy.io, api.codetabs.com, ecc.) che hanno limiti di rate e potrebbero non essere sempre disponibili. In caso di fallimento, il caricamento manuale resta sempre operativo.

1.4 — Quadro normativo

L'app implementa le regole fiscali italiane vigenti per le persone fisiche residenti in Italia (regime amministrato).

Aliquote sul reddito da capitale

Aliquote applicate dall'app per categoria di titolo
CategoriaAliquotaNote
Titoli di Stato italiani (BTP, BOT, CCT, BTPi, BTP€i)12,5%Su cedole e capital gain
Sovranazionali equiparati ai titoli di Stato (BEI, BIRS, altri organismi internazionali ex art. 31 DPR 601/73)12,5%Equiparate ai titoli di Stato
Titoli di Stato esteri "white list" (Germania, Francia, Austria, ...) ex art. 3, comma 2 D.L. 66/201412,5%Lista DM 23/03/2017 e aggiornamenti
Corporate, bancarie, supranational non white-list26%Aliquota ordinaria

L'app deduce automaticamente l'aliquota di ogni titolo analizzando i flussi di cassa del file CASH_FLOWS.csv: per ogni cedola incassata, il rapporto Tassazione cedola / Cedola lorda rivela l'aliquota applicata dall'emittente. Il dettaglio è documentato in § 3.4.

Compensazione minusvalenze (D.Lgs. 461/97)

Le minusvalenze realizzate possono essere compensate con plusvalenze nei quattro anni solari successivi. La compensazione avviene nello stesso paniere fiscale, ma le aliquote diverse (12,5% e 26%) richiedono un fattore di conversione cross-rate documentato in § 7.2.

Rateo e tassazione anticipata

Quando si compra un'obbligazione "dirty" (cioè con rateo maturato), si paga al venditore il rateo lordo. La tassazione del rateo è anticipata: viene applicata sull'intera cedola successiva. L'app gestisce questo aspetto tramite gli eventi Rateo lordo e Storno tassazione rateo nel file CASH_FLOWS, dettagliati in § 3.1.

▸ NOTA

L'app applica il regime amministrato (il sostituto d'imposta è l'intermediario). Per il regime dichiarativo non sono necessarie modifiche, ma le sequenze temporali dei flussi fiscali cambiano (anno solare invece che evento-per-evento).

⚠ DISCLAIMER

L'app è uno strumento di analisi non vincolante. I calcoli sono fatti applicando le formule di pratica corrente, ma possono divergere da quanto il vostro intermediario riporta. Per decisioni operative consultate il vostro intermediario e/o un consulente fiscale.

CAPITOLO 2File di input

L'app accetta 5 file CSV: 3 obbligatori (BONDS, CASH_FLOWS, REND_EOD) e 2 opzionali (REND_INTRADAY, PREZZI). Senza REND_INTRADAY l'app usa solo prezzi e YTM di chiusura precedente; senza PREZZI è disabilitata la sola derivazione del PMC da data di acquisto.Tutti i file devono usare le convenzioni italiane: separatore ;, decimale ,, date dd/mm/yyyy, encoding UTF-8. La prima riga è sempre l'header con i nomi colonna in inglese.

▸ FONTE

Tutti i file provengono dal sito simpletoolsforinvestors.eu/documentivari.php — sezione "Data export". Vedere § 1.3 per i dettagli sulla distribuzione e § 8.2 per l'auto-download.

2.1 — BONDS (anagrafica)

Contiene un'unica riga per ogni titolo quotato. È la fonte primaria per il "censimento" dell'universo: ogni titolo presente qui può comparire nella §02 Universo e quindi essere selezionato per il portafoglio.

Colonne

ColonnaTipoNote
isincodestringaChiave primaria (es. IT0005440620)
descriptionstringaEs. "BTP 01/11/2026 7,25%"
currencycodestringaEUR, USD, GBP, ...
minimumlotnumeroTaglio minimo (es. 1000, 10000)
marketcodestringaMOT, EUROTLX
segmentcodestringaBTP, BOT, DOMESTICMOT, EXTRAMOT, EUROTLX, ...
statusstringaQUOTATO, SOSPESO, ...
firstdatedataData di prima quotazione
issuedatedataData di emissione
issuepricenumeroPrezzo di emissione (su 100)
redemptiondatedataData di scadenza/rimborso
redemptionpricenumeroPrezzo di rimborso (tipicamente 100)

I campi required per il caricamento sono: isincode, description, minimumlot, redemptiondate. Tutti gli altri sono opzionali ma la loro assenza limita alcune funzionalità (es. senza issueprice non si può calcolare il disaggio).

2.2 — CASH_FLOWS (flussi)

Per ogni titolo, una sequenza di eventi con data e importo. Il file descrive il cash flow netto di un acquisto virtuale di 10.000 di nominale alla data di elaborazione (riportata nel file). Eventi e importi sono scalati di conseguenza.

Il sistema usa un nominale di riferimento 10000: ogni importo viene poi riscalato per il nominale effettivamente posseduto dall'utente. La scala è:

Scala flusso al nominale utente
scale = (lots × minimumLot) / 10000

importoEffettivo = importoFile × scale

Colonne

ColonnaTipoNote
isincodestringaIdentifica il titolo
descriptionstringaDescrizione (ridondante con BONDS)
eventtypestringaSale, Income, Taxes, ...
eventdescriptionstringaTipo specifico dell'evento
eventdatedataData del flusso
eventamountnumeroImporto del flusso (positivo o negativo)

Categorie di eventi

L'app riconosce e tratta 11 tipi diversi di eventdescription:

Eventi presenti nel file CASH_FLOWS
EventdescriptionSegnoFrequenzaDescrizione
Pagamento nominale1 (t=0)Esborso clean (prezzo × nominale / 100)
Rateo lordo0 o 1 (t=0)Solo se compra mid-cedola
Storno tassazione rateo+0 o 1 (t=0)Credito d'imposta sul rateo
Imposta sul disaggio0 o 1 (t=0)Solo BOT/ZC: tassazione anticipata
Credito disaggio+0 o 1 (t=0)Quota disaggio già maturata
Commissioni di acquisto0 o 1 (t=0)Stima dell'intermediario presente nel file. L'app la scarta e la sostituisce con Commissioni utente calcolata dalla tabella di §03 (vedi § 3.3).
Incasso cedola lorda+N (t>0)Cedole future, lorde
Tassazione cedolaN (t>0)Tassa applicata sulla cedola (12,5% o 26%)
Rimborso nominale+1 (t=scadenza)Rimborso a 100 (o redemptionprice)
Tassazione capital gain0 o 1 (t=scadenza)Solo se prezzo carico ≠ 100
Addebito disaggio di emissione0 o 1 (t=scadenza)Tassa su disaggio non ancora pagata
⚠ COMMISSIONI: OVERRIDE TOTALE

La voce Commissioni di acquisto del file CASH_FLOWS — pur essendo letta — NON impatta i calcoli di IRR/YTM dell'app. Viene scartata in buildPortfolioCashflows() e rimpiazzata da una riga Commissioni utente generata dalla tabella di §03.

RIF CODICE

La categorizzazione degli eventi è implementata in src/js/06-portfolio.js → _classifyCFItem(), che li raggruppa in 4 categorie visive per il chart cashflow: coupon, repay, buy, other.

Da queste 11 voci, l'app deriva tutte le metriche aggregate del portafoglio: investito totale, controvalore lordo, IRR netto, YTM, composizione mensile dei flussi.

2.3 — REND_EOD e REND_INTRADAY

Due file paralleli con la stessa struttura, differenti solo per timestamp dell'ultima rilevazione:

  • REND_EOD: prezzi e metriche di fine giornata (chiusura del mercato del giorno precedente)
  • REND_INTRADAY: aggiornamento durante la giornata di trading (delay ~15-30 minuti)

L'app combina i due: usa askprice di REND_INTRADAY se disponibile, altrimenti price di REND_EOD. Stessa cosa per duration, YTM, spread.

Colonne principali

ColonnaNote
isincodeIdentifica il titolo
price (EOD) / askprice (INTRADAY)Prezzo clean su 100
volumeVolume nominale scambiato
volumevalueControvalore scambiato
grossytmYTM lordo (decimale, es. 0.0325 = 3,25%)
netytmYTM netto applicando aliquota inferita
grossdurationDuration lorda (Macaulay) in anni
netduration / supernetdurationVariants netti
zspread Z-spread: spread parallelo costante sopra la curva spot zero-coupon di riferimento (tipicamente swap), che applicato a tutti i flussi riproduce il prezzo del bond. Misura del rischio di credito + liquidità. Espresso come decimale (es. 0,0125 = 125 bps = 1,25%).
ispreadI-spread: spread sul punto interpolato della par-swap curve alla maturità del bond. Approssimazione più semplice dello Z-spread, meno sensibile alla struttura a termine. Stesso formato decimale.
currentcouponrateCedola corrente (% nominale annuo)
couponperiodicityMesi tra cedole (6 = semestrale, 12 = annuale)
ratingsp, ratingmoodys, ratingfitchRating delle tre agenzie principali

L'app legge questi file ma non li ricalcola: si fida del valore pre-calcolato dalla fonte. L'unica eccezione è quando l'utente attiva i prezzi live: in quel caso, il prezzo da Borsa Italiana sostituisce askprice nel calcolo del controvalore.

2.4 — PREZZI storici (opzionale)

Quinto file, opzionale. Contiene lo storico dei prezzi di chiusura di tutti i titoli, distribuito compresso (.csv.zip).

Serve solo nella §06 Switch & Costi per la funzione "deriva PMC dalla data di acquisto": dato l'ISIN e la data di acquisto dell'utente, l'app cerca il prezzo storico più vicino e lo usa come PMC se l'utente non lo inserisce manualmente.

Colonne

ColonnaTipoNote
isincodestringaIdentifica il titolo (obbligatoria)
endvaluedatedataData di regolamento (giorno lavorativo successivo alla negoziazione) — obbligatoria
pricevaluenumeroPrezzo clean su 100 (decimale con virgola) — obbligatoria
pricetypestringaOpzionale: LP = Last Price (da contratto effettivo) o RP = Reference Price (di riferimento teorico)
marketcodestringaOpzionale: MOT, EUROTLX
referencedatedataOpzionale: data di negoziazione
volume, mintoday, maxtodaynumeroOpzionali: volumi e prezzi min/max della seduta

L'app usa endvaluedate come chiave temporale (la data di regolamento è quella registrata come "data prezzo" nello storico). Quando per la stessa data esistono righe con pricetype=LP e pricetype=RP, l'app preferisce LP (prezzo effettivamente scambiato) e usa RP solo come fallback per giorni senza scambi.

▸ NOTA

Se il file PREZZI non viene caricato, la funzione "deriva da data" è disabilitata ma il resto dell'app funziona normalmente. È necessario inserire il PMC manualmente per ogni titolo in §06.

CAPITOLO 3Calcoli base

Questo capitolo documenta i blocchi elementari di calcolo che compongono metriche più complesse trattate nei capitoli successivi. Sono i pezzi "primari" che alimentano l'IRR netto, il YTM residuo, la compensazione fiscale e l'analisi di switch.

3.1 — Rateo lordo

Quando un'obbligazione viene comprata a metà periodo cedolare, il compratore paga al venditore il rateo (quota di cedola maturata fino alla data di acquisto). L'app calcola il rateo per due motivi:

  • Determinare il dirty price = clean + rateo, che è il vero esborso al momento dell'acquisto
  • Per i titoli per cui il rateo non è già nel file CASH_FLOWS (es. BOT, ZC), ricalcolarlo dai dati dell'anagrafica

Formula generale (titoli a tasso fisso con cedole)

Rateo lordo per 100 di nominale
rateo = ((settlement - lastCouponDate) / regularPeriodDays) × nextCouponAmount

dove:
  settlement       = data di regolamento dell'operazione (T+2 dalla trade date)
  lastCouponDate   = data dell'ultima cedola staccata prima del settlement
  regularPeriodDays = numero di giorni tra lastCouponDate e nextCouponDate
                     (cedole con periodicità regolare; se prima cedola
                     irregolare, vedere casi speciali sotto)
  nextCouponAmount = importo della prossima cedola lorda per 100 di nominale

Esempio numerico

BTP semestrale, cedola 4% annuo (=2% semestrale = 2,00 per 100 lordo). Ultima cedola: 1 marzo. Prossima cedola: 1 settembre. Settlement: 15 giugno.

VariabileValore
lastCouponDate1 marzo
nextCouponDate1 settembre
regularPeriodDays184 giorni (1 marzo → 1 settembre, anno non bisestile)
settlement − lastCouponDate106 giorni
nextCouponAmount2,00 per 100
rateo(106 / 184) × 2,00 = 1,152 per 100

Per nominale 10.000: rateo = 10.000 × 1,152% = 115,20 €.

Casi speciali gestiti dall'app

  1. Cedola lunga/corta iniziale: alcuni BTP hanno una prima cedola con periodo non regolare (es. da emissione di metà gennaio a prima cedola di marzo successivo). L'app usa il regularPeriodDays della successiva cedola regolare, non della prima cedola.
  2. Inflation-linked (BTPi, BTP€i): il rateo lordo è rivalutato per il coefficiente di indicizzazione. L'app legge il rateo direttamente dal file CASH_FLOWS quando disponibile, evitando di ricalcolarlo.
  3. BOT e zero-coupon: non hanno cedole, quindi non hanno rateo. L'unico componente "rateo-simile" è la quota di disaggio maturata (vedere § 3.2).

Tassazione anticipata del rateo

Pagando il rateo lordo al venditore, il compratore sta acquisendo un diritto a futura cedola. Quando incassa la prossima cedola, la tassazione viene applicata sull'intero importo, inclusa la quota già pagata come rateo. Per evitare doppia tassazione, l'intermediario applica uno storno tassazione rateo:

Storno tassazione rateo (sul rateo pagato)
stornoTaxRateo = rateo × aliquota

Es. rateo 115,20 € su titolo 12,5%:
  stornoTaxRateo = 115,20 × 0,125 = +14,40 €

Questo importo viene accreditato al compratore a t=0 come credito
d'imposta. Quando arriva la cedola di settembre, la tassa applicata
sul totale è già "pre-pagata" da questo storno.
RIF CODICE

Implementato nella funzione computeDietimi() in bond_dashboard.html. Su un campione test di ~1.150 titoli a tasso fisso, il rateo calcolato dall'app combacia col rateo del file CASH_FLOWS nell'89-95% dei casi entro 0,01 per 100. Le divergenze residue derivano da cedole irregolari (long-first), indicizzazione (BTP€i, BTPi), e arrotondamenti dell'emittente.

3.2 — Disaggio e aggio

Il disaggio di emissione è la differenza positiva tra prezzo di rimborso (tipicamente 100) e prezzo di emissione (es. 98). L'aggio è il caso opposto (emissione sopra 100). Per la fiscalità italiana, il disaggio è equiparato a interesse e quindi tassato con l'aliquota propria del titolo.

L'app distingue tre meccanismi di tassazione del disaggio:

Caso BOT e zero-coupon

I BOT non hanno cedole: tutto il rendimento deriva dal disaggio. La tassazione è anticipata al momento dell'acquisto: quando il compratore paga il BOT, paga anche l'imposta sul disaggio relativo al periodo dal proprio acquisto alla scadenza.

Imposta sul disaggio (BOT)
disaggioMaturando = (rimborso − issuePrice) × (giorniResidui / giorniTotali)
impostaSulDisaggio = disaggioMaturando × aliquota

dove
      rimborso      = redemptionprice (tipicamente 100)
      issuePrice    = prezzo di emissione
      giorniResidui = scadenza − dataAcquisto
      giorniTotali  = scadenza − dataEmissione
      aliquota      = aliquota fiscale del titolo (es. 12,5% per i BOT)

Es. BOT a 6 mesi (180 gg totali), emesso a 98,50.
Acquistato 90 giorni dopo l'emissione a 99,20. Scadenza tra 90 giorni.

  issuePrice          = 98,50
  rimborso            = 100,00
  disaggio totale     = 100 − 98,50 = 1,50 per 100
  giorniResidui       = 90,  giorniTotali = 180
  disaggioMaturando   = 1,50 × (90 / 180) = 0,7500 per 100
  impostaSulDisaggio  = 0,7500 × 0,125 = 0,09375 per 100

  Per 10.000 di nominale: 0,09375 × 100 = 9,38 €  (l'utente paga 9,38 € di tassa anticipata sul disaggio)

Più ci si avvicina alla scadenza, meno disaggio resta da maturare, meno imposta da pagare. Per un BOT comprato il giorno dell'emissione, l'imposta è massima; comprandolo il giorno prima della scadenza è quasi zero.

Caso titoli con cedole + disaggio

Per BTP emessi sotto la pari, il disaggio si distribuisce in modo diverso:

  • Credito disaggio a t=0: se l'utente compra dopo l'emissione, una parte del disaggio è già stata "tassata" virtualmente nel prezzo. L'utente riceve un credito d'imposta proporzionale ai giorni passati dall'emissione.
  • Addebito disaggio di emissione a scadenza: la tassa sul restante disaggio viene applicata al momento del rimborso.
Credito disaggio (t=0, all'acquisto)
disaggioGiàMaturato = (rimborso − issuePrice) × (giorniDaEmissione / giorniTotali)
creditoDisaggio = disaggioGiàMaturato × aliquota

Es. BTP a 10 anni, emesso a 95 da emissione 3 anni fa.
Acquisto oggi a 97.

  disaggio totale teorico = 100 − 95 = 5 per 100
  disaggioGiàMaturato = 5 × (3 / 10) = 1,50 per 100
  creditoDisaggio = 1,50 × 0,125 = +0,188 per 100 (entrata a t=0)
▸ NOTA

I valori Credito disaggio, Imposta sul disaggio, Addebito disaggio di emissione sono già presenti nel file CASH_FLOWS. L'app li usa direttamente, non li ricalcola. Le formule sopra servono solo per interpretare i numeri che si vedono nel chart cashflow di §03.

3.3 — Commissioni

L'app applica una tabella di commissioni configurabile dall'utente, visibile in §03 sotto il pulsante "⚙ Commissioni e spese per tipo di titolo". Non assume un broker specifico: presenta valori di default ragionevoli che l'utente può modificare riga per riga per riflettere le tariffe del proprio intermediario. Le modifiche sono salvate nello stato esportabile.

Struttura della tabella

La tabella è organizzata per tipologia di titolo, non per broker o per fascia di nominale. Ogni riga corrisponde a una categoria di strumento, riconoscibile dai campi segmentcode, marketcode e — per i BOT — dalla maturity o dalla vita residua all'acquisto. Le categorie sono:

  • BOT — Acquisto in negoziazione (mercato secondario): 3 sotto-righe per la durata complessiva del titolo all'emissione:
    • bot-trim — Trimestrale (≤ 100 gg di durata all'emissione)
    • bot-sem — Semestrale (101–200 gg)
    • bot-ann — Annuale (> 200 gg)
  • BOT — Acquisto in asta: per i BOT comprati in asta, gli intermediari italiani applicano un tariffario diverso da quello secondario, articolato in 4 fasce per vita residua calcolata dalla data di regolamento:
    • bot-asta-le80 — vita residua ≤ 80 giorni
    • bot-asta-81-140 — vita residua 81–140 giorni
    • bot-asta-141-270 — vita residua 141–270 giorni
    • bot-asta-ge271 — vita residua ≥ 271 giorni
  • Titoli di Stato — MOT: ctz (CTZ Zero Coupon), btp (BTP tasso fisso), cct (CCT/CCTeu variabili)
  • Corporate MOT: corporate-mot (DomesticMOT, ExtraMOT, EuroMOT)
  • EuroTLX: eurotlx
  • Altro / Default: altro (fallback)

Parametri per ogni riga

Ogni riga ha quattro campi modificabili:

  • pct — Commissione proporzionale in % (es. 0,400 = 0,4%)
  • base — Su cosa si applica la percentuale, scelto tra:
    • nominal — sul prezzo nominale (clean × quantità, default)
    • purchase — sul controvalore di acquisto (dirty × quantità)
    • face — sul valore facciale (lotti × minLot)
  • min — Minimo proporzionale in € (floor sulla parte percentuale)
  • fix — Commissione fissa in € (addendum sempre sommato)

Formula esatta

Commissione totale per operazione
prop      = (pct / 100) × max(0, baseValue)
commTotal = max(min, prop) + fix

dove:
  baseValue = valore scelto da `base` (nominal | purchase | face) × quantità
  min       = floor sulla parte proporzionale (non sul totale)
  fix       = importo fisso aggiunto SEMPRE, anche se min è già attivo

Nota: min è un floor sulla sola componente proporzionale, non sul totale. Il fix viene aggiunto dopo, indipendentemente. Non c'è alcun cap (max) superiore.

Esempio numerico

BTP, controvalore nominale 30.000 €, default app
Riga "btp": pct=0,400, base=nominal, min=7,00, fix=4,75
baseValue = 30.000  (clean × quantità)
prop      = (0,400 / 100) × 30.000 = 120,00
commTotal = max(7,00 ; 120,00) + 4,75
          = 120,00 + 4,75
          = 124,75 €

Stesso BTP ma piccolo lotto: nominale 1.500 €
baseValue = 1.500
prop      = 0,004 × 1.500 = 6,00
commTotal = max(7,00 ; 6,00) + 4,75
          = 7,00 + 4,75              (qui il min ha effetto)
          = 11,75 €

Acquisto in asta: BOT vs altri titoli di Stato

Per gli acquisti effettuati direttamente in asta di collocamento (regolamento = data di emissione + 1 giorno lavorativo), il trattamento commissionale differisce tra i due gruppi di titoli di Stato italiani:

  • BTP, CTZ, CCT / CCTeu in asta: commissioni nulle per legge sui collocamenti retail. L'app ignora qualunque valore impostato in tabella e restituisce {pct:0, min:0, fix:0}.
  • BOT in asta: la legge consente commissioni e gli intermediari le applicano, tipicamente con una struttura per vita residua. L'app usa quindi una tabella dedicata bot-asta-* con 4 fasce (vedere sopra), separata da quella del mercato secondario. I valori sono preimpostati ai limiti massimi previsti dal DM MEF 15/01/2015 (Norme per la trasparenza nel collocamento dei titoli di Stato).
    Sulla commissione così calcolata l'app applica inoltre il tetto dell'art. 2 c. 3 dello stesso decreto, via applyBotAuctionCommissionCap():
    • lettera b) — se il prezzo medio ponderato d'asta è ≥ 100, la commissione è azzerata;
    • lettera a) — se il prezzo è < 100 ma il totale (prezzo + commissione + imposta sostitutiva) supera 100, la commissione è ridotta in modo che l'onere complessivo non superi 100 € per 100 € di capitale, fino ad azzerarsi. Nota: la riduzione agisce sulla sola commissione; se anche a commissione nulla il totale resta sopra 100 (perché l'imposta sostitutiva non è comprimibile), la commissione si ferma a 0.

La detection dell'asta è basata sulla coincidenza tra la data di regolamento del titolo e la data di acquisto:

Detection asta — isAstaAcquisto()
isAstaAcquisto(isin, bond, manualDate):
  // Solo titoli di Stato italiani (BOT, BTP, CTZ, CCT, CCTEU)
  if seg not in {BOT, BTP, CTZ, CCT, CCTEU}: return false
  primoRegolamento = bond.firstdate + 1 giorno lavorativo
  // 1. Se l'utente ha sovrascritto manualmente la data di acquisto:
  //    è considerato in asta se la data manuale coincide con primoRegolamento,
  //    firstdate, o issuedate; oppure se è ANTERIORE a una di queste (gap storico).
  if manualDate is set:
    if manualDate ∈ {primoRegolamento, firstdate, issuedate}: return true
    if manualDate < min(primoRegolamento, firstdate, issuedate): return true

  // 2. Altrimenti, controlla i cashflows del file:
  //    se la data "Pagamento nominale" coincide con primoRegolamento → asta.
  for cf in CASH_FLOWS[isin]:
    if cf.eventdescription == "Pagamento nominale" and cf.date == primoRegolamento:
      return true

  return false

Per i BOT in asta, il bracket di vita residua viene calcolato dalla differenza tra redemptiondate e la data di regolamento (in pratica: la durata residua del BOT all'asta in cui è stato emesso):

Detection bracket — detectBotResidualBracket()
detectBotResidualBracket(bond, settlementDate):
  ref  = settlementDate (default: primoRegolamento del titolo)
  days = (redemptiondate − ref) in giorni solari
  if days ≤ 80   → 'le80'      (vita residua ≤ 80 gg)
  if days ≤ 140  → '81-140'    (vita residua 81–140 gg)
  if days ≤ 270  → '141-270'   (vita residua 141–270 gg)
  else           → 'ge271'     (vita residua ≥ 271 gg)
▸ NOTA

Per un acquisto in vera asta, la vita residua all'asta coincide con la durata complessiva del BOT (perché si compra al regolamento, ossia al "tempo zero" del titolo). Quindi in pratica i 4 bracket bot-asta-* mappano sulle durate emesse dal MEF: BOT a 3 mesi (~91 gg) → bot-asta-81-140; BOT a 6 mesi (~182 gg) → bot-asta-141-270; BOT a 12 mesi (~365 gg) → bot-asta-ge271. Il bracket bot-asta-le80 serve per acquisti effettuati a una riapertura d'asta di un BOT già circolante con poco tempo alla scadenza.

L'utente può forzare manualmente la data di acquisto per ogni titolo se l'heuristic di detection automatica non corrisponde alla realtà.

Modifica e persistenza

I valori in tabella sono modificabili in qualunque momento. Cambiare un valore innesca un ricalcolo immediato di tutti gli IRR del portafoglio. Le modifiche sono incluse nello stato esportabile (vedere Cap. 11 quando pubblicato), così possono essere ripristinate in sessioni successive.

RIF CODICE

Implementato in src/js/06-portfolio.js: readCommRow(key) legge la riga della UI, getCommParams(isin, bond, manualDate) sceglie la riga giusta in base al titolo e all'asta/secondario, isAstaAcquisto() rileva l'acquisto in asta, detectBotMaturity() classifica per durata di emissione (uso secondario), detectBotResidualBracket() classifica per vita residua dal regolamento (uso asta), e calcComm(cp, baseValue) applica la formula.

3.4 — Aliquota fiscale inferita

L'app non ha una tabella hard-coded di aliquote per ISIN. Invece, deduce l'aliquota di ogni titolo guardando i rapporti tassazione / lordo presenti nel file CASH_FLOWS.

Questo approccio è più robusto di una lista manuale, perché segue automaticamente eventuali aggiornamenti della white list dei titoli di Stato esteri (DM 4 settembre 1996 e successivi) o cambiamenti dell'emittente.

Inferenza aliquota
Per ogni ISIN, raccoglie due tipi di ratios dai cashflows:

  1. Per ogni data con (Incasso cedola lorda, Tassazione cedola):
       ratio = |Tassazione cedola| / |Incasso cedola lorda|

  2. Per ogni data con (Rateo lordo, Storno tassazione rateo):
       ratio = |Storno tassazione rateo| / |Rateo lordo|

  avg = media dei ratios raccolti

  if avg < 0.19 : aliquota = 12,5%      (singolo threshold, non intervalli)
  else          : aliquota = 26%
  (nessuna categoria "sconosciuta": è sempre binaria se almeno un ratio esiste)

Confidence:
  ratios.length >= 4 → 'alta'
  ratios.length >= 2 → 'media'
  ratios.length == 1 → 'bassa'
  ratios.length == 0 → ritorna null  (nessuna inferenza possibile)

Il risultato è un oggetto: {rate: 12.5|26, avgRatio: number, n: number, confidence: 'alta'|'media'|'bassa'} oppure null se il file non contiene coppie utilizzabili. Nota: rate è in punti percentuali (12.5 o 26), non in decimale.

Esempi reali dai file

Inferenza su titoli reali
TitoloCedola lordaTassazioneratioAliquota
BTP 01/11/2027 6,5%325,00−40,620,12512,5% ✓
Austria 2,75% 2026137,50−17,190,12512,5% ✓ (white-list)
UniCredit subord. 5,375%268,75−69,880,26026% ✓ (corporate)

Edge case

Il threshold a 0,19 è la media tra 12,5% e 26%. Casi borderline (improbabili nella pratica italiana) verrebbero classificati al ribasso o al rialzo senza segnalazione esplicita.

Quando l'inferenza ritorna null (zero ratios disponibili), il default applicato dipende dal contesto:

  • in §03 (calcolo dietimo/rateo): 12,5% (assumendo titolo di Stato in mancanza di dati)
  • in §06 (capital gain in vendita): 26% (fallback prudenziale lato tassazione)
L'utente può sempre forzare manualmente l'aliquota effettiva in §06 (panello "Prezzi di carico" → colonna aliquota).

▸ NOTA

L'inferenza è memorizzata in _INFER_TAX_CACHE e viene invalidata automaticamente quando l'utente ricarica i file di input (la cache è azzerata in rebuildUniverse()). Durante una sessione, l'inferenza per ogni ISIN viene fatta una sola volta.

RIF CODICE

Funzione inferTaxRateFromCashflows(isin) in bond_dashboard.html. Ritorna {rate: 12.5, avgRatio, n, confidence} o {rate: 26, ...} o null.

CAPITOLO 4Rendimenti

Questo capitolo documenta come l'app calcola le metriche di rendimento: l'IRR (Internal Rate of Return) come algoritmo generale, e le due varianti contestuali utilizzate in §03 — l'IRR storico (dal momento dell'acquisto reale alla scadenza) e il YTM residuo (dal momento attuale alla scadenza, valutato ai prezzi di mercato correnti). La distinzione è cruciale per non confondere ciò che si è guadagnato con ciò che si guadagnerà.

4.1 — IRR ACT/365

L'IRR è il tasso di sconto annuo che rende il valore attuale netto (NPV) di una serie di flussi uguale a zero. È la metrica standard di rendimento per un portafoglio obbligazionario su un orizzonte pluriennale.

Formula

Definizione IRR — convenzione ACT/365
NPV(r) = Σᵢ CFᵢ / (1 + r)^tᵢ = 0

dove:
  CFᵢ = flusso di cassa i-esimo (negativo = uscita, positivo = entrata)
  tᵢ  = (dataᵢ − data₀) / 365      ← ACT/365: giorni solari, anno fisso 365 gg
  r   = tasso annuo, soluzione dell'equazione

La convenzione ACT/365 conta i giorni solari effettivi tra ogni flusso e la data del primo flusso (in cui il portafoglio viene acquistato), divisi per 365 giorni costanti. Non si fa distinzione tra anni bisestili e non. È la convenzione più diffusa per i confronti inter-titolo perché è insensibile a stagionalità del calendario.

Algoritmo di risoluzione

L'equazione NPV(r) = 0 non ha soluzione analitica: si risolve numericamente. L'app implementa una strategia a due livelli:

irrACT365(cashflows) — algoritmo
1. Newton-Raphson da 7 punti di partenza diversi:
     starts = [0.03, 0.05, 0.08, 0.0, 0.10, 0.15, -0.02]
   Per ogni start (indipendentemente, fino a 100 iterazioni per start):
     - max 100 iterazioni:
         r_new = r − NPV(r)/NPV'(r)
     - convergenza: |r_new − r| < 1e-9 e |NPV(r_new)| < 1e-4
     - se r ≤ -0.999 si riprende da -0.99 (singolarità di (1+r)^t)
   Se uno qualunque converge → ritorna r.

2. Fallback: bisezione sull'intervallo iniziale [-0.95, +5.00] (max 200 iter)
     - parte solo se NPV(-0.95) e NPV(+5.00) hanno SEGNO OPPOSTO;
       altrimenti ritorna NaN (impossibile applicare bisezione)
     - convergenza: |NPV(mid)| < 1e-6 → ritorna mid
     - se NPV(mid) non finito durante l'iterazione → ritorna NaN
     - se le 200 iterazioni si esauriscono senza tolleranza raggiunta →
       ritorna comunque il midpoint corrente (NON NaN)

I 7 starting points di Newton-Raphson coprono lo spettro dei rendimenti plausibili: 0% (yield nullo, raro ma possibile), 3-5% (BTP a medio termine a tassi normali), 8-15% (high-yield o BTP a tassi alti), -2% (rendimenti nominali negativi durante l'era dei tassi nulli). Il fallback a bisezione è la rete di sicurezza per casi patologici con flussi molto sbilanciati nel tempo (zero coupon, perpetual non supportate).

In pratica, per portafogli con flussi ragionevoli la convergenza arriva ben prima delle 200 iterazioni di bisezione; il caso di "esaurimento iterazioni senza tolleranza" non si verifica con flussi tipici.

▸ PRECONDIZIONI

L'IRR converge solo se sono soddisfatte tre condizioni:

  1. L'array dei flussi contiene almeno 2 elementi.
  2. Esiste almeno un flusso negativo (l'investimento).
  3. Esiste almeno un flusso positivo (i ritorni attesi).

Se una di queste condizioni manca, irrACT365 ritorna direttamente NaN senza tentare la risoluzione: matematicamente l'NPV non può attraversare lo zero.

Esempio numerico

BTP 10y comprato sotto la pari
Flussi di cassa annualizzati (esemplificativi):
  data           amount     t (anni)
  ─────────────  ─────────  ─────────
  oggi           -9.800     0.000     ← acquisto a 98 su 100
  oggi+1 anno      +250     1.000     ← cedola netta 2,5%
  oggi+2 anni      +250     2.000
  oggi+3 anni      +250     3.000
  ...
  oggi+10 anni     +250     10.000
  oggi+10 anni  +10.000    10.000    ← rimborso a 100

NPV(r) = -9800 + Σ(250/(1+r)^t) + 10000/(1+r)^10 = 0
Newton-Raphson da r=0,03 converge a r ≈ 0,02719
IRR ≈ 2,72% netto annuo composto
RIF CODICE

src/js/06-portfolio.js · irrACT365()

4.2 — IRR storico §03 — dall'acquisto alla scadenza

Il KPI IRR storico mostrato in §03 usa come prima data di cash-flow la data effettiva di acquisto di ciascun titolo. Per default tale data è letta dal file CASH_FLOWS (riga "Pagamento nominale"); può essere sovrascritta titolo per titolo tramite il campo Data acquisto nella tabella dei titoli selezionati in §03.

Includendo i flussi storici, l'IRR rappresenta il rendimento annualizzato che il portafoglio sta producendo dall'acquisto, coerente con il prezzo realmente pagato (PMC). Cambia solo se cambiano le cedole future (es. CCT a tasso variabile) o se si aggiunge/rimuove un titolo.

Composizione dei flussi

Costruzione cash flow del portafoglio storico
Per ogni ISIN in portafoglio:
  scale = (lotti × minLot) / 10000      ← i CF nel file sono per 10.000 nominale

  Per ogni flusso del file CASH_FLOWS:
    amount = file.amount × scale

  [se purchaseDate override attivo: vedi sezione "Override data" sotto]

  + Cedole sintetiche backfillate (solo se purchaseDate < prima data CF)
    ← vedi sezione "Backfill cedole storiche" sotto

  + Aggiungere "Commissioni utente" (negative) sulla data dell'acquisto
    secondo la tabella commissioni (vedi §3.3)

flows = unione (con segno) di tutti i flussi di tutti i titoli

IRR = irrACT365(flows)
⚠ PMC IMPLICITO — DERIVAZIONE DAL FILE

Il PMC (Prezzo Medio di Carico) è il prezzo clean per 100 derivato dal file CASH_FLOWS con la formula:

PMC clean (per 100) = −PagamentoNominale × 100 / 10.000

Il flusso "Pagamento nominale" del file rappresenta già la sola componente clean del prezzo di acquisto (rateo e commissioni sono righe distinte: Rateo lordo, Commissioni di acquisto). Il PMC implicito NON include rateo né commissioni: è solo la componente clean del controvalore. Questo è coerente con la pratica fiscale italiana, dove il "prezzo di carico" per il calcolo del capital gain è il prezzo clean, non il prezzo dirty.

Override data di acquisto — tre scenari

Il campo Data acquisto in §03 permette di specificare per ogni titolo una data diversa da quella nel file CASH_FLOWS. Il comportamento dipende dalla relazione tra la data impostata e la prima data nel file:

ScenarioCondizioneEffetto
Nessun override Campo vuoto Si usano le date del file CASH_FLOWS senza modifiche.
Override dopo il file purchaseDate > prima data CF I flussi di acquisto (Pagamento nominale, Rateo lordo, Credito disaggio…) vengono spostati alla nuova data. I CF tra la data originale e quella nuova vengono eliminati (già incassati prima del tracciamento). Rateo e dietimo vengono ricalcolati per la nuova data.
Override prima del file purchaseDate < prima data CF I flussi di acquisto vengono spostati all'indietro alla nuova data. I CF del file sono conservati integralmente (hanno tutti data successiva). Le cedole nel gap tra purchaseDate e la prima data CF — assenti nel file — vengono ricostruite sinteticamente (vedi sotto).

Backfill cedole storiche (override prima del file)

Quando la data di acquisto è precedente alla prima data del file CASH_FLOWS, le cedole incassate nel periodo intermedio non sono presenti nel file. Ometterle distorcerebbe l'IRR verso il basso (reddito non contabilizzato). L'app le ricostruisce dai metadati del titolo:

Algoritmo backfill cedole mancanti (solo bond TF)
Per ogni data cedolare nel gap (purchaseDate, originale_CF_date) — esclusi gli estremi:

  cedola_lorda = couponRate / couponPeriodicity × 10.000
  tassazione   = cedola_lorda × aliquota

  Aggiunge:
    {data: couponDate, amount: +cedola_lorda, desc: 'Incasso cedola lorda'}
    {data: couponDate, amount: -tassazione,   desc: 'Tassazione cedola'}

dove:
  couponRate        = da REND_EOD (currentcouponrate)
  couponPeriodicity = da REND_EOD
  aliquota          = inferita da inferTaxRateFromCashflows() (12,5% o 26%)
  date cedolari     = calendario teorico da couponMonths + couponPeriodicity
                      (stesso calendario usato per il calcolo del rateo)

Estremi esclusi:
  purchaseDate      → già coperto dal rateo lordo (accrual parziale)
  originale_CF_date → la prima cedola successiva è già nel file
⚠ LIMITAZIONI DEL BACKFILL
  • Solo bond TF: ZC e BOT non hanno cedole. Il disaggio/imposta sul disaggio è già gestito linearmente dal dietimo ed è corretto per qualsiasi data di acquisto.
  • Primo periodo long-first: se il gap include il primo periodo cedolare del titolo (cedola iniziale irregolare), l'app usa la cedola standard rate/N, che può differire leggermente dalla cedola long-first effettiva.
  • Bond step-up / step-down: si usa couponRate corrente come approssimazione. Se il tasso storico nel gap era diverso (es. un BTP step-up durante la fase a cedola bassa), il valore backfillato è approssimato.
  • Metadati assenti: se couponMonths o couponPeriodicity non sono presenti nel file REND_EOD per quel titolo, il backfill è silenziosamente saltato — i flussi mancanti non vengono aggiunti e l'IRR può risultare leggermente sottostimato.
  • Acquisto prima dell'emissione: se l'utente imposta una purchaseDate precedente a bond.issuedate, l'app NON applica un controllo: genererebbe cedole "fantasma" datate prima dell'emissione. Configurazione erronea che resta a carico dell'utente.
  • Aliquota: la tassazione delle cedole backfillate usa l'aliquota inferita dai cashflows (cfr. § 3.4) o 12,5% come fallback se l'inferenza non è disponibile.
RIF CODICE

src/js/06-portfolio.js · irrACT365()

Implementato in src/js/06-portfolio.js → buildPortfolioCashflows(): blocco BACKFILL dopo il blocco rateo/dietimo. L'IRR viene poi calcolato da updatePortfolioCalculations() tramite irrACT365(aggSimpleCF) — nessuna modifica alla funzione IRR stessa.

4.3 — YTM residuo §03 — da oggi al rimborso

Il KPI YTM residuo (da-oggi) mostrato in §03 risponde a una domanda diversa: se io comprassi questo portafoglio oggi a prezzo di mercato, quale rendimento annualizzato otterrei?

È la metrica utile per:

  • Confronto col rendimento di un titolo alternativo (anche di nuovo acquisto) → metrica omogenea
  • Valutare lo switch della strategia (vedi §06)
  • Decidere se tenere o vendere: il YTM residuo è il rendimento a cui si rinuncia vendendo

Costruzione dei flussi (simulazione di vendita-e-ricompra)

Algoritmo computeYTMResidualFromMarketPrices()
1. Simula la vendita di tutto il portafoglio al prezzo di mercato corrente:
   - controvalore vendita per posizione = (clean × quantità) + rateo maturato
     ⇒ è il valore DIRTY corrente, comprensivo di rateo
   - aliquota fiscale neutra 12,5% (non rileva: la YTM non dipende dal
     capital gain, che è funzione del PMC dell'utente)
   - commissioni a zero (commPct=0, commFix=0): vendita virtuale
   ⇒ totalMarketValue = Σ(controvaloreVendita)

2. Determina tradeDate:
   - è la data del PRIMO "Pagamento nominale" trovato nei cashflows di
     una delle posizioni (cioè la data storica originaria di acquisto)
   - fallback: oggi (se nessuna posizione ha "Pagamento nominale")

3. Raccoglie SOLO i flussi futuri (data > tradeDate):
   - esclude i flussi di acquisto già consumati
     (Pagamento nominale, Rateo lordo, Storno tassazione rateo,
      Imposta sul disaggio, Commissioni utente, Commissioni di acquisto)
   - include cedole nette, tasse cedola, rimborsi, capital gain

4. Costruisce la serie:
     CF₀ = -totalMarketValue   (datato tradeDate)
     CFᵢ = flussi futuri netti (i > 0, tutti dopo tradeDate)

5. YTM_residuo = irrACT365(CF_series)

Sorgente del "prezzo di mercato corrente" (in ordine di priorità):
  prezzo live fetchato da Borsa Italiana (cfr. § 8.3) → askprice INTRADAY → price EOD

In pratica: il YTM residuo è l'IRR di una strategia virtuale in cui oggi si paga il valore dirty di mercato (clean + rateo) e si ricevono i flussi futuri. Non c'è capital gain né commissione di vendita nel calcolo: sono questioni di esecuzione, non di rendimento atteso.

▸ NOTA — convenzione data di attualizzazione

L'IRR del YTM residuo viene calcolato ancorando l'outflow alla tradeDate storica (primo "Pagamento nominale" nei cashflows), non a oggi. Per portafogli acquistati molti anni fa, questo significa che l'IRR risultante è il rendimento misurato sul periodo tradeDate → scadenza, valutando però il portafoglio col valore di mercato di oggi. È una scelta convenzionale: per un'interpretazione più rigorosa "da oggi in avanti", usare il YTM-A di §06 dove tradeDate è quella della nuova operazione di switch.

RIF CODICE

Funzioni computeYTMResidualFromMarketPrices() e computeYTMofStrategyA() in bond_dashboard.html (righe ~6878-6945).

4.4 — Differenza tra IRR storico e YTM residuo

I due KPI tipicamente differiscono. Capire perché è fondamentale per usarli correttamente.

I tre fattori che generano lo scarto

  • PMC vs prezzo corrente: se il titolo si è apprezzato rispetto al PMC dell'utente (PMC < prezzo di mercato), l'utente ha pagato meno per cashflows futuri identici a quelli che pagherebbe oggi chi comprasse al prezzo di mercato → IRR storico > YTM residuo. Viceversa per titoli che hanno perso valore (PMC > prezzo di mercato).
  • Commissioni di acquisto: l'IRR storico include le commissioni come uscita aggiuntiva al carico; il YTM residuo no (è calcolato a commissioni zero) → ceteris paribus, per questa sola voce IRR storico < YTM residuo.
  • Cedole già staccate: per l'IRR storico, le cedole incassate fra la purchaseDate e oggi sono flussi positivi reali; per il YTM residuo (che parte dalla tradeDate corrente) queste cedole sono già "consumate" e non concorrono. L'effetto sull'IRR storico è positivo, particolarmente per cedole alte o holding lunghi.

In pratica i tre effetti operano in direzioni diverse e l'IRR può risultare maggiore o minore del YTM caso per caso. Lo scenario più frequente — titolo apprezzato dopo l'acquisto in un contesto di tassi calati — porta tipicamente a IRR storico > YTM residuo, perché l'effetto "prezzo" domina su quello commissionale.

Esempio: BTP comprato 5 anni fa

Scenario semplificato
BTP 4,5% scadenza fra 5 anni (cedola semestrale, 2,25 per 100 a coupon)
  Comprato 5 anni fa a 92,00 (PMC clean)
  Prezzo di mercato OGGI: 102,00 (sopra la pari, tassi calati nel frattempo)
  Cedola annuale netta ≈ 3,94 (4,5% × 87,5% = al netto del 12,5% statale)

╭──────────────────────────────────────────────────────╮
│ IRR STORICO (5y già passati + 5y futuri = 10y tot)   │
│   Flussi: -92, +3,94×10, +100 a t=10                 │
│   IRR ≈ 5,18% annuo                                  │
│   "Sto guadagnando il 5,18% annuo da quando l'ho     │
│   comprato"                                          │
╰──────────────────────────────────────────────────────╯

╭──────────────────────────────────────────────────────╮
│ YTM RESIDUO (5y futuri solamente, dal prezzo attuale)│
│   Flussi: -102, +3,94×5, +100 a t=5                  │
│   IRR ≈ 4,07% annuo                                  │
│   "Se ricomprassi oggi a 102, otterrei il 4,07%"     │
╰──────────────────────────────────────────────────────╯

⇒ IRR storico (5,18%) > YTM residuo (4,07%)
   perché il prezzo è salito e il rendimento residuo è inferiore

Quando uno è più alto dell'altro

SituazioneIRR storico vs YTM residuo
Prezzo è salito dopo l'acquisto (tassi scesi) IRR storico > YTM residuo
Prezzo è sceso dopo l'acquisto (tassi saliti) IRR storico < YTM residuo
Comprato vicino a prezzo di mercato attuale IRR storico ≈ YTM residuo
Comprato in asta a 100 e prezzo è ancora ~100 Coincidono entro frazioni di bps
▸ USO PRATICO

Per celebrare il passato (cosa ho guadagnato): IRR storico. Per decidere il futuro (vendere, comprare, switchare): YTM residuo. Il KPI di §06 Switch & Costi confronta sempre il YTM residuo della strategia A con l'IRR-B di una nuova strategia, perché entrambi partono da oggi a prezzo di mercato — confronto omogeneo.

4.5 — Ipotesi di reinvestimento implicita

Un punto sottile dell'IRR — spesso trascurato nei tool retail — è l'ipotesi di reinvestimento implicita. La formula NPV sconta i flussi futuri al tasso r: questo equivale ad assumere che ogni flusso intermedio (cedola, rimborso parziale) venga reinvestito al medesimo tasso r fino alla scadenza dell'investimento più lungo.

Implicazioni pratiche

  • Se le cedole vengono reinvestite a un tasso più basso di r, il rendimento realizzato sarà inferiore all'IRR riportato.
  • Se le cedole vengono reinvestite a un tasso più alto di r, il rendimento realizzato sarà superiore all'IRR riportato.
  • Se le cedole non vengono reinvestite (es. spese in consumi), l'IRR diventa puramente "matematico" e non descrive il rendimento del capitale netto investito.

Sensibilità della metrica

Quanto è realistica l'assunzione di reinvestimento dipende dallo scenario di tassi futuri:

  • In ambiente di tassi stabili (es. cedole 3-5% in un mercato che mantiene rendimenti simili), l'errore di reinvestimento è di solito modesto e l'IRR riflette bene il rendimento effettivo.
  • In ambiente di tassi in calo o in salita marcata, l'IRR può sovrastimare (calo) o sottostimare (salita) il rendimento realizzato. Per portafogli con duration lunga e cedole elevate l'errore è maggiore; per duration brevi o cedole basse è ridotto.
  • L'app non corregge automaticamente per questa imprecisione: per scenari "what-if" sul reinvestimento, l'utente deve fare valutazioni esterne.

Alternative

Esistono metriche alternative che evitano l'ipotesi di reinvestimento:

  • MIRR (Modified IRR): assume un tasso di reinvestimento esplicito (es. risk-free rate)
  • Holding-Period Return: rendimento aritmetico non annualizzato
  • Yield-to-Maturity da quotazioni di mercato: è già implicito nel prezzo

L'app non implementa MIRR. Per scelta, riporta l'IRR "standard" (con reinvestimento implicito al medesimo tasso) perché è la convenzione universale per il confronto inter-titolo. L'utente consapevole interpreterà il numero come "rendimento teorico massimo se riesco a reinvestire le cedole allo stesso tasso", non come "rendimento garantito".

⚠ LIMITE DICHIARATO

Tutti i confronti di IRR/YTM nell'app — incluso il KPI di switch Δ-rendimento in §06 — sono soggetti a questa ipotesi. Un Δ di 50 bps di IRR non equivale meccanicamente a 50 bps di rendimento realizzato extra in tasca: dipende da come vengono effettivamente reinvestite le cedole.

CAPITOLO 5Ottimizzatore §04

L'ottimizzatore in §04 cerca portafogli alternativi che partono dalla composizione attuale (selezione utente) e producono metriche di rendimento/rischio confrontabili o migliori. Non è un'ottimizzazione analitica deterministica: è una ricerca Monte Carlo casuale guidata da vincoli. Questo capitolo descrive le tre fasi: costruzione del pool, ricerca, scoring & filtro.

5.1 — Costruzione del pool candidato

Prima della ricerca, l'app costruisce il pool di titoli candidati: il sottoinsieme dell'universo che rispetta tutti i vincoli posti dall'utente nel pannello Vincoli. Solo i titoli del pool possono entrare nelle alternative.

Vincoli applicabili

Vincolo UIFiltro applicato
Valuta (multi-select)b.ccy ∈ ccys_set
Emittente (multi-select)b.issuerdescription ∈ issuers_set
Rating S&P (multi-select)b.ratingSP ∈ ratings_set
Status (multi-select)b.status ∈ status_set
Mercato (multi-select)b.marketcode ∈ markets_set
Segmento (multi-select)b.segmentcode ∈ segments_set
Vita residua minb.vr ≥ minVR (anni)
Duration netta min/maxminDur ≤ b.netDur ≤ maxDur
Scadenza massimab.redemptiondate ≤ maxDate

Più due requisiti tecnici sempre attivi:

  • b.hasCF = true (i flussi di cassa sono presenti nel file)
  • b.netytm finito (YTM netto calcolato)
  • Se metrica di rischio = zspread: b.zspread finito
  • Se metrica di rischio = duration: b.duration finita
  • Se metrica di rischio = both: entrambe finite

Vincoli vuoti = non vincolante

Se un campo multi-select non ha selezioni (set vuoto), il filtro non si applica. Questo permette di liberare un vincolo senza dover deselezionarne uno per volta.

▸ COMODITÀ — PREFILL

Sopra il pannello vincoli c'è un piccolo banner "Suggerisci dai miei titoli": clicca e l'app prefilla i multi-select coi valori distinti presenti nella selezione attuale (es. se hai BTP e CCT, preimposta segmentcode ∈ {BTP, CCT}). Utile per allentare un vincolo partendo dal portafoglio corrente invece che dal vuoto.

src/js/07-optimizer.js · _buildPoolFromConstraints() · buildOptimizerPool()

5.2 — Algoritmo di ricerca Monte Carlo

Una volta costruito il pool, l'app prova N iterazioni casuali (default 4.000, configurabile tra 200 e 20.000 tramite il campo Iterazioni) per scoprire combinazioni di titoli che migliorano il portafoglio base.

Dimensione del portafoglio campione

Range di numero titoli per campione
sizeMin = max(2, |baseSelection| − 1)
sizeMax = min(|pool|, max(|baseSelection| + 2, 6))

Per ogni iterazione:
  k = randInt(sizeMin, sizeMax)
  pesca k titoli distinti dal pool

L'app cerca portafogli di dimensione vicina a quella attuale (più o meno 1-2 titoli), con un floor di 2 e un cap di 6 quando il portafoglio iniziale è piccolo.

Allocazione dei lotti

Per ogni campione, l'app assegna inizialmente avgLots a ciascun titolo (media dei lotti del portafoglio base), poi riscala affinché il controvalore totale combaci con quello del portafoglio base (entro la tolleranza utente, default ±10%).

Riscalatura del campione al budget base
totalLotsBase = Σ lotti del portafoglio base
avgLots       = max(1, round(totalLotsBase / |baseSelection|))
              ← almeno 1 lotto per titolo, anche se la media è < 1

Per ogni campione:
  1. Assegna avgLots a ciascun titolo pescato
  2. Calcola m = computePortfolioMetrics(sample)
  3. scale = baseInvest / m.invest
  4. Per ogni titolo: lots ← round(lots × scale), almeno 1
  5. Ricalcola m con i lotti riscalati
  6. Se |m.invest − baseInvest| / baseInvest > tolBudgetPct → SCARTA

Deduplicazione

Ogni campione è identificato da una chiave canonica (ISIN ordinati | lotti). I campioni già visti sono saltati. Questo evita di valutare due volte lo stesso portafoglio in iterazioni diverse.

src/js/07-optimizer.js · runOptimizer() — loop principale

5.3 — Funzione obiettivo

L'app supporta 4 modalità di ottimizzazione selezionabili dal dropdown Modalità. Ognuna ha un criterio diverso per classificare un candidato come "migliore" del baseline.

Modalità (UI: dropdown)Etichetta UILogica
dominate
(default)
"Dominanza Pareto (IRR↑, rischio↓)" Il candidato deve dominare la baseline in senso di Pareto: non essere peggiore su nessuna dimensione (IRR ≥ base, rischio ≤ base) ed essere strettamente migliore su almeno una. Con riskMetric=both, le dimensioni sono tre (IRR, duration, zspread) e tutte e tre vanno rispettate.
sameYield "Stesso IRR, minor rischio" Mantiene |candidato.irr − base.irr| ≤ tolIRR (entro 5 bps) e richiede rischio strettamente inferiore al baseline. Per riskMetric=both: duration e zspread del candidato devono rientrare nelle rispettive tolleranze rispetto al baseline (cioè non peggiorare oltre tolleranza), e almeno una delle due deve essere strettamente inferiore.
sameRisk "Stesso rischio, maggior IRR" Mantiene |candidato.rischio − base.rischio| ≤ tol e richiede IRR strettamente superiore al baseline. Per riskMetric=both, entrambi duration e zspread devono restare nelle tolleranze.
ladder "Ladder (scala di scadenze)" Costruisce un portafoglio "scala" con maturità distribuite a intervalli regolari. Logica di selezione e filtraggio descritta in § 5.4.
⚠ NON CONFONDERE CON IL GENERATORE §05

Il Generatore §05 ha modalità chiamate maxIRR, minRisk, balanced, ladder, income: sono nomi simili ma diversi dalle modalità dell'Ottimizzatore §04. La differenza fondamentale è che l'Ottimizzatore parte dal portafoglio attuale e cerca varianti; il Generatore costruisce un portafoglio ex novo dato un budget e degli obiettivi. Vedi § 6.1 per il confronto completo.

Tolleranze adottate

Tolleranze applicate a sameYield e sameRisk
tolIRR = 0,0005     (5 bps di IRR)
tolDur = 0,10       (0,1 anni di duration)
tolZ   = 0,0005     (5 bps di Z-spread)

Dove si applicano:
  - dominate:  NON usa tolleranze. Usa un epsilon 1e-9 (precisione numerica)
               per evitare match spuri da arrotondamenti. La dominanza è "stretta"
               in senso matematico (non statisticamente significativa).
  - sameYield: usa tolIRR per definire "stesso IRR" (cioè |Δirr| ≤ tolIRR).
               Quando riskMetric=both, usa anche tolDur/tolZ per evitare che
               il candidato peggiori troppo l'altra dimensione di rischio.
  - sameRisk:  usa tolDur o tolZ per definire "stesso rischio" (cioè
               |Δrisk| ≤ tol). Per riskMetric=both, entrambe le tolleranze
               devono essere rispettate.

Queste tolleranze servono a non scartare candidati che sono solo marginalmente diversi dal baseline sulla dimensione vincolata.

Ordinamento e ranking dei candidati

Per ogni candidato sopravvissuto al filtro modalità, l'app calcola i delta rispetto al baseline che vengono mostrati nel pannello:

  • ΔIRR = candidato.irr − base.irr
  • Δduration = base.dur − candidato.dur (segno invertito: ridurre la duration è considerato vantaggio)
  • Δzspread = base.zspread − candidato.zspread

L'ordinamento dei candidati nel risultato finale NON è "IRR decrescente puro": l'app usa uno score composto dipendente da riskMetric e dalla modalità:

Score di ranking (dopo i filtri di modalità)
    mode='ladder'  : score = fitness + 0.5 × IRR
    mode≠'ladder', riskMetric='zspread'  : score = IRR − 1.0   × zspread
    mode≠'ladder', riskMetric='duration' : score = IRR − 0.001 × duration
    mode≠'ladder', riskMetric='both'     : score = IRR − 0.5   × zspread
                                                  − 0.0005 × duration

I candidati sono ordinati per score decrescente. Solo i primi 4 vengono mostrati nel pannello risultati come "alternative" cliccabili per applicarle come nuova selezione. Gli altri, anche se hanno superato i filtri, non sono visualizzati (ma il loro numero totale è riportato nel testo riepilogativo: "X candidati validi su Y campioni testati").

src/js/07-optimizer.js · runOptimizer() · renderOptimizerAlternatives()

5.4 — Fitness ladder — formula 0,5 / 0,3 / 0,2

La modalità ladder è qualitativamente diversa dalle altre: cerca portafogli con maturità distribuite a intervalli regolari (es. una scadenza ogni 2 anni dal 2026 al 2034), così che il flusso di rimborsi di capitale sia spalmato nel tempo. È una strategia di gestione del rischio di reinvestimento più che di rendimento.

Componenti della fitness

Per ogni candidato, l'app calcola un punteggio fitness ∈ [0, 1] che combina tre componenti normalizzate, ognuna in [0, 1]:

computeLadderFitness() — formula finale
fitness = 0,5 × rungAlignment
        + 0,3 × rungCoverage
        + 0,2 × weightUniformity

con:
  rungAlignment   = quanto le scadenze effettive coincidono con la griglia ideale
  rungCoverage    = frazione dei "gradini" ideali che hanno almeno un titolo
  weightUniformity = uniformità del peso (nominale) tra i gradini coperti

I pesi (0,5 / 0,3 / 0,2) riflettono la priorità progettuale: è più importante che le scadenze siano nei punti giusti (alignment) che il numero di gradini coperti, e i pesi sono un dettaglio secondario.

1. Rung alignment (peso 0,5)

Allineamento alle scadenze ideali
Griglia ideale: idealRungs[i] = startYear + i × intervalYears, per i = 0..numRungs-1

Per ogni titolo del candidato:
  bestRung   = arg min_i |maturity − idealRungs[i]|
  bestDelta  = min_i |maturity − idealRungs[i]|  (in ms)
  deviation_i = bestDelta / intervalMs  (frazione di intervallo)

totalDeviation = Σ deviation_i sui titoli
avgDeviation   = totalDeviation / k
rungAlignment  = max(0, 1 − avgDeviation)

Un candidato con 5 titoli che cadono esattamente sui 5 gradini ideali ha rungAlignment = 1. Se le scadenze sono mediamente a metà tra due gradini, rungAlignment ≈ 0,5. Se sono distanti almeno un intervallo intero (totalmente fuori griglia), rungAlignment = 0.

2. Rung coverage (peso 0,3)

Copertura dei gradini
coveredRungs   = numero di gradini ideali con peso totale > 0
rungCoverage   = coveredRungs / numRungs

Un candidato che concentra tutti i titoli su 2 gradini ideali (anche se perfettamente allineati) ha rungCoverage = 2 / numRungs. Questo penalizza i ladder "incompleti".

3. Weight uniformity (peso 0,2)

Uniformità del peso tra gradini
Per ogni gradino coperto, peso = Σ (lotti × minLot) dei titoli assegnati
mean        = media dei pesi dei gradini coperti
stddev      = deviazione standard dei pesi
weightDispersion = stddev / mean
weightUniformity = max(0, 1 − weightDispersion)

Un candidato con 5 gradini coperti e peso uguale su ognuno ha weightUniformity = 1. Se un gradino ha il 50% del peso e gli altri quattro il 12,5% ciascuno, la weightUniformity scende notevolmente. Questa componente cattura il principio "non concentrare la scadenza in un anno solo".

Filtro applicato in modalità ladder

L'app non si limita a ordinare i candidati per fitness: applica anche soglie di esclusione per evitare ladder "sacrificali" che peggiorano troppo le metriche standard:

Filtri di esclusione ladder
1. fitness ≥ 0,40                  (sotto questa soglia, non è un vero ladder)
2. irrLoss = baseM.irr − c.m.irr
   irrLoss ≤ 0,003                 (max 30 bps di IRR sacrificato)
3. cRisk ≤ baseRisk × 1,20         (max +20% di rischio accettato)

dove la formula di "rischio" dipende da riskMetric:
  riskMetric='zspread':  risk = zspread
  riskMetric='duration': risk = duration
  riskMetric='both':     risk = duration + zspread × 100
                         ← combinazione lineare arbitraria, non è uno z-spread
                           equivalente in punti — è solo un proxy comparativo

I candidati che superano questi tre filtri sono ordinati per fitness e mostrati come alternative ladder nel pannello risultati.

src/js/09-generator.js · computeLadderFitness() · pickBonds() ladder branch · src/js/07-optimizer.js · runOptimizer() ladder mode

CAPITOLO 6Generatore §05

Il generatore in §05 costruisce portafogli ex novo partendo da un budget e da obiettivi specifici, senza appoggiarsi a una selezione preesistente. È la controparte "creativa" dell'ottimizzatore: l'ottimizzatore migliora ciò che hai, il generatore propone ciò che non hai ancora.

6.1 — Differenza tra ottimizzatore e generatore

CaratteristicaOttimizzatore §04Generatore §05
Input principale Selezione attuale (§02) Budget in € + obiettivi
Budget target Match al controvalore della selezione attuale Importo libero deciso dall'utente
Pool Filtrato dai vincoli §04 (prefix "c") Filtrato dai vincoli §05 (prefix "g")
Allocazione lotti Inizia da avgLots, riscala al budget Iterativa greedy basata su weights obiettivo (vedi §6.2)
Obiettivi disponibili dominate, sameYield, sameRisk, ladder balanced, maxIRR, minRisk, ladder, income
Vincolo income mensile Soglia minima mensile + 4 modalità di tolleranza

Il generatore è quindi più adatto quando si parte da zero (es. nuova liquidità da impiegare) o quando si vuole esplorare configurazioni radicalmente diverse dal portafoglio attuale.

Parametri UI del Generatore

Campo UIIDDefaultRange / valori
Importo investibile (EUR) genBudget 50.000 min 1.000, step 1.000
Obiettivo genObjective balanced maxIRR, minRisk, balanced, ladder, income
N. titoli desiderato genCount 5 3, 5, 7, 10, 15
Termine ultimo operazione genMaxDate data opzionale: scadenze ≤ questa data
Iterazioni genIter 3.000 200 – 20.000, step 500

Il parametro genIter ha default 3.000, diverso dal default dell'ottimizzatore (4.000): il generatore esplora uno spazio più ampio (campionamento ex-novo dal pool intero) e in pratica raggiunge saturazione un po' prima. L'utente può comunque alzare le iterazioni fino a 20.000 se il pool è molto grande o i vincoli sono molto stringenti.

6.2 — Allocazione del budget

Una volta scelti i k titoli candidati per un'alternativa, l'app deve decidere quanti lotti assegnare a ciascuno per saturare il budget. La strategia varia per obiettivo, ma segue uno schema comune in più fasi, preceduto dalla selezione dei k titoli.

Fase 0: selezione dei k titoli (pickBonds)

Prima dell'allocazione lotti, l'app deve scegliere quali k titoli dal pool comporranno il candidato. La strategia è semplice e dipende dall'obiettivo:

  • Per objective='ladder': selezione "scala-aware". Se è specificato intervalYears, si costruisce una griglia target di k gradini e per ognuno si pesca il bond con maturity più vicina (con tiebreak random fra i 3 più vicini). Altrimenti il pool viene diviso per maturity in k bucket di popolazione uguale e si pesca uno a caso da ciascuno.
  • Per tutti gli altri obiettivi (maxIRR, minRisk, balanced, income): random sample senza replacement dal pool. Nessun tilt pre-selezione: i titoli "buoni" per l'obiettivo emergono dalla combinazione di allocazione (fase 3) + filtro/scoring di runGenerator.

In particolare per income: la selezione casuale può produrre molti candidati che non coprono la soglia mensile e vengono scartati da coverageOk. Aumentare il numero di iterazioni (genIter) migliora la probabilità di trovare candidati validi quando la soglia è alta.

Fase 1: costo unitario

Costo per lotto (stima)
Per ogni titolo b nel pick:
  cost_b = investedPerLot(b)   ← EUR per 1 lotto, da CF "Pagamento nominale"

minTotal = Σ cost_b   (1 lotto di ciascuno)

if minTotal > budget × 1,05:
  return null   ← infeasibile: il pick è troppo costoso. Riprova con meno titoli.

Fase 2: pesi obiettivo

Ogni titolo riceve un peso in base all'obiettivo, che determinerà la sua quota proporzionale del budget:

Pesi per obiettivo
balanced / ladder:  weight_b = 1                   ← uniforme

maxIRR:             weight_b = 1 + max(0, b.netytm) × 10
                    ↑ tilt verso titoli ad alto YTM

minRisk:            weight_b = 1 / (1 + b.zspread × 50 + b.duration × 0,05)
                    ↑ tilt verso titoli a basso rischio (z-spread + duration bassi)

income:             weight_b = 1                    ← uniforme
                    (la selezione è guidata da computeMonthlyIncome, non dai pesi)

I pesi non sono normalizzati: la somma può non essere 1. La normalizzazione avviene nella fase 3.

Fase 3: allocazione greedy

Allocazione iterativa
1. Calcola lo "want" ideale per ogni titolo:
     wants_b = (budget × weight_b / Σw) / cost_b
   = quanti lotti vorrei se distribuissi il budget secondo i pesi

2. Inizia con lots_b = 1 per ogni titolo (saturazione minima — già fatta in fase 1)

3. Loop greedy (max 10.000 iterazioni di safety cap):
   per ogni titolo i:
     gap_i = wants_i − lots_i
     se gap_i ≤ 0          → titolo "già saturo", non considerato
     se spent + cost_i > budget  → non ci sta nel budget residuo, scartato
   se nessun titolo è candidabile → STOP
   altrimenti scegli il titolo con gap_i MASSIMO,
              lots[i]++,  spent += cost_i

NOTA: il lotto minimo iniziale (1 per titolo) non viene mai rimosso, anche se
      il "wants" di quel titolo fosse < 1 — può capitare per titoli costosi
      con peso obiettivo basso. Il risultato è una lieve sovra-rappresentazione
      del titolo costoso, accettata come price-to-pay per garantire diversificazione.

Questo greedy è una buona euristica per il problema del knapsack multi-vincolo (budget come capacità, lotti come oggetti indivisibili). Non è ottimo nel senso stretto, ma in pratica produce allocazioni coerenti con i pesi target.

Tolleranza di budget

Dopo l'allocazione, il generatore scarta candidati che si discostano più del 20% dal budget target (sopra o sotto). Questa tolleranza è hardcoded nel generatore (a differenza dell'ottimizzatore, dove la tolleranza budget è un campo UI con default 10% ma modificabile dall'utente fino a qualsiasi valore).

La maggiore ampiezza è giustificata dal fatto che il generatore parte da zero e satura il budget con lotti interi non sempre allineati al target; mentre l'ottimizzatore parte già da un portafoglio attuale di cui replica approssimativamente la stessa dimensione, quindi può permettersi una tolleranza più stretta.

src/js/09-generator.js · allocateBudget() · pickBonds()

6.3 — Copertura mensile (obiettivo income)

L'obiettivo income è esclusivo del generatore. Cerca portafogli i cui flussi netti mensili coprano una soglia indicata dall'utente (es. "voglio almeno 500 €/mese netti dal portafoglio").

Costruzione del flusso mensile

computeMonthlyIncome() — algoritmo
1. Filtra i flussi del candidato:
   - ESCLUDE flussi di acquisto: Pagamento nominale, Rateo lordo,
     Storno tassazione rateo, Imposta sul disaggio, Commissioni
   - INCLUDE flussi di rendimento: cedole nette (= Incasso lorda + Tassazione),
     Rimborso nominale, Tassazione capital gain, Credito disaggio,
     Addebito disaggio

2. Aggrega per mese (chiave 'YYYY-MM') sommando gli amount netti
3. Itera dal primo al ultimo mese (anche se vuoti)
   → values[i] = importo netto del mese i

4. min      = min(values)
   avg      = media(values)
   monthsBelow = #mesi con value < threshold

Le 4 modalità di tolleranza

ModalitàCriterio coverageOk
strict monthsBelow == 0 — nessun mese può scendere sotto soglia
soft monthsBelow ≤ 1 — al più 1 mese sotto soglia
avg avgVal ≥ threshold — la media nel periodo basta
cumulative Banca virtuale: il saldo cumulativo (value − threshold) parte da zero e non deve mai scendere sotto zero. Equivale a dire "il surplus dei mesi precedenti può coprire il deficit del mese corrente, ma il surplus dei mesi futuri no". L'algoritmo si interrompe (break) al primo mese in cui il saldo diventa negativo.

La modalità cumulative riflette la logica di "accantonamento": nella vita reale si accantonano le cedole extra di un mese buono per pagare le spese di un mese magro. Va però usata con cognizione di causa, perché è asimmetrica nel tempo (vedi callout sotto). La modalità strict è eccessivamente restrittiva (filtra il 95%+ dei candidati realistici).

Esempio cumulative

Cumulative: la "banca" parte da zero e non può andare a debito
Threshold = 500 €/mese
                      Δmensile      Saldo cumulativo
Mese      Value      (value-thr)    (post-iterazione)
─────     ──────     ──────────     ─────────────────
gen        0          -500           -500   ← bank < 0: BREAK, coverageOk=FALSE

(L'algoritmo si ferma al primo mese in cui il saldo cumulativo va in negativo.)

──────────────────────────────────────────────────────────────────

Threshold = 500 €/mese, PROFILO INVERTITO (cedola arriva PRIMA)

Mese      Value      Δmensile       Saldo cumulativo
─────     ──────     ────────       ─────────────────
gen       1.200      +700           +700    ← surplus accumulato
feb         0        -500           +200    ← magro coperto dal surplus
mar       1.200      +700           +900
apr         0        -500           +400
mag       1.200      +700          +1.100
giu         0        -500           +600
⇒ Il saldo cumulativo non scende mai sotto 0. coverageOk = TRUE

──────────────────────────────────────────────────────────────────

Threshold = 400 €/mese (più basso, primo profilo)

Mese      Value      Δmensile       Saldo cumulativo
─────     ──────     ────────       ─────────────────
gen        0         -400           -400    ← bank < 0: BREAK, coverageOk=FALSE

(Anche con threshold più basso, se il primo mese ha valore zero il vincolo
fallisce immediatamente: la "banca" parte da zero e non c'è surplus pregresso.)
⚠ ASIMMETRIA DEL CUMULATIVE

Il vincolo cumulative è asimmetrico nel tempo: il surplus dei mesi precedenti può coprire il deficit del mese corrente, ma il surplus dei mesi futuri no. Conseguenza pratica: un portafoglio i cui flussi iniziano con un mese a zero euro non può mai passare il check cumulative, anche se i mesi successivi sono ricchissimi di cedole. Per portafogli con cedole "lumpy" semestrali o annuali, l'ordine dei mesi conta — se la finestra parte in un mese magro, scegliere avg al posto di cumulative.

L'utente può quindi decidere un livello di soglia "compatibile" col portafoglio cercando il punto di equilibrio: una soglia troppo alta causa infeasibleCount elevato; una troppo bassa rende l'obiettivo banale.

src/js/09-generator.js · computeMonthlyIncome() · readIncomeConstraint()

6.4 — Funzione di scoring

Quando il generatore ha collezionato i candidati validi, deve ordinarli per "qualità". Lo score combina il rendimento atteso, eventuali penalty e l'obiettivo scelto. Lo score è una metrica interna usata solo per il sort: i candidati top-4 (hardcoded) sono mostrati come alternative cliccabili per applicarle come nuova selezione.

Penalty di budget (comune a tutti gli obiettivi)

Penalty per scostamento dal budget
budgetDev      = |metrics.invest − budget| / budget
budgetPenalty  = (budgetDev − 0,15) × 5,   se budgetDev > 0,15
                 0,                         altrimenti

Una deviazione dal budget oltre il 15% inizia a penalizzare lo score. Sopra il 20% il candidato è già stato escluso (vedi §6.2), quindi il range effettivo di penalty è 0..0,25 in pratica.

Formule di score per obiettivo

scorePortfolio() per ciascun obiettivo
maxIRR:
  score = irr − budgetPenalty

minRisk:
  score = −zspread − 0,001 × duration + 0,2 × irr − budgetPenalty
          ↑ premia rischio basso, dà un piccolo bonus all'IRR

balanced:
  score = irr − 0,5 × zspread − 0,0005 × duration − budgetPenalty
          ↑ trade-off IRR vs rischio combinato

ladder:
  score = 0,10 × ladderFitness + 0,3 × irr − budgetPenalty
          ↑ peso pesante sul ladder, IRR è secondario

income:
  // Soglia "effettiva" per il scoring
  threshold = utente.monthlyThreshold (se > 0)
              altrimenti: monthly.avgVal × 0,5
              altrimenti (avgVal anche 0): fallback al puro maxIRR (vedi sotto)

  Se threshold > 0:
    minRatio      = min(1, monthly.minVal / threshold)   // clamp [0..1]
    avgRatio      = min(2, monthly.avgVal / threshold)   // clamp [0..2]
                    ↑ avg può superare la soglia di 2× e ricevere bonus pieno
  coverageScore = (2 × minRatio + avgRatio) / 3
                    ↑ pesa più il MIN (consistenza) della MEDIA
    score = 2 × coverageScore + 0,5 × irr − budgetPenalty

  Se threshold == 0 (e nessun avgVal positivo) → fallback:
    score = irr − budgetPenalty       // come maxIRR

Il peso 2 × minRatio rispetto a 1 × avgRatio riflette il principio "consistenza > media": un portafoglio con minimo mensile basso ma media alta è meno desiderabile di uno con minimo e media simili. Il cap a 2 per avgRatio impedisce a un portafoglio con media molto sopra soglia di "compensare" un minimo molto basso.

Penalty income trasversale

Indipendentemente dall'obiettivo, se un vincolo di income è attivo (threshold > 0), tutti i candidati ricevono una piccola penalty proporzionale al numero di mesi sotto soglia:

score -= monthsBelow × 0,0005

Questo "sconto" è quasi simbolico (0,5 bps per mese mancante), ma è abbastanza per spezzare i pareggi tra candidati altrimenti uguali a favore di quello con migliore copertura.

▸ DESIGN

Gli score non sono percentuali né tassi: sono punteggi relativi validi solo per l'ordinamento interno. Confrontare lo score di due candidati con obiettivi diversi non ha senso. Le metriche mostrate all'utente (IRR, Z-spread, duration, copertura) sono quelle assolute.

src/js/09-generator.js · scorePortfolio() · runGenerator()

CAPITOLO 7Switch & Costi §06

La sezione §06 analizza l'operazione tecnico-fiscale di switch tra due strategie: vendere il portafoglio attuale (strategia A) e comprare un portafoglio diverso (strategia B). È la sezione più complessa dell'app perché coinvolge: fiscalità delle plusvalenze (12,5% vs 26% con compensazione cross-rate), attrito di costi (commissioni di vendita + acquisto + tasse), e la metrica corretta per decidere se lo switch è conveniente nel tempo.

7.1 — Vendita della strategia A

Vendere una posizione obbligazionaria genera più componenti monetarie distinte. La funzione computeSalePerPosition() le calcola per ogni titolo della strategia A.

Componenti della vendita

Componenti monetarie della vendita di una posizione
nominal     = lotti × minLot
scale       = nominal / 10000              (CASH_FLOWS sono per 10.000 nominale)

1. CLEAN PROCEEDS (entrata)
     cleanProceeds = |"Pagamento nominale" del file| × scale
     = controvalore al prezzo clean × quantità
       (per i titoli sotto la pari include già il rateo di scarto di emissione)

2. ACCRUED RECEIVED (entrata)
     accruedReceived = |"Rateo lordo" del file| × scale
     = rateo cedolare che il compratore paga al venditore

3. TAX ON ACCRUED (uscita)
     accruedTax = "Storno tassazione rateo" del file × scale
     = imposta sul rateo (compete al venditore, opposto del rateo ricevuto)

4. CAPITAL GAIN TAX (uscita, se plus) — reddito DIVERSO, compensabile
     capGainTax = capGain > 0 ? capGain × taxRate/100 : 0     (vedi §7.2)

5. IMPOSTA SCARTO DI EMISSIONE (uscita) — reddito di CAPITALE, NON compensabile
     disaggioHoldingTax = max(0, disaggioDebitAtSale − creditoDisaggio)   (vedi §7.2)

6. COMMISSION (uscita)
     controvaloreVendita = cleanProceeds + accruedReceived  (lordo)
     commission          = calcComm(getCommParams(isin, bond), controvaloreVendita)
     (vedi cap. 3.3 per la formula calcComm)

NETTO (pre-compensazione):
  proceedsNet = controvaloreVendita − accruedTax − disaggioHoldingTax − capGainTax − commission

Nota su "Storno tassazione rateo": nel file CASH_FLOWS questo è un flusso positivo alla data di acquisto (credito d'imposta che il broker accantona sul rateo lordo pagato al venditore originario). In una vendita immediata da nostra posizione, il segno si inverte: noi diventiamo il venditore, quindi paghiamo l'imposta sul rateo che riceviamo dal nuovo compratore.

▸ DISAGGIO & SWITCH

Per i titoli sotto la pari (TF/ZC) il corso secco incorpora già il rateo di scarto di emissione. Ciò che si regola separatamente nei cash flow non è una componente di prezzo, ma l'imposta sostitutiva sullo scarto, tramite il meccanismo del conto unico (d.lgs. 239/96, art. 3):

  • all'acquisto il broker accredita l'imposta sullo scarto maturato da emissione a data acquisto ("Credito disaggio", segno +);
  • alla vendita addebita l'imposta sullo scarto maturato da emissione a data vendita;
  • il netto a carico del venditore = imposta sullo scarto del solo periodo di possesso, trattata come reddito di capitale e quindi non compensabile con le minusvalenze.

Questa imposta è esposta come voce a sé (disaggioHoldingTax) e sottratta dal netto. Il capital gain (§7.2) cattura quindi solo il movimento di prezzo di mercato, senza lo scarto di possesso — che altrimenti verrebbe contato due volte.

src/js/08-switch.js · computeSalePerPosition()

7.2 — Capital gain — formula D.Lgs. 461/97

La vendita anticipata di un titolo genera plusvalenza o minusvalenza rispetto al prezzo di carico fiscale. Il D.Lgs. 461/97 impone una formula non banale per il calcolo del PrezzoCaricoCG e PrezzoScaricoCG, che non coincide col PMC nudo e crudo: bisogna "scorporare" il disaggio di emissione, perché viene tassato separatamente a scadenza.

Convenzione — prezzo teorico composto, scarto lineare

Prezzo di carico fiscale (per 100)
tradeDate     = data originaria di acquisto del titolo A
                (letta dal CASH_FLOWS, prima riga "Pagamento nominale" —
                NON è la data dello switch; può essere ridefinita
                implicitamente dall'override delle tranches in §06)
issueP        = bond.issueprice          (prezzo di emissione, default 100)
redemptionP   = bond.redemptionprice     (prezzo di rimborso, default 100)
bondLifeDays  = redemptiondate − issuedate         (giorni vita totale)
daysSinceIssue = tradeDate − issuedate              (giorni dall'emissione all'acquisto)

teoricPrice   = issueP × (redemptionP / issueP)^(daysSinceIssue / bondLifeDays)
                ↑ prezzo teorico in capitalizzazione COMPOSTA
                  (fonte: pt = pe·(pr/pe)^frac — lo scarto, ai fini del prezzo
                   teorico per il capital gain, matura in regime composto)

prezzoCaricoCG = purchasePricePer100 − (teoricPrice − issueP)
                ↑ purchase pulito dallo scarto già maturato all'acquisto
                  (la fonte aggiunge anche + co% per le commissioni d'acquisto;
                   qui il PMC dai cashflows è clean — vedi callout ASIMMETRIA PMC)

⚠ Se issueP >= redemptionP: teoricPrice = issueP costante (nessuno scarto).
Prezzo di scarico fiscale (per 100) — SIMMETRICO al carico
Lo scarico è ancorato allo STESSO teoricPrice del carico (data di ACQUISTO),
NON a un teorico ricalcolato a oggi. Lo scarto del periodo di possesso è
reddito di capitale ed è già tassato dal conto unico (vedi §7.1 e sotto):
includerlo anche qui sarebbe doppio conteggio.

Per bond cedolari (no ZC/BOT):
  prezzoScaricoCG = salePricePer100 − (teoricPrice − issueP)

Per ZC e BOT (nessun prezzo di mercato di scarico):
  prezzoScaricoCG = prezzoCaricoCG   →  capGain = 0

diffCGPer100 = prezzoScaricoCG − prezzoCaricoCG
             = salePricePer100 − purchasePricePer100   (il termine scarto si cancella)
capGain      = diffCGPer100 / 100 × nominal
             ↑ cattura SOLO il movimento di prezzo di mercato

capGainTax   = capGain > 0 ? capGain × (taxRate/100) : 0
               ↑ minusvalenze: tax = 0 ma capGain resta negativo per compensazione
Imposta sostitutiva sullo scarto di possesso (conto unico, d.lgs. 239/96)
scartoPoints     = (redemptionP − issueP)            (punti prezzo; >0 solo sotto la pari)
                   ↑ base (pr−pe), NON normalizzata per pr (fonte)
magnitudePerDay  = scartoPoints/100 × taxRate/100 × 10.000 / bondLifeDays
                   ↑ imposta/giorno per 10.000 nominali (stessa base dei cash flow)
daysIssueToSale  = oggi − issuedate

disaggioDebitAtSale = magnitudePerDay × daysIssueToSale × scale      (addebito alla vendita)
creditoDisaggio     = "Credito disaggio" dai cash flow × scale       (accredito all'acquisto, +)

disaggioHoldingTax  = max(0, disaggioDebitAtSale − creditoDisaggio)
                      ↑ imposta NETTA sul solo possesso · reddito di CAPITALE · NON compensabile
                      Solo TF/ZC sotto la pari; per i BOT è anticipata e già nel prezzo.
⚠ ASIMMETRIA PMC

Il purchasePricePer100 usato come PMC è di default il prezzo clean per 100 derivato dai cashflows (−Pagamento nominale × 100 / 10.000, vedi § 4.2), oppure un valore di override (overridePMCPer100) calcolato da computeWeightedPMC() sulle tranches inserite dall'utente in §06 (pannello Prezzi di carico).

Per costruzione, il PMC derivato dai cashflows è la sola componente clean — non include il rateo né le commissioni di acquisto storiche. Per gli switch su posizioni vecchie con commissioni di acquisto significative o tranching complesso, conviene compilare le tranches in §06 per ottenere un override PMC più aderente al carico fiscale reale.

▸ FALLBACK PREZZO DI MERCATO

Se per qualche motivo bond.price non è disponibile (prezzo live non fetchato, EOD assente per il titolo), l'app usa come salePricePer100 il PMC stesso. Conseguenza: in mancanza di prezzo di mercato, capital gain ≡ 0 per costruzione. È un fallback prudenziale che evita errori grossolani, ma può sottostimare il payback effettivo: aggiornare i prezzi live prima di interpretare i numeri.

Neutralità della formula

Con l'ancoraggio simmetrico, la formula è neutra per costruzione nel caso limite "stesso prezzo di acquisto e vendita", qualunque siano le date: se salePrice = purchasePrice, il termine (teoricPrice − issueP) si cancella identicamente fra carico e scarico, quindi diffCG = 0 e nessuna tassa da capital gain (la cancellazione vale per qualunque forma di teoricPrice, lineare o composta, perché carico e scarico usano lo stesso ancoraggio). Lo scarto eventualmente maturato durante il possesso non finisce nel capital gain: è gestito esclusivamente da disaggioHoldingTax.

Nota: nel modello precedente lo scarico era ancorato a un teorico ricalcolato alla data odierna (teoricPriceAtSale). Poiché tradeDate è una data storica (≠ oggi), questo generava un capital gain residuo pari allo scarto di possesso — contandolo due volte (una come reddito diverso compensabile, una come reddito di capitale). L'ancoraggio simmetrico elimina il doppio conteggio.

src/js/08-switch.js · computeSalePerPosition() — sezione Capital gain

7.3 — Compensazione cross-rate 12,5/26

Una volta calcolato il capital gain per ogni posizione, computeCapGainCompensation() applica le regole di compensazione: prima compensazione same-rate (plus e minus della stessa aliquota), poi compensazione cross-rate tra le due aliquote applicando un fattore correttivo proporzionale alle aliquote stesse.

Pre-requisiti: aliquota per posizione

Ogni posizione di A ha una taxRatePct (12,5 o 26) proveniente da una pipeline a 3 livelli:

  1. Override utente: la select per-ISIN nel pannello Prezzi di carico §06, valori ammessi 12.5 o 26.
  2. Inferenza automatica: la funzione inferTaxRateFromCashflows() (cfr. § 3.4) che ricava il tax rate dai ratios tassazione/lordo del file CASH_FLOWS, e popola la select con il valore inferito al caricamento.
  3. Fallback globale: se per qualche motivo la select non è popolata, si usa l'aliquota di default impostata a livello globale (default 26% prudenziale, modificabile in §06).

Algoritmo a 2 step

Step 1 — Netting per aliquota
Per ogni posizione, classifica in base a taxRatePct:
  rate < 20%  → aliquota 12,5%  (titoli di Stato e sovranazionali)
  rate ≥ 20%  → aliquota 26%   (corporate e altro)

Somma le plusvalenze (cg > 0) e le minusvalenze (cg < 0, prese in modulo):
  gains12, losses12, gains26, losses26

Netto per aliquota:
  net12raw = gains12 − losses12     (può essere positivo o negativo)
  net26raw = gains26 − losses26

Tax lordo (pre-compensazione cross-rate):
  tax12gross = max(0, net12raw) × 0,125
  tax26gross = max(0, net26raw) × 0,26
Step 2 — Compensazione cross-rate (regola di mercato)
La regola corretta nella prassi italiana è simmetrica nelle due direzioni,
ma con fattori inversi.

DIREZIONE A — Minus 12,5% residua compensa plus 26%:
  1€ di minus 12,5% abbatte (12,5/26)€ di plus 26%       ← fattore 0,4808
  ↑ Direzione "ridotta": il fattore < 1 perché si parifica
    il peso fiscale della minus (12,5%) alla plus (26%).
  Esempio: 1.000€ di minus 12,5% offset 480,77€ di plus 26%.
           Minus utilizzata: 1.000€, plus 26% residua: 519,23€

DIREZIONE B — Minus 26% residua compensa plus 12,5%:
  1€ di minus 26% abbatte (26/12,5)€ di plus 12,5%       ← fattore 2,08
  ↑ Direzione "amplificata": il fattore > 1 perché si parifica
    il peso fiscale della minus (26%) alla plus (12,5%).
  Esempio: 1.000€ di minus 26% offset 1.000€ di plus 12,5%
           con SOLO 480,77€ di minus utilizzata.
           Minus residua 26%: 519,23€
⚠ NORMATIVA E PRUDENZA INTERPRETATIVA

Il quadro normativo italiano è disciplinato dal D.Lgs. 461/97 art. 6 c.5 (riportabilità minusvalenze per 4 esercizi) e dalla manovra di Ferragosto 2011 (D.L. 138/2011 conv. L. 148/2011, art. 2 c. 28), con aggiornamento al 26% dal 1/7/2014 (D.L. 66/2014).

La direzione A (minus 12,5% → plus 26%) è pacificamente applicata in regime amministrato dagli intermediari italiani con il fattore correttivo 12,5/26.

La direzione B (minus 26% → plus 12,5%) col fattore 26/12,5 è meno frequente in regime amministrato: alcuni intermediari la consentono solo in regime dichiarativo, altri la negano del tutto. L'app riporta il calcolo matematico in entrambe le direzioni; l'utente è responsabile della verifica dell'applicabilità alla propria posizione con l'intermediario o il commercialista.

Esempio numerico

Switch con minus 26% disponibili (Direzione B)
Posizioni:
  BTP venduto in plus  →  capGain = +1.000 €  →  rate 12,5%
  Corp. obb. in loss   →  capGain = −2.500 €  →  rate 26%

Step 1 — netting same-rate:
  net12raw = +1.000        net26raw = −2.500
  tax12gross = 1.000 × 0,125 = 125 €
  tax26gross = 0  (net negativo, nessuna tassa)

Step 2 — cross-rate Direzione B (minus 26% → plus 12,5%):
  residualLoss26  = 2.500
  net12taxable    = 1.000
  capacity12      = residualLoss26 × (26/12,5) = 2.500 × 2,08 = 5.200
                    ↑ capacità teorica di offset, ma plus 12,5% disponibile è solo 1.000

  crossOffsetOn12 = min(net12taxable, capacity12) = min(1.000, 5.200) = 1.000
                    ↑ tutto l'utile su BTP viene compensato

  lossUsed26      = crossOffsetOn12 × (12,5/26) = 1.000 × 0,4808 = 480,77
                    ↑ minus 26% effettivamente consumata (in euro originari)

  crossSaving     = 1.000 × 0,125 = 125 €
  tax12           = (1.000 − 1.000) × 0,125 = 0
  tax26           = 0
  totalTax        = 0
  saving          = 125 − 0 = 125 €

Minus 26% residua = 2.500 − 480,77 = 2.019,23 €
                    ↑ disponibile per altre compensazioni future (entro 4 esercizi)
▸ NOTA SUI RESIDUI

L'app calcola correttamente la tassa oggi dopo lo switch ma non traccia i residui di minusvalenze non utilizzate, che per legge si possono portare avanti per 4 esercizi fiscali. L'utente che ha minus residue da precedenti operazioni dovrebbe valutarne l'utilizzo separatamente in dichiarazione, fuori dall'app.

src/js/08-switch.js · computeCapGainCompensation()

7.4 — Acquisto della strategia B

L'acquisto della strategia B è in pratica una replica del flusso di apertura standard del §03, applicato al portafoglio target. Le voci che rilevano per lo switch sono solo quelle che cadono il giorno dello switch (la tradeDate di B):

Componenti dell'acquisto di B (alla tradeDate)
Per ogni titolo in targetSel, sommatorie sul giorno dello switch:

  purchaseNominal  = Σ "Pagamento nominale"           (negativo, l'uscita principale)
  purchaseAccrued  = Σ "Rateo lordo"                  (negativo, rateo pagato al venditore)
  purchaseComm     = Σ "Commissioni utente"           (negativo, broker fee per acquisto)
  purchaseOther    = Σ delle altre voci day-0:
                       + Storno tassazione rateo      (positivo: credito d'imposta sul rateo
                                                       pagato, = aliquota × Rateo lordo)
                       + Credito disaggio             (positivo: credito sul disaggio già
                                                       tassato a monte)
                       − Imposta sul disaggio         (negativo: anticipo dell'imposta sul
                                                       disaggio residuo a scadenza)
                     Il netto di purchaseOther può essere positivo o negativo
                     a seconda del titolo (cedolare vs ZC/BOT, sopra o sotto la pari).

purchaseDay0 = purchaseNominal + purchaseAccrued + purchaseComm + purchaseOther
purchaseCost = − purchaseDay0   (positivo: cassa totale necessaria per comprare B)

Il purchaseCost è la cassa che l'utente deve effettivamente mettere sul tavolo per acquistare la strategia B, al netto del credito d'imposta sul rateo (Storno tassazione rateo) e delle eventuali regolazioni del disaggio. È diverso dal "prezzo dirty lordo" (clean + accrued + commissioni), che è invece la spesa prima di queste compensazioni.

Il switchCost è la differenza tra questo e quello che si incassa dalla vendita di A:

switchCost = purchaseCost − sale.totalProceedsNet

   se > 0 : l'utente deve mettere cassa extra
   se < 0 : l'utente avanza cassa rispetto al controvalore di A

src/js/08-switch.js · computeSwitchAnalysis() — sezione Cost of buying B

7.5 — Attrito economico dello switch

L'attrito è il costo "irrecuperabile" dello switch: il denaro che esce dal nostro patrimonio per effettuare l'operazione, non per riposizionare il capitale. È la grandezza usata per calcolare il payback (cap. 7.7).

Formula

Switch friction — componenti irrecuperabili
switchFriction = sale.compensation.totalTax     ← tassa netta su capital gain (dopo cross-rate)
               + sale.totalDisaggioHoldingTax   ← imposta sostitutiva sullo scarto di emissione
                                                  (reddito di capitale, NON compensabile)
               + sale.totalCommission           ← commissioni di vendita A
               + |purchaseComm|                 ← commissioni di acquisto B

Cosa NON è attrito

Importante: non entrano nell'attrito:

  • Il rateo pagato comprando B: è denaro che si recupera al prossimo stacco cedola di B. È una temporanea uscita di cassa, non una perdita.
  • La differenza di nominale tra A e B: è semplice riallocazione del capitale, non perdita.
  • La differenza tra prezzo clean di A e quello di B: idem, riallocazione.
  • La tassa sul rateo incassato vendendo A (accruedTax): è una uscita di cassa reale al momento dello switch, ma non un costo aggiuntivo causato dallo switch. Per ragioni economiche dettagliate vedi §7.5.1 sotto.
⚠ ASIMMETRIA STORICA

Un piccolo dettaglio fiscale che l'utente esperto noterà: il capital gain dello switch è calcolato sulla differenza scarico − carico, ma le commissioni storiche di acquisto (eventualmente pagate quando A è stato comprato) non vengono incluse nel PMC implicito a meno che l'utente non le abbia sovrascritte manualmente. Per gli switch su posizioni vecchie con commissioni significative, conviene impostare manualmente il PMC effettivamente sostenuto, altrimenti l'app sovrastima leggermente la plusvalenza (e quindi l'attrito).

src/js/08-switch.js · computeSwitchAnalysis() — variabile switchFriction

7.5.1 — Perché accruedTax non è attrito

La tassa sul rateo incassato vendendo A — accruedTax = aliquota × rateoLordoIncassato — è una delle voci più discusse del calcolo. È un'uscita di cassa reale al momento dello switch (l'utente vede meno soldi in conto), e l'intuizione iniziale è includerla nell'attrito. La precedente versione dell'app lo faceva. Tuttavia un'analisi più rigorosa mostra che è una scorrettezza concettuale che gonfia sistematicamente il payback.

Il caso degenere: switch su titolo identico

Il modo più chiaro di vedere il problema è considerare uno switch "ipotetico" verso un titolo identico: stessa ISIN, stesso prezzo, stessa data. Economicamente non sta succedendo nulla: il capitale resta nello stesso bond. L'unico costo reale è quello delle due commissioni (vendita + riacquisto). Ma con la vecchia formula:

Obbligazione cedola 6,5% annuale, 100k nominale, rateo lordo 3.250 (metà periodo). Switch identico.
Vendita A:
  cleanProceeds      = +100.000
  accruedReceived    =   +3.250
  accruedTax         =     −406,25      ← 12,5% × 3.250
  sellComm           =      −50
  proceedsNet        = +102.793,75

Acquisto B (stesso titolo, stessa data, stesso prezzo):
  Pagamento nominale = −100.000
  Rateo lordo        =   −3.250
  Storno tassazione  =     +406,25      ← 12,5% × 3.250  (credito al buyer)
  buyComm            =      −50
  purchaseCost       = +102.893,75

switchCost (cassa) = purchaseCost − proceedsNet = 100,00 €   ✓ corretto (solo le 2 commissioni)

switchFriction VECCHIA = 406,25 + 0 + 50 + 50 = 506,25 €    ✗ sovrastima di 406,25
switchFriction NUOVA  = 0      + 0 + 50 + 50 =  100,00 €    ✓ corretto

NOTA: l'esempio usa cedola annuale per semplicità. Per BTP/CCT a cedola
semestrale la logica è identica con periodi cedolari più brevi (3.250 €
sarebbe in quel caso la cedola semestrale intera, non un rateo intermedio).

L'argomento economico

L'accruedTax rappresenta la tassazione del reddito maturato fino allo switch. Quella tassa l'avrei pagata comunque, in due modi diversi:

  • Tenendo A: al prossimo stacco cedola pago aliquota × cedola_lorda_semestre_intero; in questa cifra è inclusa aliquota × rateo_maturato_durante_il_possesso, più la quota del periodo che sarebbe maturata dopo. Il sistema fiscale è progettato così: chi vende cede al compratore la quota residua e ne paga la tassa pro-rata oggi.
  • Vendendo A in switch: pago oggi aliquota × rateo_incassato. Il nuovo compratore riceve a sua volta uno "Storno tassazione rateo" simmetrico, riducendo il proprio futuro carico fiscale alla cedola successiva.

La somma di queste due quote ha lo stesso valore presente attuale (a meno di un trascurabile effetto di time value su qualche settimana). L'accruedTax è quindi un'anticipazione di una tassa altrimenti differita, non un costo extra.

La simmetria del modello: già contato in switchCost

Lato acquisto B, il flusso Storno tassazione rateo nei CASH_FLOWS è un credito per il nuovo compratore: pari a aliquota_B × rateo_pagato_per_B. Questo credito è già incluso nel calcolo di purchaseCost (l'utente versa meno cassa per acquistare B). Nel switchCost finale (cassa netta), i due effetti — accruedTax in vendita A e storno_tax_rateo in acquisto B — si compensano correttamente.

Se l'app contasse accruedTax anche in switchFriction, ridoppierebbe quell'importo dal punto di vista del payback: la cassa serve già nel switchCost, e l'effetto "asimmetrico" eventuale (cambio aliquota tra A e B) emerge automaticamente dalla differenza di proceeds/cost senza bisogno di contarlo a parte.

▸ COME VEDE L'UTENTE QUESTA QUANTITÀ

L'app continua a mostrare la accruedTax totale come voce informativa separata nel pannello §06 ("Tassa sul rateo vendita A — informativa, non attrito"). Quel numero indica quanta cassa effettivamente esce dal conto al momento dello switch per ragioni fiscali sul rateo, anche se non costituisce attrito economico nel senso del payback. È utile per la pianificazione di tesoreria, ma non per giudicare la convenienza dello switch.

Implicazioni del cambiamento

Con la formula corretta, su switch che coinvolgono BTP/CCT venduti a metà periodo cedolare, il paybackYears tipicamente si accorcia di parecchio (l'overstatement era ~0,4% del nominale per BTP con cedola 6,5% venduto a metà periodo). I confronti tra strategie con e senza rateo significativo diventano più onesti. La metrica switchCost e Δyield restano invariati: solo switchFriction e di conseguenza paybackYears sono toccati.

7.6 — La metrica corretta: IRR-B vs YTM-A

Questa è forse la distinzione tecnica più importante del capitolo. L'app NON calcola un fantomatico "IRR dello switch" perché sarebbe matematicamente ambiguo (la serie temporale è incompleta). Invece confronta due IRR separati, entrambi calcolati a partire dalla tradeDate dello switch:

IRR-B — la nuova strategia come investimento fresco

IRR di B come acquisto nuovo (oggi → scadenza)
flussi_B = built.agg   (aggregato del portafoglio B costruito da buildPortfolioCashflows)

irrB = irrACT365(flussi_B)

interpretazione:
  "Se prendessi i soldi necessari per comprare B (= purchaseCost) e li
   investissi oggi in B, quale rendimento annualizzato otterrei tenendolo
   a scadenza?"

YTM-A — il costo di opportunità di vendere

YTM di A dal mercato (oggi → scadenza)
flussi_A_futuri = solo flussi di A successivi a tradeDate
totalMarketValue_A = Σ controvaloreVendita su tutte le posizioni di A

cf_series_A = [{date: tradeDate, amount: -totalMarketValue_A}, ...flussi_A_futuri]
ytmA = irrACT365(cf_series_A)

interpretazione:
  "Se NON vendessi A e mi limitassi a tenerlo valutato al prezzo di
   mercato di oggi, quale rendimento residuo otterrei fino a scadenza?"

Da notare: il YTM-A usa aliquota neutra 12,5% e commissioni a zero (è una
vendita VIRTUALE, non un'esecuzione reale). Vedi §4.3.

Confronto omogeneo

Entrambi IRR partono da oggi, a prezzo di mercato, su un orizzonte uguale (fino alla scadenza più lontana). Il confronto è quindi statisticamente onesto:

deltaYield = irrB − ytmA

   se > 0 : la strategia B rende più di A (su base annuale composta)
   se < 0 : la strategia B rende meno di A
   se ≈ 0 : i due rendimenti sono equivalenti
▸ ATTENZIONE

deltaYield è un Δ di tassi annuali composti, non di euro. Per tradurlo in euro/anno serve la moltiplicazione per il capitale investito (vedi 7.7), e va comunque interpretato alla luce dell'ipotesi di reinvestimento (vedi 4.5).

src/js/08-switch.js · computeSwitchAnalysis() — variabili irrB, ytmA, deltaYield

7.7 — Vantaggio annuale e breakeven

Il vantaggio annuale traduce il Δ-yield in euro/anno sul capitale effettivamente investito in B:

Annual advantage
annualAdvantage = deltaYield × purchaseCost

  ↑ moltiplichiamo per purchaseCost (= cassa per comprare B) e NON per
    totalValueA (= valore lordo di A), perché dopo lo switch il capitale
    che produce rendimento è quello che è entrato effettivamente in B,
    non il "valore di A prima di tasse e commissioni".

Esempio:
  deltaYield = +0,5% annuo
  purchaseCost = 100.000 €
  → annualAdvantage = 500 €/anno

Breakeven dello switch

Il breakeven (o payback) è il numero di anni necessari affinché il vantaggio annuale di B compensi l'attrito iniziale dello switch:

Payback in anni
paybackYears = switchFriction / annualAdvantage

  definito solo se:
    - switchFriction > 0 (esiste un costo non zero da recuperare)
    - annualAdvantage > 0 (B effettivamente rende più di A)

  Altrimenti: paybackYears = NaN (lo switch non si "ripaga" mai con il
  delta yield, e quindi non conviene farlo solo per ragioni di rendimento;
  potrebbe avere altre motivazioni come riduzione duration, miglioramento
  del rating, ecc.)

Esempio numerico — switch convenienza

Switch da BTP a 3y verso BTP a 5y a tassi saliti
Strategia A: BTP scadenza 2027, prezzo 96 (acquistato a 100)
Strategia B: BTP scadenza 2030, prezzo 92 (acquisto nuovo)

Vendita A:
  cleanProceeds        =  96.000
  accruedReceived      =     500
  controvaloreVendita  =  96.500
  accruedTax           =      63   (12,5% × rateo)
  capGain              =  -4.000   (prezzo < PMC)
  capGainTax           =       0   (minus, no tax dovuta)
  commission           =      50
  proceedsNet          =  96.387

Acquisto B:
  purchaseNominal      = -92.000
  purchaseAccrued      =    -300
  purchaseComm         =     -50
  purchaseOther        =    +37,5   (storno tassazione rateo = 12,5% × 300)
                                    (credito disaggio = 0, imposta disaggio = 0:
                                     BTP non sotto la pari)
  purchaseDay0         = -92.312,5
  purchaseCost         =  92.312,5

Attrito switch (formula NUOVA, senza accruedTax):
  switchFriction = cgTax(0) + disaggioHoldingTax(0) + sellComm(50) + buyComm(50) = 100
  (qui disaggioHoldingTax = 0 perché il BTP non è sotto la pari; per un titolo
   acquistato sotto la pari comparirebbe l'imposta sostitutiva sullo scarto di emissione)

(Voce informativa separata, NON inclusa nell'attrito:
   Tassa sul rateo vendita A = accruedTax = 63 €
   → cassa-out reale, ma già contata in switchCost
     e non riflette un costo aggiuntivo dello switch — vedi §7.5.1)

Confronto rendimenti:
  ytmA = 4,10%  (YTM residuo di A su 3y, dal mercato)
  irrB = 4,55%  (IRR di B come investimento fresco su 5y)
  deltaYield = +0,45%

Annual advantage:
  annualAdvantage = 0,0045 × 92.312,5 ≈ 415 €/anno

Payback:
  paybackYears = 100 / 415 = 0,241 anni ≈ 3 mesi
  ↑ lo switch si ripaga in 3 mesi: molto conveniente
  (con la vecchia formula sarebbe stato 0,39 anni — 5 mesi —
   per via dell'errato conteggio dell'accruedTax)

Inoltre la minus di 4.000 € su A resta disponibile per compensare
future plusvalenze 26% (entro 4 esercizi).
▸ COMPLETEZZA DEL PAYBACK

Il payback include solo l'attrito monetario diretto. Non include: (a) il rischio della scelta sbagliata (Δyield reale futuro diverso da quello stimato oggi); (b) il valore della perdita di minus pregresse consumate nel cross-rate; (c) cambiamenti di duration/rischio del portafoglio. Sono valutazioni complementari da fare separatamente.

src/js/08-switch.js · computeSwitchAnalysis() — variabili annualAdvantage, paybackYears

CAPITOLO 8Prezzi live e auto-download

L'app supporta due forme di accesso a dati esterni: il download automatico dei file CSV dal sito sorgente e i prezzi live da Borsa Italiana per i singoli ISIN. Entrambi richiedono un proxy CORS per aggirare le restrizioni del browser su richieste cross-origin. Questo capitolo descrive come funzionano e quali limiti hanno.

8.1 — Il problema CORS e la soluzione proxy

Per ragioni di sicurezza, i browser bloccano le richieste fetch da una pagina servita da file:// o da localhost verso domini esterni che non dichiarano l'header Access-Control-Allow-Origin. Sia simpletoolsforinvestors.eu che borsaitaliana.it rientrano in questa categoria.

L'app aggira il limite passando ogni richiesta attraverso un CORS proxy: un servizio terzo che riceve la URL target, la fetcha lato server e ritorna la risposta col CORS header corretto. L'app prova i proxy nell'ordine, usando il primo che risponde con successo.

Lista dei proxy configurati

CORS_PROXIES — definito in src/js/05-live-prices.js
const CORS_PROXIES = [
  'https://api.codetabs.com/v1/proxy?quest=',
  'https://go.x2u.in/proxy?email=...&apiKey=...&url=',
];

Il primo è un servizio gratuito (codetabs); il secondo richiede una API key personale che l'utente ha configurato. L'app prova sempre in quest'ordine: se codetabs è online e veloce, x2u non viene mai chiamato. Se codetabs è giù, x2u è il fallback.

Nel sorgente sono presenti, commentati, anche corsproxy.io, api.allorigins.win e cors-anywhere.com: sono state opzioni di backup testate in passato e poi disabilitate (cambio di policy sui referer, necessità di parsing JSON wrapper, instabilità). Possono essere riattivate scommentando la riga corrispondente in caso di disservizio dei due proxy attivi.

⚠ DIPENDENZA ESTERNA

I servizi proxy sono servizi pubblici di terze parti su cui l'app non ha controllo. Possono cambiare URL, andare giù, introdurre rate limiting o cambiare politiche di utilizzo. L'utente è avvisato della dipendenza in fase di configurazione iniziale. Se entrambi i proxy falliscono, l'app non ha alternative automatiche: l'utente deve scaricare manualmente i file dal sito sorgente.

Timeout e error handling

L'app applica due timeout distinti a seconda della dimensione attesa del payload e del flusso chiamante:

Robustezza del fetch via proxy — due flussi
Flusso auto-download CSV/ZIP (via _fetchViaProxy):
  AbortController timeout = 45 000 ms (45 s)
  ↑ file CSV/ZIP del sito sorgente possono essere 5-10 MB

Flusso prezzi live Borsa (via fetchLivePriceViaProxy):
  AbortController timeout = 12 000 ms (12 s)
  ↑ pagina HTML di Borsa Italiana è leggera (50-200 KB)
  ↑ il timeout stretto evita che l'utente aspetti troppo
    in §02/§03 dopo aver cliccato sul pulsante "Borsa"

Per ogni proxy nella lista (in ordine):
  1. fetch(proxy + encodeURIComponent(url))   con il timeout applicabile
  2. Se status != 200: passa al proxy successivo
  3. Se la risposta inizia con '{': può essere un JSON wrapper
     (es. allorigins.win usa {"contents":"<HTML>...", "status":{...}})
     → parse JSON e usa il campo contents
  4. asArrayBuffer=true (solo flusso auto-download per ZIP) → ritorna ArrayBuffer
     Altrimenti → ritorna text

Se tutti i proxy falliscono: throw new Error('Tutti i proxy CORS hanno fallito')

src/js/12-fetch-site.js · _fetchViaProxy() · src/js/05-live-prices.js · CORS_PROXIES · fetchLivePriceViaProxy()

8.2 — Auto-download dei file CSV

Il 6° tile in §01 ("Scarica da web") avvia l'auto-download dei fino a 5 file dal sito sorgente: BONDS, CASH_FLOWS, PREZZI, REND_EOD, REND_INTRADAY. Di questi, cashflows e prezzi sono distribuiti come ZIP (estratti lazy via JSZip), gli altri 3 come CSV nudi. L'operazione è in 3 fasi: scraping della pagina indice, fetch dei file, e validazione.

Step 1 — Scraping della pagina indice

_parseSiteLinks() — estrazione URL dai link
1. Fetch della pagina:
     https://www.simpletoolsforinvestors.eu/documentivari.php

2. Rimuovi i commenti HTML <!-- ... --> per evitare falsi positivi:
     i link "vecchi" del giorno prima a volte sono nei commenti.

3. Per ogni "file label" da scaricare, l'app ha un flag atteso (zip: true/false):
     bonds        → label "Obbligazioni quotate"                    · atteso CSV
     cashflows    → label "Cashflow di tutti i titoli"              · atteso ZIP
     prezzi       → label "Archivio con lo storico prezzi"          · atteso ZIP
     rendintraday → label "Rendimenti e durate calcolati intraday"  · atteso CSV
     rendeod     → label "Rendimenti e durate calcolati End of Day" · atteso CSV

4. Per ogni label:
     a. Trova la posizione del label nel testo (lowercase match)
     b. Cerca il primo URL `/data/export/{HASH}.csv[.zip]` con pos >= label_pos
     c. Il match deve essere unico (non già "usato" per un altro label)
        e l'estensione del file deve combaciare col flag atteso (zip o csv);
        in caso di mismatch viene emesso un warning in console
     d. Se la distanza testuale tra label e URL supera 8.000 caratteri,
        emette un warning (probabile mismatch della pagina)

5. Restituisce un oggetto { bonds: url, cashflows: url, prezzi: url, ... }

Il sito rigenera le URL ogni giorno con hash esadecimale nel path (formato: https://www.simpletoolsforinvestors.eu/data/export/{HASH}.csv oppure .csv.zip, dove {HASH} è una sequenza hex di lunghezza variabile — verosimilmente MD5 o simile, ma l'app tratta qualsiasi [A-Fa-f0-9]+). Per questo motivo non si può hardcodare l'URL: bisogna scrapare la pagina ogni volta.

Step 2 — Fetch e gestione ZIP

Per ogni file rilevato
Se il file è .zip:
  1. Carica JSZip 3.10.1 da cdnjs.cloudflare.com (lazy, solo al primo bisogno)
     L'inserimento è idempotente: una volta caricato, JSZip resta in
     `window.JSZip` per le chiamate successive.
  2. _fetchViaProxy(url, asArrayBuffer=true)
  3. await jszip.loadAsync(arrayBuffer)
  4. Estrai il primo file .csv contenuto → text content

Se il file è .csv:
  1. _fetchViaProxy(url, asArrayBuffer=false) → text content

In entrambi i casi, il testo del CSV viene incanalato in handleFile() come
se l'utente lo avesse trascinato manualmente.

Step 3 — Validazione e UI

Ogni file scaricato passa attraverso la stessa validateFile(text, schema) di un upload manuale (vedi cap. 2). Se la validazione fallisce per qualsiasi motivo (header mancanti, encoding sbagliato, righe corrotte), il file viene rifiutato e l'utente vede l'errore esatto sul tile in §01. Gli altri file continuano a essere caricati normalmente.

Durante l'auto-download il 6° tile mostra una barra di progresso incrementale (uno step per ogni file completato). Al termine, l'utente vede lo stato finale di ognuno dei 5 tile (✓ caricato, ✗ errore con descrizione).

src/js/12-fetch-site.js · fetchSiteDataExport() · _parseSiteLinks() · _extractCSVFromZip()

8.3 — Prezzi live da Borsa Italiana

Indipendentemente dall'auto-download dei file di anagrafica, l'app può recuperare il prezzo intraday di ultimo contratto di un singolo ISIN da Borsa Italiana. È un servizio "on-demand" attivato per singolo bond dalla §02 o §03 (icona pulsante "Borsa").

Costruzione dell'URL Borsa Italiana

buildBorsaItalianaURL(bond) — pattern URL
Determina path e mic (Market Identifier Code) in base ai campi del bond:

  marketcode      segmentcode         path                         mic
  ──────────      ───────────         ────                         ───
  EUROTLX         (qualsiasi)         eurotlx                      ETLX
  MOT             BTP                 mot/btp                      MOTX
  MOT             BOT                 mot/bot                      MOTX
  MOT             EXTRAMOT            extramot                     XMOT
  MOT             EUROMOT/DOMESTICMOT mot/euro-obbligazioni        MOTX
  MOT             (default)           mot/euro-obbligazioni        MOTX

URL finale:
  https://www.borsaitaliana.it/borsa/obbligazioni/{path}/dati-completi.html
    ?isin={ISIN}&mic={mic}&lang=it

Parsing del prezzo

parseBorsaItalianaPrice(html) — strategia regex flat
La pagina HTML contiene una tabella con righe del tipo:
  <tr><td>Prezzo Ultimo Contratto</td><td>95,35</td></tr>

L'app NON fa parsing DOM ma usa regex flat su tutto il testo HTML:

1. Cerca la label con regex case-insensitive: /Prezzo\s+ultimo\s+contratto/i
   Se non la trova → return null

2. Apre una "finestra" di 600 caratteri dopo la label
   Cerca il primo numero italiano valido (formato:
   d{1,4}(\.d{3})*,d{1,5} — opzionali separatori migliaia)

3. Converti formato italiano → number:
   "95,35"    → 95.35
   "1.234,56" → 1234.56

4. Validazione: 0 < price ≤ 5000 (sanity check per evitare di catturare
   anni, ID di pagina, ecc.)
   Se fuori range → return null

5. Cerca anche data/ora ultimo contratto con regex separata:
   /(?:Data\s*-?\s*Ora\s+ultimo\s+contratto|Data\s+e\s+ora)[\s\S]{0,200}?.../
   estratto se presente, altrimenti tradeTime = null

Restituisce {price: number, tradeTime: string|null} o null.

La strategia "regex flat" (anziché DOM parsing) è scelta per robustezza: cambi di classi CSS, attributi aggiunti, indentazione diversa non rompono il parser. Cambi sostanziali della struttura (rinominazione della label "Prezzo ultimo contratto") sì.

Aggiornamento del modello dati

setLivePrice(isin, price, tradeTime, source)
LIVE_PRICES.set(isin, {
  price,        // numero (es. 95.35)
  tradeTime,    // string oppure null, dal sito Borsa Italiana
  source,       // 'proxy' | 'paste' | 'manual'
                //   'proxy'  = fetch automatico via CORS proxy
                //   'paste'  = utente ha incollato HTML di Borsa e l'app l'ha parsato
                //   'manual' = utente ha digitato direttamente il prezzo
  fetchedAt     // timestamp del fetch (Date.now()), in millisecondi
});

Quando USE_LIVE_PRICES = true e LIVE_PRICES.has(isin):
  → tutti i calcoli §03/§06 usano questo prezzo invece di bond.price/askprice
  → getEffectivePagNom() in §03 ricomputa il "Pagamento nominale" virtuale
    come: newPagNom = −(livePrice / 100) × 10.000   (riferimento 10.000 nominale)

Stato e UI

L'utente può:

  • Attivare/disattivare globalmente l'uso dei prezzi live (checkbox in §03 "Usa prezzi live")
  • Pulire tutti i prezzi live in una volta
  • Vedere il pannello live-prices con ISIN, prezzo, timestamp dell'ultimo aggiornamento (formato umano "X min fa") e timestamp dell'ultimo contratto se disponibile
  • Aprire manualmente la pagina Borsa Italiana per verificare visualmente
▸ AGGIORNAMENTO

L'app non auto-aggiorna i prezzi live: l'utente deve cliccare per farne refresh. Il campo fetchedAt permette di sapere quanto tempo è passato dall'ultimo fetch (mostrato come "X min fa" o "Y ore fa" dalla funzione tsAgo); non c'è una classificazione esplicita "fresh / stale" — è responsabilità dell'utente decidere se aggiornare.

Inoltre, Borsa Italiana fornisce i prezzi con un ritardo regolamentare di 15 minuti (badge UI ⏱ delayed 15 min): il prezzo "live" è in realtà l'ultimo contratto fino a 15 minuti fa, non in tempo reale. Il campo tradeTime riporta l'orario dell'ultimo contratto effettivo e dà l'effettiva attualità del dato di mercato.

src/js/05-live-prices.js · buildBorsaItalianaURL() · parseBorsaItalianaPrice() · setLivePrice() · getEffectivePagNom()

8.4 — Edge case e limiti

Alcune situazioni note in cui il fetch live può fallire o produrre risultati ambigui:

SituazioneComportamento app
Mercato chiuso (weekend, festivi) Borsa restituisce ultimo prezzo del giorno prima. L'app non lo distingue da un prezzo intraday; mostra tradeTime se disponibile.
ISIN non quotato su MOT/EuroTLX La pagina Borsa restituisce 404 o pagina vuota; l'app non trova "Prezzo Ultimo Contratto" e fallisce con messaggio esplicito.
ZC senza scambi recenti (illiquidità) "Prezzo Ultimo Contratto" può essere assente; l'app ritorna errore di parsing.
Cambi di template Borsa Borsa cambia HTML occasionalmente. Se la label "Prezzo ultimo contratto" viene rinominata, l'app fallisce: il fix richiede aggiornamento del regex di parsing in parseBorsaItalianaPrice().
Proxy lenti o down Timeout 12s (prezzi live) o 45s (auto-download CSV), poi fallback al proxy successivo; se entrambi giù, errore finale all'utente.
Pagina con cookie banner intrusivo L'HTML contiene comunque la tabella; non c'è impatto a meno che il sito non cambi in modo significativo.

Stato del 6° tile (auto-download)

Casi simili valgono per l'auto-download:

  • Se il sito sorgente è giù → tutti i 5 download falliscono
  • Se un singolo file è in errore → solo quello fallisce, gli altri proseguono
  • Se cambia la struttura HTML del sito → _parseSiteLinks() può non trovare i link e ritorna oggetto vuoto
  • Se il file .zip è corrotto → JSZip fa throw, gestito con messaggio esplicito all'utente

src/js/12-fetch-site.js · fetchSiteDataExport() — error paths

CAPITOLO 9Glossario

Definizioni concise dei termini tecnici usati nel manuale e nell'app, in ordine alfabetico. I riferimenti ai capitoli sono per approfondire.

ACT/365
Convenzione di calcolo dei giorni in cui si usa il numero esatto di giorni solari trascorsi (ACT = "actual") e si divide per 365 fissi (indipendentemente dagli anni bisestili). È la convenzione standard usata dall'app per tutti gli IRR. Vedi cap. 4.1.
accruedTax
Tassa sul rateo lordo incassato vendendo una posizione (aliquota × rateo). È un'uscita di cassa reale al momento dello switch, ma NON è "attrito" nel senso del payback: rappresenta un'anticipazione di tassa altrimenti differita, e il lato simmetrico (Storno tassazione rateo nei cashflows del nuovo acquisto B) la compensa correttamente nel calcolo del switchCost. Mostrata come voce informativa separata in §06. Vedi cap. 7.5.1.
Aliquota
Percentuale di imposta sostitutiva applicata su un reddito finanziario. In Italia, sulle obbligazioni si applica 12,5% ai titoli di Stato italiani, ai sovranazionali equiparati ex art. 31 DPR 601/73 (BEI, BIRS, ecc.), e ai titoli di Stato esteri di paesi "white-list" (DM 4/9/1996 e successivi aggiornamenti); 26% a tutto il resto (corporate, bancari, emittenti di paesi non in white-list). Vedi cap. 1.4 e 3.4.
Asta (acquisto in)
Acquisto di un titolo di Stato italiano direttamente al collocamento primario MEF. Per BTP/CTZ/CCT le commissioni sono per legge nulle; per BOT le commissioni esistono ma seguono un tariffario per vita residua diverso da quello del secondario. Vedi cap. 3.3.
Asta (detection)
L'app riconosce automaticamente un acquisto in asta confrontando la data di regolamento (T+1 dalla data di emissione) con la data del flusso "Pagamento nominale" nei cashflows oppure con la data manuale (override utente). Accetta anche date anteriori al primoRegolamento (gap storico). Si applica solo ai titoli di Stato italiani (BOT/BTP/CTZ/CCT/CCTEU). Vedi cap. 3.3.
Attrito di switch
Costo "irrecuperabile" di un'operazione di switch tra strategie: tassa netta su capital gain (dopo cross-rate) + commissione di vendita + commissione di acquisto. NON include il rateo ri-pagato comprando B (recuperato al prossimo coupon di B) né la tassa sul rateo incassato vendendo A (è anticipazione di tassa altrimenti differita, non costo extra). Vedi cap. 7.5 e 7.5.1.
Backfill cedole storiche
Quando l'utente imposta una purchaseDate precedente alla prima data del file CASH_FLOWS, l'app ricostruisce sinteticamente le cedole incassate nel gap usando i metadati del titolo (couponRate, couponPeriodicity). Solo per bond TF (per ZC/BOT non ci sono cedole intermedie). L'aliquota usata per la tassazione delle cedole ricostruite è quella inferita dai cashflows (cfr. § 3.4) o 12,5% come fallback. Vedi cap. 4.2.
BOT (Buono Ordinario del Tesoro)
Titolo di Stato italiano zero-coupon a breve termine (3, 6, 12 mesi). Tassazione 12,5% sul disaggio. Le commissioni in asta seguono 4 fasce per vita residua: ≤80 gg, 81-140 gg, 141-270 gg, ≥271 gg. Vedi cap. 3.3.
BTP (Buono del Tesoro Poliennale)
Titolo di Stato italiano a tasso fisso con cedola semestrale. Tassazione 12,5% su cedole e disaggio. Commissioni in asta nulle.
CCT / CCTeu (Certificato di Credito del Tesoro)
Titolo di Stato italiano a tasso variabile. Il CCTeu (emissioni dal 2010 in poi) ha cedola legata all'Euribor 6m + spread; il CCT "tradizionale" (emissioni più datate, ancora in circolazione fino al ~2025) aveva cedola legata al rendimento del BOT a 6 mesi + spread. Commissioni in asta nulle. Tassazione 12,5%.
CTZ (Certificato del Tesoro Zero-Coupon)
Titolo di Stato italiano zero-coupon a 24 mesi. Tassazione 12,5% sul disaggio. Commissioni in asta nulle.
Capital gain (capGain)
Plusvalenza o minusvalenza tra prezzo di vendita e prezzo di carico fiscale, calcolata secondo D.Lgs. 461/97. Non coincide col PMC nudo: si scorpora il disaggio di emissione, tassato separatamente. Vedi cap. 7.2.
Cashflow netto mensile
Somma algebrica di tutti i flussi (cedole, rimborsi, tasse) di un portafoglio aggregati per mese, esclusi i flussi di acquisto. Usato dal Generatore §05 per l'obiettivo "income" e dalla §03 nel diagramma cashflow. Vedi cap. 6.3.
Compensazione cross-rate
Meccanismo che permette di compensare minusvalenze e plusvalenze tassate ad aliquote diverse (12,5% titoli di Stato vs 26% corporate), applicando un fattore correttivo proporzionale alle aliquote stesse. Funziona in due direzioni con fattori inversi:
  • Direzione A: minus 12,5% → plus 26% con fattore 12,5/26 ≈ 0,4808. 1.000 € di minus 12,5% offset 480,77 € di plus 26% (residua 519,23 € di plus 26% tassabile). Pacificamente applicata in regime amministrato.
  • Direzione B: minus 26% → plus 12,5% con fattore 26/12,5 = 2,08. 1.000 € di minus 26% può offset fino a 2.080 € di plus 12,5%; se la plus 12,5% disponibile è solo 1.000 €, vengono "consumati" 480,77 € di minus 26% (residua 519,23 €). Meno frequente in regime amministrato.
Vedi cap. 7.3 per i dettagli normativi e i caveat di prudenza interpretativa.
CORS proxy
Servizio di terze parti che rilegge una URL come server-to-server e aggiunge l'header CORS necessario per consentire al browser di leggere la risposta. L'app usa codetabs e x2u in fallback. Vedi cap. 8.1.
Coverage (income)
Capacità di un portafoglio di coprire una soglia minima di cashflow netto mensile. Quattro modalità: strict (no mese sotto soglia), soft (max 1 mese), avg (media nel periodo basta), cumulative (saldo "banca" mai negativo). Vedi cap. 6.3.
Disaggio di emissione
Differenza positiva tra prezzo di rimborso e prezzo di emissione: es. titolo emesso a 95 e rimborsato a 100, disaggio = 5. Per i titoli di Stato si tassa al 12,5% sulla parte maturata. Per i BOT viene anticipata all'acquisto. Vedi cap. 3.2.
D.Lgs. 461/97
Decreto legislativo che disciplina i redditi diversi di natura finanziaria in Italia, comprese le regole di compensazione minusvalenze e le formule per il calcolo del capital gain su redemption e su vendita anticipata. È la base normativa dei calcoli fiscali dell'app (insieme alle modifiche introdotte da D.L. 138/2011 conv. L. 148/2011 e D.L. 66/2014 per le aliquote vigenti).
Duration (netta, lorda, supernet)
Sensibilità del prezzo del bond a variazioni dei tassi (in anni). L'app usa netduration di default, fallback su supernetduration o grossduration. Pre-calcolata dal sito sorgente. Vedi cap. 2.3.
Fitness ladder
Punteggio in [0, 1] che misura quanto un portafoglio assomiglia a un ladder ideale. Combina rungAlignment (peso 0,5), rungCoverage (0,3), weightUniformity (0,2). Soglia di accettazione: 0,4. Vedi cap. 5.4.
Friction
Sinonimo di "attrito di switch". Vedi cap. 7.5.
IRR (Internal Rate of Return)
Tasso di sconto che rende NPV = 0. L'app lo risolve con Newton-Raphson da 7 starting point + fallback bisezione su [-0,95; +5]. Convenzione ACT/365. Vedi cap. 4.1.
IRR storico (§03)
IRR calcolato dalla data di acquisto reale (PMC) fino alla scadenza. Risponde alla domanda "cosa sto guadagnando dall'acquisto?". Vedi cap. 4.2.
IRR-B
IRR della strategia B come investimento fresco oggi. Usato in §06 insieme a YTM-A per il confronto omogeneo dello switch. Vedi cap. 7.6.
Ladder (scala di scadenze)
Strategia in cui le scadenze dei titoli sono distribuite a intervalli regolari (es. una scadenza ogni 2 anni). Riduce il rischio di reinvestimento concentrato in una singola data. Vedi cap. 5.4 e 6.
Minimum lot (minLot)
Taglio minimo di un titolo (es. 1.000 €, 10.000 €, 100.000 €). Il "lotto" dell'app è 1 × minLot. Per i BTP retail il taglio è tipicamente 1.000 €; per i corporate il taglio varia: 1.000 € o 10.000 € per i titoli quotati su MOT/EuroTLX accessibili al retail, fino a 100.000 € per le emissioni istituzionali. Letto da BONDS.csv (campo minimumlot).
MIRR (Modified IRR)
Variante dell'IRR che assume un tasso di reinvestimento esplicito (es. risk-free rate). NON implementata nell'app: si usa l'IRR standard con ipotesi di reinvestimento implicita al medesimo tasso. Vedi cap. 4.5.
Modalità ottimizzatore (dominate / sameYield / sameRisk / ladder)
Le 4 modalità dell'ottimizzatore §04 che filtrano i candidati generati dal Monte Carlo:
  • dominate (default): dominanza Pareto stretta sulla baseline
  • sameYield: IRR vicino al baseline (entro 5 bps) + rischio strettamente inferiore
  • sameRisk: rischio vicino al baseline + IRR strettamente superiore
  • ladder: portafoglio scala, con filtri di IRR loss e rischio relativi
Vanno distinte dagli obiettivi del generatore §05 (maxIRR, minRisk, balanced, ladder, income), che usano nomi diversi e logica diversa. Vedi cap. 5.3 e 6.1.
Monte Carlo (ricerca)
Approccio random sampling usato sia dall'ottimizzatore §04 (default 4.000 iterazioni) che dal generatore §05 (default 3.000 iterazioni): si campionano casualmente sottoinsiemi del pool e si valutano le metriche risultanti. Non garantisce l'ottimo globale, ma trova buone soluzioni in tempi accettabili. Vedi cap. 5.2 e 6.2.
Newton-Raphson
Metodo numerico iterativo per trovare radici di una funzione: r_new = r − f(r)/f'(r). Convergenza tipica in pochi step per IRR. Vedi cap. 4.1.
NPV (Net Present Value)
Valore attuale di una serie di flussi futuri, scontati a un tasso r: NPV = Σ CFᵢ / (1+r)^tᵢ. Per definizione, l'IRR è il tasso che rende NPV = 0. Vedi cap. 4.1.
Pareto (dominanza)
Concetto matematico: un candidato domina la baseline in senso di Pareto se è "non peggiore" su tutte le dimensioni (IRR ≥ base, rischio ≤ base) ed è strettamente migliore su almeno una. Implementata nella modalità dominate dell'ottimizzatore §04; con riskMetric=both, le dimensioni sono tre (IRR, duration, zspread). Vedi cap. 5.3.
Payback (anni)
Anni necessari affinché il vantaggio annuale di rendimento dello switch (Δyield × purchaseCost) ripaghi l'attrito iniziale. paybackYears = switchFriction / annualAdvantage. Vedi cap. 7.7.
PMC (Prezzo Medio di Carico)
Prezzo clean per 100 nominale a cui si è effettivamente comprata una posizione (NON include rateo né commissioni di acquisto). Di default è il prezzo derivato dai cashflows del file (−Pagamento nominale × 100 / 10.000); l'utente può sovrascriverlo in §06 (pannello "Prezzi di carico") inserendo tranches con prezzo e quantità, da cui computeWeightedPMC() calcola un PMC medio ponderato. Vedi cap. 4.2 e 7.2.
Pool candidato
Sottoinsieme dell'universo che rispetta tutti i vincoli posti dall'utente (valuta, emittente, rating, mercato, duration, ecc.). Solo i titoli del pool possono entrare nelle alternative. Vedi cap. 5.1.
Prefill (banner)
Banner sopra il pannello vincoli di §04 che, su click, preimposta i multi-select coi valori distinti del portafoglio attuale. Utile per allentare un vincolo partendo dalla composizione corrente.
Prezzo clean / dirty
Clean: prezzo "a corso secco" del bond, senza rateo. Dirty: prezzo "tel quel", clean + rateo lordo. L'app usa clean per le quotazioni e dirty implicito quando aggrega controvalori. Vedi cap. 3.1.
Rateo lordo
Quota di cedola maturata tra l'ultimo stacco e la data di settlement, prima della tassazione. Il compratore lo paga al venditore aggiunto al prezzo clean. Vedi cap. 3.1.
REND_EOD / REND_INTRADAY
File CSV con rendimenti e duration pre-calcolati dal sito sorgente. EOD = end of day (close); INTRADAY = aggiornamento periodico in giornata (cadenza dipendente dal sito). REND_EOD è obbligatorio; REND_INTRADAY, se presente, ha priorità su EOD per tutti i campi che condivide (askprice, netytm, netduration, zspread, ispread, cedola corrente, ratings). Solo volume/volumevalue restano da EOD. Vedi cap. 2.3.
Reinvestimento (ipotesi)
L'IRR assume implicitamente che ogni flusso intermedio (cedola, rimborso parziale) venga reinvestito al medesimo tasso r fino alla scadenza più lunga. La realisticità dell'assunzione dipende dallo scenario di tassi futuri: in ambiente stabile è una buona approssimazione del rendimento effettivo; in scenari di tassi che si muovono molto, il rendimento realizzato può essere superiore (tassi in salita) o inferiore (tassi in calo) all'IRR. Vedi cap. 4.5.
Score (portfolio)
Punteggio interno calcolato dall'ottimizzatore §04 (vedi §5.3) o dal generatore §05 (vedi §6.4) per ordinare i candidati. Combina IRR, rischio, copertura income (solo generatore), fitness ladder, e una penalty di budget. Non è un'unità misurabile: non è un tasso né un'unità economica, serve solo per il sort interno. Le metriche assolute mostrate all'utente (IRR, Z-spread, duration, copertura) sono quelle interpretabili. Vedi cap. 5.3 e 6.4.
Storno tassazione rateo
Voce del CASH_FLOWS che rappresenta il credito d'imposta sul rateo lordo (12,5% × rateo, accreditato al buyer all'acquisto). In vendita anticipata, si inverte di segno: diventa la tassa che il venditore paga sul rateo incassato. Vedi cap. 7.1.
Switch
Operazione di vendita totale di una strategia (A) e acquisto contestuale di un'altra (B). L'app calcola attrito, payback, Δyield. Vedi cap. 7.
switchCost
Cassa netta che l'utente deve mettere/avere indietro per fare lo switch: purchaseCost(B) − proceedsNet(A). Diverso dall'attrito.
switchFriction
Attrito di switch. Vedi voce "Attrito di switch" e cap. 7.5.
Tolleranza (income)
Modalità di validazione della copertura mensile nel Generatore: strict / soft / avg / cumulative. Vedi cap. 6.3.
tradeDate
Variabile interna dell'app che identifica la data originaria di acquisto di una posizione, letta dal CASH_FLOWS (prima riga "Pagamento nominale"). In §06 NON è la data dello switch ma la data storica del carico; il capital gain in 7.2 si calcola sull'intervallo tradeDate → oggi. Può essere sovrascritta implicitamente dall'override delle tranches in §06. Vedi cap. 7.2.
Universo
Insieme totale di tutti i titoli letti dal file BONDS.csv (ordine di grandezza tipico: alcune migliaia di ISIN, varia nel tempo a seconda di emissioni e scadenze). Da qui si filtra il pool con i vincoli posti dai pannelli §04/§05. Vedi cap. 2.1 e 5.1.
Vita residua (VR)
Anni mancanti alla scadenza del titolo da oggi. Per i BOT in asta, è la base per le 4 fasce di commissioni (≤80 / 81-140 / 141-270 / ≥271 gg). Vedi cap. 3.3.
YTM (Yield to Maturity)
Rendimento a scadenza di un singolo bond: il tasso che eguaglia il prezzo di mercato al valore attuale dei flussi futuri. L'app legge la versione grossytm e netytm dal file REND_EOD/INTRADAY. Per portafogli, l'app non calcola una media degli YTM dei singoli titoli (sarebbe scorretto), ma un IRR aggregato sui flussi combinati: il risultato dipende dai pesi relativi delle posizioni e dal timing dei flussi, e in generale differisce dalla media aritmetica o ponderata dei singoli YTM. Vedi cap. 2.3 e 4.
YTM-A (residuo da oggi)
IRR della strategia A simulando una vendita virtuale a prezzo di mercato + riacquisto immediato. Confrontabile con IRR-B come "costo di opportunità" di vendere. Vedi cap. 4.3 e 7.6.
ZC (Zero Coupon)
Bond senza cedole: il rendimento si manifesta tutto come differenza tra prezzo di rimborso e prezzo di emissione (disaggio). Esempi: BOT, CTZ, alcuni sovranazionali ZC. Vedi cap. 3.2.
Z-spread
Spread parallelo costante (espresso come decimale nei file, es. 0,0125 = 125 bps) sopra la curva spot zero-coupon di riferimento (tipicamente swap) che, applicato a tutti i flussi del bond, riproduce il prezzo di mercato. Misura combinata di rischio di credito, illiquidità, e premio convexity rispetto al benchmark. Pre-calcolato dal sito sorgente. Vedi cap. 2.3.

CAPITOLO 10Limiti dichiarati e disclaimer

Questo capitolo elenca esplicitamente i limiti dell'app: cosa NON fa, dove i calcoli possono divergere dalla realtà, e quali assunzioni sono incorporate. L'utente che usa l'app per decisioni reali deve conoscere questi limiti.

10.1 — Non sostituisce consulenza professionale

Bond Strategy Lab è uno strumento di analisi, non un consulente finanziario, fiscale o legale. Non emette raccomandazioni d'investimento. Tutti i calcoli sono basati su:

  • Dati pubblici di terze parti (sito sorgente)
  • Formule documentate nei capitoli precedenti
  • Convenzioni standard di mercato

Decisioni d'investimento e fiscali concrete dovrebbero essere validate da un consulente abilitato che conosca la posizione individuale dell'utente (regime fiscale, residenza, altri investimenti, obiettivi di vita).

10.2 — Limiti dei calcoli di rendimento

IRR e ipotesi di reinvestimento

L'IRR (vedi cap. 4) assume che ogni cedola venga reinvestita al medesimo tasso fino alla scadenza più lunga. Quanto sia realistica l'assunzione dipende dallo scenario di tassi futuri: in ambiente di tassi stabili l'IRR è una buona approssimazione del rendimento effettivo; in scenari di tassi in calo o in salita marcata, il rendimento realizzato può essere inferiore (calo) o superiore (salita) all'IRR. L'app riporta l'IRR standard senza correzioni — è la convenzione universale per il confronto inter-titolo, ma l'utente consapevole non lo legge come "rendimento garantito".

Tassi futuri sconosciuti

Per CCT/CCTeu (tasso variabile) e per la valutazione dei flussi nominali a tasso flottante, l'app usa il tasso "corrente" come proxy per i flussi futuri. È un'approssimazione: i tassi reali futuri saranno diversi.

Volatilità del prezzo

L'app valuta il portafoglio al prezzo di mercato corrente. Il valore reale tra un mese, sei mesi o un anno sarà diverso. La duration riportata aiuta a stimare la sensibilità ai tassi, ma è una misura locale: per shock grandi (oltre 100-200 bps) la duration sottostima la perdita reale (convessità ignorata).

Liquidità non considerata

L'app non distingue tra titoli liquidi (BTP benchmark, BOT) e illiquidi (corporate piccoli, ETC poco scambiati). I prezzi quotati sui titoli illiquidi possono essere "indicativi": vendere realmente a quel prezzo può non essere possibile, o solo con uno spread bid-ask significativo che l'app non modella.

10.3 — Limiti dei calcoli fiscali

Aliquote inferite, non dichiarate

L'app deduce l'aliquota fiscale (12,5% o 26%) dal rapporto tra tassazione cedola e cedola lorda nei cashflows (cap. 3.4). Su casi standard funziona; su casi atipici (es. prima cedola irregolare, obbligazioni soggette a normative specifiche, fondi con regimi misti) l'inferenza può sbagliare. L'utente può sovrascrivere manualmente l'aliquota in §06.

Capital gain — formula semplificata

La formula di capital gain in cap. 7.2 è il "Sistema A" (capitalizzazione composta) del D.Lgs. 461/97. Casi particolari NON coperti:

  • Titoli con tassazione speciale (es. immobiliari, ETC su materie prime, derivati)
  • Cedole step-up / step-down: tassate correttamente quando presenti nel file CASH_FLOWS (l'app legge l'importo effettivo della singola cedola). Nel backfill di cedole storiche (cap. 4.2), l'app usa il couponRate corrente come approssimazione, che può differire dalla cedola storica step se il titolo era in una fase diversa.
  • Titoli con clausola di richiamo anticipato (callable / puttable): l'IRR considera la scadenza nominale, non eventuali date di call/put anticipate. Per titoli "in the money" del call (es. bond ad alta cedola in ambiente di tassi calati), il rischio di rimborso anticipato è significativo e l'IRR mostrato è una sovrastima. Yield-to-worst e yield-to-call non sono calcolati.
  • Conversioni valutarie: per bond in valuta estera (USD, GBP, CHF, ecc.) l'app riporta tutti i valori nella valuta nominale del titolo senza convertirli in euro. Non applica tassi di cambio storici per plus/minus da cambio. L'utente che ha bond in valuta deve fare la conversione separatamente per ottenere il rendimento in euro.

Minusvalenze residue non tracciate

Le minusvalenze non utilizzate nella compensazione cross-rate (cap. 7.3) restano disponibili per 4 esercizi fiscali successivi secondo il D.Lgs. 461/97 art. 6 c.5. L'app NON ha memoria di minusvalenze pregresse dell'utente, né le accumula nel tempo. L'utente con uno "stock" di minus residue da operazioni passate dovrebbe gestirle in dichiarazione, fuori dall'app.

Bollo, imposta di soggiorno, altri tributi

L'app NON calcola:

  • Imposta di bollo 0,2% annua sui prodotti finanziari (calcolata e addebitata dall'intermediario)
  • Eventuali ritenute supplementari per residenza fiscale fuori UE
  • Tassazione plusvalenze in regime amministrato vs dichiarativo: l'app calcola la plusvalenza nominale come da formula §7.2. In regime amministrato è l'intermediario ad applicare la ritenuta a titolo d'imposta sulla plusvalenza realizzata (al netto delle minus pregresse del dossier amministrato). In regime dichiarativo è l'utente a calcolare e versare l'imposta in dichiarazione (modello Redditi PF, quadro RT). I numeri dell'app possono guidare entrambi i regimi: in amministrato sono per così dire "informativi", in dichiarativo "operativi".

Convenzioni internazionali e doppia imposizione

Per bond di emittenti esteri possono esistere trattati contro la doppia imposizione che riducono la ritenuta operata dal paese emittente. L'app NON modella la doppia imposizione: usa direttamente il netto delle cedole come riportato nel file CASH_FLOWS (calcolato dal sito sorgente in base alle convenzioni applicabili al paese dell'emittente). Non gestisce crediti d'imposta esteri.

L'utente con bond esteri in regime dichiarativo deve verificare separatamente che la ritenuta estera applicata sia consistente con il trattato bilaterale del paese emittente, e — se serve — chiedere il rimborso o il credito d'imposta in dichiarazione.

10.4 — Limiti dei dati di input

Affidabilità del sito sorgente

I file CSV provengono da simpletoolsforinvestors.eu, un sito di terze parti. L'app NON valida la correttezza dei dati: si fida del sito. Errori del sito (es. cedola sbagliata, data di scadenza errata) si propagano direttamente nei calcoli dell'app. L'utente può fare spot-check confrontando con Borsa Italiana o altre fonti.

Frequenza di aggiornamento

I file vengono rigenerati dal sito una volta al giorno (sera EOD, intraday durante apertura mercato). Per BOT in fase di emissione nuova, l'anagrafica può comparire con qualche giorno di ritardo rispetto all'asta.

Universo limitato

Il sito copre principalmente MOT (BTP, BOT, CTZ, CCT, EXTRAMOT, DOMESTICMOT, EUROMOT) e EuroTLX. Bond su altri mercati (es. quotati solo in OTC, mercati esteri non collegati) NON sono presenti. Anche su MOT, alcuni titoli molto piccoli possono mancare.

File mancanti o parziali

Caricare meno di tutti i file riduce la funzionalità:

  • BONDS, CASH_FLOWS, REND_EOD: minimi indispensabili per §02/§03. Senza uno di questi l'app non può costruire l'universo né calcolare i flussi.
  • REND_INTRADAY (opzionale): senza, l'app usa prezzi e YTM di chiusura precedente (EOD) per tutti i titoli.
  • PREZZI (opzionale): senza, è disabilitato il pulsante "Cerca prezzo storico" in §06 (che cerca il prezzo associato a una data inserita nelle tranches). L'utente può comunque inserire manualmente il prezzo nella tranche.

10.5 — Limiti tecnici dell'app

Stato volatile

L'app è 100% client-side: lo stato vive nella memoria del browser. Chiudere la pagina cancella tutto (file, selezione, vincoli). L'utente deve esportare manualmente lo stato JSON per persistenza tra sessioni.

Performance su portafogli molto grandi

L'app è stata testata con portafogli fino a ~30 ISIN e universo fino a ~4.000 bond. Ottimizzatore con 4.000 iterazioni × 30 ISIN impiega ~5-10 secondi su un laptop moderno. Sopra queste soglie l'esperienza utente può degradarsi.

Dipendenza da proxy CORS

Auto-download e prezzi live richiedono il funzionamento dei proxy CORS configurati (codetabs, x2u). Se entrambi sono giù, l'app funziona ma senza fetch remoto: l'utente deve caricare i file manualmente. Vedi cap. 8.1.

Browser compatibility

Testato su Chrome, Firefox, Safari, Edge recenti (2024+). Browser molto vecchi o configurazioni hardened (extensions aggressive, modalità privata stretta) possono interferire col fetch o con il file drop.

10.6 — Privacy e dati personali

L'app non invia ad Anthropic, all'autore, né ad alcun server di proprietà del progetto, alcun dato dell'utente. I CSV caricati restano nella memoria del browser e nei calcoli locali.

Le uniche richieste di rete fatte dall'app sono:

  • Caricamento font da Google Fonts (fonts.googleapis.com, fonts.gstatic.com)
  • Caricamento JSZip da CDN (cdnjs.cloudflare.com), solo se si usa l'auto-download di file ZIP
  • Auto-download CSV via CORS proxy verso simpletoolsforinvestors.eu (solo se l'utente clicca il tile di auto-download)
  • Prezzi live via CORS proxy verso borsaitaliana.it (solo su click esplicito per ogni ISIN)

I server proxy vedono URL e content type delle richieste passate attraverso loro (per natura del servizio), ma non vedono dati del portafoglio dell'utente.

⚠ TRASPARENZA — API KEY DI TERZI

Uno dei proxy CORS (go.x2u.in) richiede una API key personale

I parametri della chiave possono essere configurati nel file API_KEYS.txt prima del build, e sono usati solo se il proxy è attivo. In alternativa l'utente può configurare un proprio proxy direttamente modificando il file _05-live-prices.js prima del build, o il file 05-live-prices.js dopo il build, o rimuovendo del tutto il fallback x2u.

Se si utilizza la build in bundle (un solo file contenente l'intera app) le chiavi e i proxy possono essere impostati nella variabile CORS_PROXY della sezione: LIVE PRICES — Borsa Italiana fetch via CORS proxy, dialog dello script js.

Il file dello stato JSON esportato contiene tutti i dati dell'utente (file CSV originali in formato compresso, selezione, override, snapshot di lavoro). L'utente è responsabile della sicurezza di questo file (es. non condividerlo via canali non sicuri se contiene dati sensibili).

10.7 — Disclaimer finale

⚠ AVVERTENZA

L'autore di Bond Strategy Lab fornisce questo strumento "as-is", senza garanzia di accuratezza, completezza o idoneità a un uso specifico. L'utente è interamente responsabile delle proprie decisioni d'investimento e dell'interpretazione corretta dei calcoli mostrati. Bug e imprecisioni possono esistere; l'app non sostituisce il prospetto informativo del singolo titolo, la documentazione fornita dal proprio intermediario, né il consiglio di un consulente abilitato. Investire in obbligazioni comporta rischi (default emittente, oscillazione dei tassi, illiquidità, cambio per i bond in valuta estera) che l'app non elimina né garantisce di rendere visibili in toto. Usa con consapevolezza.

Fine del manuale. Per segnalazioni, contattare l'autore.