← Files italian-investorARCHIVED FILE
skills/italian-investor/scripts/tax_engine.py
16.3 KB · Oct 2, 2026 · 00:32 UTC
#!/usr/bin/env python3
"""Motore fiscale deterministico per strumenti finanziari, residente in Italia.
Non contiene giudizi: classifica lo strumento, applica l'aritmetica e dichiara
esplicitamente cosa resta da verificare. Nessuna dipendenza esterna.
python scripts/tax_engine.py classifica --tipo etf
python scripts/tax_engine.py vendita --tipo etf --pmc 90 --prezzo 120 \
--quantita 100 --minus 2000
"""
import argparse
import json
import sys
# Aliquota e frazione imponibile agevolata: valori di riferimento, NON fonte.
# Vanno riverificati (references/regole-correnti.md) prima dell'uso.
ALIQUOTA_ORDINARIA = 0.26
FRAZIONE_IMPONIBILE_AGEVOLATA = 0.4808 # 26% * 0.4808 = 12,5% effettivo
CAPITALE = "reddito_di_capitale"
DIVERSO = "reddito_diverso"
# categoria_plus: natura del provento positivo
# categoria_minus: natura della differenza negativa
# agevolato: la componente beneficia dell'imposizione ridotta
CATEGORIE = {
"etf": dict(categoria_plus=CAPITALE, categoria_minus=DIVERSO, agevolato=False,
nota="OICR armonizzato: plus reddito di capitale, minus reddito diverso"),
"oicr": dict(categoria_plus=CAPITALE, categoria_minus=DIVERSO, agevolato=False,
nota="Fondo comune / OICR armonizzato"),
"azione": dict(categoria_plus=DIVERSO, categoria_minus=DIVERSO, agevolato=False,
nota="Partecipazione non qualificata"),
"obbligazione": dict(categoria_plus=DIVERSO, categoria_minus=DIVERSO, agevolato=False,
nota="Obbligazione corporate: capital gain reddito diverso"),
"titolo_stato": dict(categoria_plus=DIVERSO, categoria_minus=DIVERSO, agevolato=True,
nota="Titolo di Stato IT / White List / ente assimilato"),
"certificate": dict(categoria_plus=DIVERSO, categoria_minus=DIVERSO, agevolato=False,
nota="Certificate: reddito diverso"),
"liquidita": dict(categoria_plus=CAPITALE, categoria_minus=None, agevolato=False,
nota="Interessi: reddito di capitale, nessuna minusvalenza"),
}
# Tipi per cui il motore NON calcola: il regime dipende da elementi da accertare.
DA_ACCERTARE = {
"etc_etn": "ETC/ETN: il trattamento ETF non e' applicabile automaticamente. "
"Verificare la sezione 'Taxation in Italy' del prospetto del singolo "
"strumento prima di classificarne i proventi.",
"etf_non_armonizzato": "OICR non armonizzato: il regime puo differire e concorrere "
"al reddito complessivo. Verificare caso per caso.",
"cripto": "Regime delle cripto-attivita modificato piu volte: verificare l'anno "
"d'imposta prima di qualunque calcolo.",
"fondo_pensione": "Previdenza complementare: regime autonomo, fuori dallo schema "
"redditi di capitale / redditi diversi.",
"pir": "PIR: possibile esenzione subordinata a requisiti di durata e composizione.",
}
FONTI_DA_CITARE = [
"TUIR art. 44 (redditi di capitale) - DPR 917/1986, Normattiva: "
"https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:"
"decreto.del.presidente.della.repubblica:1986-12-22;917",
"TUIR art. 67-68 (redditi diversi, minusvalenze) - DPR 917/1986, Normattiva",
"DL 66/2014 art. 3 c. 5 (redditi diversi da titoli pubblici computati al 48,08%) - "
"Normattiva: https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:"
"decreto.legge:2014-04-24;66~art3",
"Agenzia delle Entrate, Circolare 19/E del 27/06/2014 (OICR: proventi e perdite "
"riferibili a titoli pubblici computati al netto del 51,92%): "
"https://def.finanze.it/DocTribFrontend/getPrassiDetail.do?id="
"%7B7953D773-A884-4630-A7EB-EF5187839207%7D",
"Istruzioni ai modelli dichiarativi / prassi Agenzia delle Entrate: "
"https://www.agenziaentrate.gov.it/portale/",
"Borsa Italiana, ETC/ETN - Valori Ufficiali: per la fiscalita' rimanda alla "
"sezione 'Taxation in Italy' dei Supplementi ai Prospetti di emissione: "
"https://www.borsaitaliana.it/etc-etn/statisticheetc/valoriufficialicopy/"
"valoriufficialicopy.htm",
"Vigenza: dal 01/01/2027 si applica il nuovo testo unico D.Lgs. 117/2026 e la "
"numerazione degli articoli cambia. Verificare il periodo d'imposta del caso.",
]
def classifica(tipo):
tipo = tipo.lower().strip()
if tipo in DA_ACCERTARE:
return {
"tipo": tipo,
"calcolabile": False,
"motivo": DA_ACCERTARE[tipo],
"verificare": [DA_ACCERTARE[tipo]],
"fonti": FONTI_DA_CITARE,
}
if tipo not in CATEGORIE:
return {
"tipo": tipo,
"calcolabile": False,
"motivo": "Tipo non riconosciuto. Risalire da ISIN alla natura giuridica "
"prima di procedere.",
"tipi_noti": sorted(CATEGORIE) + sorted(DA_ACCERTARE),
"verificare": ["Natura giuridica dello strumento non determinata"],
"fonti": FONTI_DA_CITARE,
}
c = CATEGORIE[tipo]
return {
"tipo": tipo,
"calcolabile": True,
"categoria_plusvalenza": c["categoria_plus"],
"categoria_minusvalenza": c["categoria_minus"],
"plus_compensabile_con_zainetto": c["categoria_plus"] == DIVERSO,
"minus_alimenta_zainetto": c["categoria_minus"] == DIVERSO,
"componente_agevolata": c["agevolato"],
"nota": c["nota"],
"fonti": FONTI_DA_CITARE,
}
def simula_vendita(tipo, pmc, prezzo, quantita, minus_disponibili=0.0,
quota_stato=None, costi=0.0):
"""Simula la vendita di una posizione.
Importi in euro. `quota_stato` e' la quota agevolata di un OICR (0..1):
se None il dato e' mancante e per gli OICR viene restituito uno scenario
min/max invece di un singolo importo.
Due meccanismi distinti, da non confondere:
- Titoli pubblici agevolati: il REDDITO DIVERSO e' computato nella misura
del 48,08% dell'ammontare realizzato (art. 3 c. 5 DL 66/2014, che
modifica gli artt. 5, 6 e 7 del D.Lgs. 461/1997). La riduzione precede
quindi la compensazione con lo zainetto, e vale anche per le perdite.
- OICR/ETF con componente in titoli pubblici: proventi e perdite riferibili
alla componente pubblica sono computati al 48,08%; sulla parte restante
si applica il regime ordinario.
"""
info = classifica(tipo)
if not info["calcolabile"]:
return {"errore": info["motivo"], "verificare": info["verificare"],
"fonti": info["fonti"], "imposta_stimata": None}
if quota_stato is not None:
try:
quota_stato = float(quota_stato)
except (TypeError, ValueError):
return {
"errore": "quota_stato deve essere un numero compreso tra 0 e 1.",
"verificare": ["Quota agevolata non valida"],
"fonti": info["fonti"],
"imposta_stimata": None,
}
if not 0.0 <= quota_stato <= 1.0:
return {
"errore": "quota_stato deve essere compresa tra 0 e 1, non %.4g." % quota_stato,
"verificare": ["Quota agevolata fuori intervallo: non viene corretta automaticamente"],
"fonti": info["fonti"],
"imposta_stimata": None,
}
verificare = []
costi = max(0.0, float(costi))
controvalore = prezzo * quantita
costo = pmc * quantita
risultato = controvalore - costo - costi
out = {
"tipo": tipo,
"controvalore": r(controvalore),
"costo_carico": r(costo),
"risultato_lordo": r(risultato),
"categoria_reddito": (info["categoria_plusvalenza"] if risultato > 0
else info["categoria_minusvalenza"]),
"verificare": verificare,
"fonti": info["fonti"],
}
minus_disponibili = max(0.0, float(minus_disponibili))
e_oicr = tipo in ("etf", "oicr")
if risultato <= 0:
perdita = -risultato
if not info["minus_alimenta_zainetto"]:
minus_generate = 0.0
verificare.append("La differenza negativa su questo strumento potrebbe non "
"essere deducibile: verificare.")
elif info["componente_agevolata"]:
# La perdita e' un reddito diverso, computato al 48,08%.
minus_generate = perdita * FRAZIONE_IMPONIBILE_AGEVOLATA
verificare.append("Perdita su titoli pubblici agevolati: computata nella "
"misura del 48,08%% dell'ammontare realizzato (%.2f su "
"%.2f), art. 3 c. 5 DL 66/2014."
% (minus_generate, perdita))
elif e_oicr:
if quota_stato is None:
minus_max = perdita
minus_min = perdita * FRAZIONE_IMPONIBILE_AGEVOLATA
out.update({
"imposta_stimata": 0.0,
"minusvalenza_generata": None,
"zainetto_dopo": None,
"dato_mancante": "quota_stato",
"minusvalenza_scenario": {
"quota_agevolata_0": r(minus_max),
"quota_agevolata_100": r(minus_min),
},
})
verificare.append(
"Quota di titoli di Stato/White List del fondo NON FORNITA: la "
"minusvalenza deducibile non e' puntualmente calcolabile. La "
"Circolare 19/E del 27/06/2014 riduce del 51,92% la parte della "
"perdita riferibile ai titoli pubblici."
)
verificare.append("Minusvalenze utilizzabili entro il 4o anno successivo a quello "
"di realizzo: verificare il termine vigente.")
return out
quota_ordinaria = 1.0 - quota_stato
minus_generate = perdita * (
quota_ordinaria + quota_stato * FRAZIONE_IMPONIBILE_AGEVOLATA
)
verificare.append(
"Perdita OICR: la parte riferibile a titoli pubblici (%.2f%%) e' "
"computata al 48,08%%; minus deducibile %.2f su perdita %.2f. "
"Circolare Agenzia Entrate 19/E del 27/06/2014."
% (quota_stato * 100, minus_generate, perdita)
)
else:
minus_generate = perdita
out.update({
"imposta_stimata": 0.0,
"minusvalenza_generata": r(minus_generate),
"zainetto_dopo": r(minus_disponibili + minus_generate),
})
verificare.append("Minusvalenze utilizzabili entro il 4o anno successivo a quello "
"di realizzo: verificare il termine vigente.")
return out
compensabile = info["plus_compensabile_con_zainetto"]
if info["componente_agevolata"]:
# Il reddito diverso e' ridotto PRIMA della compensazione.
rilevante = risultato * FRAZIONE_IMPONIBILE_AGEVOLATA
minus_usate = min(minus_disponibili, rilevante)
imponibile = rilevante - minus_usate
imposta = imponibile * ALIQUOTA_ORDINARIA
out["reddito_diverso_rilevante"] = r(rilevante)
out["quota_agevolata_applicata"] = 1.0
verificare.append("Reddito diverso computato al 48,08%% dell'ammontare realizzato "
"(%.2f su %.2f) e solo dopo compensato con lo zainetto: "
"art. 3 c. 5 DL 66/2014." % (rilevante, risultato))
else:
minus_usate = min(minus_disponibili, risultato) if compensabile else 0.0
imponibile = risultato - minus_usate
if e_oicr and quota_stato is None:
# Dato mancante: nessun importo singolo, solo lo scenario.
imposta_max = imponibile * ALIQUOTA_ORDINARIA
imposta_min = imponibile * FRAZIONE_IMPONIBILE_AGEVOLATA * ALIQUOTA_ORDINARIA
out.update({
"minusvalenze_disponibili": r(minus_disponibili),
"minusvalenze_utilizzate": r(minus_usate),
"zainetto_residuo": r(minus_disponibili - minus_usate),
"imponibile": r(imponibile),
"quota_agevolata_applicata": None,
"dato_mancante": "quota_stato",
"imposta_stimata": None,
"imposta_scenario": {"quota_agevolata_0": r(imposta_max),
"quota_agevolata_100": r(imposta_min)},
})
verificare.append("Quota di titoli di Stato/White List del fondo NON FORNITA: "
"nessun importo singolo calcolabile. Riportare l'intervallo "
"%.2f - %.2f e recuperare la percentuale comunicata "
"dall'emittente o dall'intermediario."
% (imposta_min, imposta_max))
if not compensabile and minus_disponibili > 0:
verificare.append("Plusvalenza qualificata come reddito di capitale: NON "
"abbattuta dalle minusvalenze in zainetto.")
verificare.append("Aliquota e frazione imponibile sono valori di riferimento "
"non verificati (references/regole-correnti.md).")
return out
quota_agev = quota_stato or 0.0
base_agevolata = imponibile * quota_agev
base_ordinaria = imponibile - base_agevolata
imposta = (base_ordinaria * ALIQUOTA_ORDINARIA
+ base_agevolata * FRAZIONE_IMPONIBILE_AGEVOLATA * ALIQUOTA_ORDINARIA)
out["quota_agevolata_applicata"] = quota_agev
if e_oicr and quota_agev > 0.0:
verificare.append("Quota agevolata assunta pari a %.2f%%: confermarla sulla "
"comunicazione dell'emittente/intermediario."
% (quota_agev * 100))
if not compensabile and minus_disponibili > 0:
verificare.append("Plusvalenza qualificata come reddito di capitale: NON abbattuta "
"dalle minusvalenze in zainetto. Verificare la qualificazione "
"dello strumento prima di considerare definitivo il risultato.")
verificare.append("Aliquota e frazione imponibile sono valori di riferimento non "
"verificati (references/regole-correnti.md).")
out.update({
"minusvalenze_disponibili": r(minus_disponibili),
"minusvalenze_utilizzate": r(minus_usate),
"zainetto_residuo": r(minus_disponibili - minus_usate),
"imponibile": r(imponibile),
"imposta_stimata": r(imposta),
"netto_incassato": r(controvalore - imposta - costi),
"aliquota_effettiva_su_risultato": r(imposta / risultato),
})
return out
def r(x):
return round(float(x), 2)
def main(argv=None):
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
sub = p.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("classifica", help="natura fiscale di un tipo di strumento")
c.add_argument("--tipo", required=True)
v = sub.add_parser("vendita", help="simula la vendita di una posizione")
v.add_argument("--tipo", required=True)
v.add_argument("--pmc", type=float, required=True, help="prezzo medio di carico")
v.add_argument("--prezzo", type=float, required=True, help="prezzo di vendita")
v.add_argument("--quantita", type=float, required=True)
v.add_argument("--minus", type=float, default=0.0, help="minusvalenze in zainetto")
v.add_argument("--quota-stato", type=float, default=None,
help="quota agevolata del fondo, da 0 a 1; se omessa per un OICR "
"il risultato e' uno scenario min/max")
v.add_argument("--costi", type=float, default=0.0, help="commissioni di negoziazione")
a = p.parse_args(argv)
if a.cmd == "classifica":
res = classifica(a.tipo)
else:
res = simula_vendita(a.tipo, a.pmc, a.prezzo, a.quantita,
a.minus, a.quota_stato, a.costi)
print(json.dumps(res, indent=2, ensure_ascii=False))
return 1 if "errore" in res else 0
if __name__ == "__main__":
sys.exit(main())
SHA-256: 34a44e2ae93e9279ab7e255b26104803f023d3e7d213bd5fd0fc91ea15853bc6