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:
- Fetch della pagina
documentivari.phptramite CORS proxy - Estrazione dei 5 URL via regex stabile
- Download dei 5 file via proxy
- Decompressione dei
.zipvia libreria JSZip caricata da CDN - Iniezione nei loader come se fossero stati caricati a mano
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
| Categoria | Aliquota | Note |
|---|---|---|
| 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/2014 | 12,5% | Lista DM 23/03/2017 e aggiornamenti |
| Corporate, bancarie, supranational non white-list | 26% | 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.
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).
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.
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
| Colonna | Tipo | Note |
|---|---|---|
isincode | stringa | Chiave primaria (es. IT0005440620) |
description | stringa | Es. "BTP 01/11/2026 7,25%" |
currencycode | stringa | EUR, USD, GBP, ... |
minimumlot | numero | Taglio minimo (es. 1000, 10000) |
marketcode | stringa | MOT, EUROTLX |
segmentcode | stringa | BTP, BOT, DOMESTICMOT, EXTRAMOT, EUROTLX, ... |
status | stringa | QUOTATO, SOSPESO, ... |
firstdate | data | Data di prima quotazione |
issuedate | data | Data di emissione |
issueprice | numero | Prezzo di emissione (su 100) |
redemptiondate | data | Data di scadenza/rimborso |
redemptionprice | numero | Prezzo 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
| Colonna | Tipo | Note |
|---|---|---|
isincode | stringa | Identifica il titolo |
description | stringa | Descrizione (ridondante con BONDS) |
eventtype | stringa | Sale, Income, Taxes, ... |
eventdescription | stringa | Tipo specifico dell'evento |
eventdate | data | Data del flusso |
eventamount | numero | Importo del flusso (positivo o negativo) |
Categorie di eventi
L'app riconosce e tratta 11 tipi diversi di eventdescription:
| Eventdescription | Segno | Frequenza | Descrizione |
|---|---|---|---|
| Pagamento nominale | − | 1 (t=0) | Esborso clean (prezzo × nominale / 100) |
| Rateo lordo | − | 0 o 1 (t=0) | Solo se compra mid-cedola |
| Storno tassazione rateo | + | 0 o 1 (t=0) | Credito d'imposta sul rateo |
| Imposta sul disaggio | − | 0 o 1 (t=0) | Solo BOT/ZC: tassazione anticipata |
| Credito disaggio | + | 0 o 1 (t=0) | Quota disaggio già maturata |
| Commissioni di acquisto | − | 0 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 cedola | − | N (t>0) | Tassa applicata sulla cedola (12,5% o 26%) |
| Rimborso nominale | + | 1 (t=scadenza) | Rimborso a 100 (o redemptionprice) |
| Tassazione capital gain | − | 0 o 1 (t=scadenza) | Solo se prezzo carico ≠ 100 |
| Addebito disaggio di emissione | − | 0 o 1 (t=scadenza) | Tassa su disaggio non ancora pagata |
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.
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
| Colonna | Note |
|---|---|
isincode | Identifica il titolo |
price (EOD) / askprice (INTRADAY) | Prezzo clean su 100 |
volume | Volume nominale scambiato |
volumevalue | Controvalore scambiato |
grossytm | YTM lordo (decimale, es. 0.0325 = 3,25%) |
netytm | YTM netto applicando aliquota inferita |
grossduration | Duration lorda (Macaulay) in anni |
netduration / supernetduration | Variants 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%). |
ispread | I-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. |
currentcouponrate | Cedola corrente (% nominale annuo) |
couponperiodicity | Mesi tra cedole (6 = semestrale, 12 = annuale) |
ratingsp, ratingmoodys, ratingfitch | Rating 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
| Colonna | Tipo | Note |
|---|---|---|
isincode | stringa | Identifica il titolo (obbligatoria) |
endvaluedate | data | Data di regolamento (giorno lavorativo successivo alla negoziazione) — obbligatoria |
pricevalue | numero | Prezzo clean su 100 (decimale con virgola) — obbligatoria |
pricetype | stringa | Opzionale: LP = Last Price (da contratto effettivo) o RP = Reference Price (di riferimento teorico) |
marketcode | stringa | Opzionale: MOT, EUROTLX |
referencedate | data | Opzionale: data di negoziazione |
volume, mintoday, maxtoday | numero | Opzionali: 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.
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.
| Variabile | Valore |
|---|---|
| lastCouponDate | 1 marzo |
| nextCouponDate | 1 settembre |
| regularPeriodDays | 184 giorni (1 marzo → 1 settembre, anno non bisestile) |
| settlement − lastCouponDate | 106 giorni |
| nextCouponAmount | 2,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
- 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
regularPeriodDaysdella successiva cedola regolare, non della prima cedola. - 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.
- 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.
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)
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 giornibot-asta-81-140— vita residua 81–140 giornibot-asta-141-270— vita residua 141–270 giornibot-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, viaapplyBotAuctionCommissionCap():- 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)
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.
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
| Titolo | Cedola lorda | Tassazione | ratio | Aliquota |
|---|---|---|---|---|
| BTP 01/11/2027 6,5% | 325,00 | −40,62 | 0,125 | 12,5% ✓ |
| Austria 2,75% 2026 | 137,50 | −17,19 | 0,125 | 12,5% ✓ (white-list) |
| UniCredit subord. 5,375% | 268,75 | −69,88 | 0,260 | 26% ✓ (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'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.
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.
L'IRR converge solo se sono soddisfatte tre condizioni:
- L'array dei flussi contiene almeno 2 elementi.
- Esiste almeno un flusso negativo (l'investimento).
- 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
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)
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:
| Scenario | Condizione | Effetto |
|---|---|---|
| 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
- 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
couponRatecorrente 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
couponMonthsocouponPeriodicitynon 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
purchaseDateprecedente abond.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.
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.
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.
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
| Situazione | IRR 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 |
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".
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 UI | Filtro 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 min | b.vr ≥ minVR (anni) |
| Duration netta min/max | minDur ≤ b.netDur ≤ maxDur |
| Scadenza massima | b.redemptiondate ≤ maxDate |
Più due requisiti tecnici sempre attivi:
b.hasCF= true (i flussi di cassa sono presenti nel file)b.netytmfinito (YTM netto calcolato)- Se metrica di rischio = zspread:
b.zspreadfinito - Se metrica di rischio = duration:
b.durationfinita - 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.
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 UI | Logica |
|---|---|---|
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. |
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
| Caratteristica | Ottimizzatore §04 | Generatore §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 UI | ID | Default | Range / 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 è specificatointervalYears, si costruisce una griglia target dikgradini 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 inkbucket 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 dirunGenerator.
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.)
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.
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.
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.
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.
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:
- Override utente: la select per-ISIN nel pannello
Prezzi di carico §06, valori ammessi
12.5o26. - 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. - 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€
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)
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.
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 è inclusaaliquota × 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.
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
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).
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.
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
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:
| Situazione | Comportamento 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
purchaseDateprecedente 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.
- Direzione A: minus 12,5% → plus 26% con fattore
- 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
netdurationdi default, fallback susupernetdurationogrossduration. 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 baselinesameYield: IRR vicino al baseline (entro 5 bps) + rischio strettamente inferioresameRisk: rischio vicino al baseline + IRR strettamente superioreladder: portafoglio scala, con filtri di IRR loss e rischio relativi
- 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à
dominatedell'ottimizzatore §04; conriskMetric=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 cuicomputeWeightedPMC()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). Solovolume/volumevaluerestano da EOD. Vedi cap. 2.3. - Reinvestimento (ipotesi)
- L'IRR assume implicitamente che ogni flusso intermedio
(cedola, rimborso parziale) venga reinvestito al medesimo tasso
rfino 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
grossytmenetytmdal 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
couponRatecorrente 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.
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
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.