← Files italian-investorARCHIVED FILE
skills/italian-investor/scripts/lot_sale.py
6.16 KB · Oct 5, 2026 · 18:32 UTC
#!/usr/bin/env python3
"""Simula una vendita partendo dai lotti reali invece che da un PMC generico.
Questo helper collega `cost_basis.py` e `tax_engine.py`. Determina il metodo di
base fiscale solo nei casi espressamente coperti dalla regola operativa della
skill:
- amministrato -> CMP per strumenti che generano redditi diversi da cessione;
- dichiarativo -> LIFO per le fattispecie coperte dall'art. 67 c.1-bis TUIR.
ETF/OICR vengono bloccati in automatico: la determinazione del provento OICR
segue regole proprie e non va assimilata meccanicamente al LIFO dei redditi
diversi.
CSV lotti minimo:
data_acquisto,quantita,costo_unitario_eur,costi_acquisto_eur
Esempio:
python scripts/lot_sale.py vendita --tipo azione --regime dichiarativo \
--lotti examples/lotti-esempio.csv --prezzo 140 --quantita 15
"""
import argparse
import json
import sys
from cost_basis import carica_lotti, calcola_e_consuma
from tax_engine import simula_vendita
TIPI_LOT_AWARE = {"azione", "obbligazione", "titolo_stato", "certificate"}
def metodo_da_regime(tipo, regime):
tipo = str(tipo).strip().lower()
regime = str(regime).strip().lower()
if tipo not in TIPI_LOT_AWARE:
return None, (
"LOT_METHOD_NOT_AUTOMATIC: il metodo base fiscale non viene dedotto per %s. "
"Per ETF/OICR la disciplina del provento e' distinta; per altri strumenti "
"serve una qualificazione specifica." % tipo
)
if regime == "amministrato":
return "cmp", None
if regime == "dichiarativo":
return "lifo", None
return None, "Regime non supportato: %s" % regime
def _serializza_lotti(lotti):
if lotti is None:
return None
return [
{
"data_acquisto": x["data_acquisto"],
"quantita": round(float(x["quantita"]), 8),
"costo_unitario_eur": round(float(x["costo_unitario_eur"]), 8),
"costi_acquisto_eur": round(float(x.get("costi_acquisto_eur") or 0.0), 8),
"costo_totale_eur": round(float(x["costo_totale_eur"]), 8),
}
for x in lotti
]
def simula_vendita_lotti(tipo, regime, lotti, prezzo, quantita,
minus_disponibili=0.0, quota_stato=None,
costi_vendita=0.0, metodo=None):
tipo = str(tipo).strip().lower()
regime = str(regime).strip().lower()
if metodo is None:
metodo, errore = metodo_da_regime(tipo, regime)
if errore:
return {
"errore": errore,
"imposta_stimata": None,
"base_fiscale": None,
"lotti_residui": None,
}
else:
metodo = str(metodo).strip().lower()
if metodo not in ("cmp", "lifo"):
return {"errore": "metodo deve essere cmp o lifo", "imposta_stimata": None}
try:
base = calcola_e_consuma(lotti, metodo, quantita)
except ValueError as exc:
return {
"errore": str(exc),
"imposta_stimata": None,
"base_fiscale": None,
"lotti_residui": None,
}
if base.get("errore"):
return {
"errore": base["errore"],
"imposta_stimata": None,
"base_fiscale": base,
"lotti_residui": None,
}
q = float(quantita)
base_totale = float(base["base_costo_vendita_eur"])
pmc_fiscale = base_totale / q
vendita = simula_vendita(
tipo,
pmc_fiscale,
float(prezzo),
q,
minus_disponibili=float(minus_disponibili or 0.0),
quota_stato=quota_stato,
costi=float(costi_vendita or 0.0),
)
base_pubblica = dict(base)
residui = base_pubblica.pop("lotti_residui", None)
return {
"tipo": tipo,
"regime": regime,
"metodo_base": metodo,
"base_fiscale": base_pubblica,
"pmc_fiscale_derivato": round(pmc_fiscale, 8),
"vendita": vendita,
"imposta_stimata": vendita.get("imposta_stimata"),
"lotti_residui": residui,
"lotti_residui_serializzati": _serializza_lotti(residui),
"verificare": [
"Il metodo e' stato scelto in base al regime solo per una fattispecie coperta; verificare sempre che lo strumento e l'evento rientrino davvero nella regola.",
"I lotti devono essere gia' convertiti correttamente in EUR con cambi fiscalmente rilevanti e costi inerenti documentabili.",
"Lo stato residuo CMP e' un pool simulato per proseguire calcoli nello stesso regime; non usarlo per dedurre un ordine LIFO dopo un cambio di regime.",
],
}
def main(argv=None):
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
sub = p.add_subparsers(dest="cmd", required=True)
v = sub.add_parser("vendita", help="simula vendita da lotti")
v.add_argument("--tipo", required=True)
v.add_argument("--regime", choices=("amministrato", "dichiarativo"), required=True)
v.add_argument("--lotti", required=True)
v.add_argument("--prezzo", type=float, required=True)
v.add_argument("--quantita", type=float, required=True)
v.add_argument("--minus", type=float, default=0.0)
v.add_argument("--quota-stato", type=float)
v.add_argument("--costi-vendita", type=float, default=0.0)
v.add_argument("--metodo", choices=("cmp", "lifo"),
help="override esplicito; usare solo se gia' verificato")
args = p.parse_args(argv)
try:
lotti = carica_lotti(args.lotti)
out = simula_vendita_lotti(
args.tipo, args.regime, lotti, args.prezzo, args.quantita,
args.minus, args.quota_stato, args.costi_vendita, args.metodo,
)
# I dizionari normalizzati contengono campi tecnici interni: in CLI usa
# soltanto la versione serializzata.
out.pop("lotti_residui", None)
except (OSError, ValueError) as exc:
out = {"errore": str(exc), "imposta_stimata": None}
print(json.dumps(out, ensure_ascii=False, indent=2))
return 1 if out.get("errore") else 0
if __name__ == "__main__":
sys.exit(main())
SHA-256: ed9321e617ffc0c28248da0862432d6af18c4514e59259623b43fe3ed561597d