| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- from decimal import Decimal
- from django.utils import timezone
- from .models import Dividendo, Operacion, Valor
- def actualizar_precios():
- """Obtiene la última cotización de cada valor desde Yahoo Finance.
- Devuelve (correctos, errores) donde errores es una lista de
- (ticker, motivo).
- """
- import yfinance as yf
- correctos = 0
- errores = []
- for valor in Valor.objects.all():
- try:
- cierre = yf.Ticker(valor.ticker).history(period='5d')['Close']
- precio = Decimal(str(cierre.iloc[-1])).quantize(Decimal('0.0001'))
- except Exception as e:
- errores.append((valor.ticker, str(e)[:100]))
- continue
- valor.precio_actual = precio
- valor.actualizado_en = timezone.now()
- valor.save(update_fields=['precio_actual', 'actualizado_en'])
- correctos += 1
- return correctos, errores
- def posiciones():
- """Calcula la situación de cada valor con operaciones registradas.
- Devuelve una lista de diccionarios ordenada por ticker con:
- - acciones en cartera, precio medio ponderado e importe invertido
- - plusvalías realizadas (ventas) según precio medio ponderado
- - dividendos netos cobrados
- """
- resultado = []
- for valor in Valor.objects.all():
- ops = valor.operaciones.order_by('fecha', 'pk')
- if not ops and not valor.dividendos.exists():
- continue
- acciones = Decimal('0')
- coste = Decimal('0') # coste de las acciones aún en cartera
- precio_medio = Decimal('0')
- plusvalias = Decimal('0') # realizadas en ventas
- comisiones = Decimal('0')
- for op in ops:
- comisiones += op.comisiones
- if op.tipo == Operacion.COMPRA:
- acciones += op.cantidad
- coste += op.cantidad * op.precio + op.comisiones
- if acciones > 0:
- precio_medio = (coste / acciones).quantize(Decimal('0.0001'))
- else:
- if acciones <= 0:
- continue
- pm = coste / acciones
- vendidas = min(op.cantidad, acciones)
- ingresos = op.cantidad * op.precio - op.comisiones
- plusvalias += ingresos - (vendidas * pm)
- acciones -= vendidas
- coste -= vendidas * pm
- if acciones > 0:
- precio_medio = (coste / acciones).quantize(Decimal('0.0001'))
- else:
- precio_medio = Decimal('0')
- dividendos = sum((d.neto for d in valor.dividendos.all()), Decimal('0'))
- valoracion = None
- latente = None
- if acciones > 0 and valor.precio_actual is not None:
- valoracion = (acciones * valor.precio_actual).quantize(Decimal('0.01'))
- latente = (valoracion - coste).quantize(Decimal('0.01'))
- resultado.append({
- 'valor': valor,
- 'acciones': acciones,
- 'precio_medio': precio_medio,
- 'invertido': coste.quantize(Decimal('0.01')),
- 'plusvalias': plusvalias.quantize(Decimal('0.01')),
- 'comisiones': comisiones,
- 'dividendos': dividendos,
- 'valoracion': valoracion,
- 'latente': latente,
- })
- return sorted(resultado, key=lambda p: p['valor'].ticker)
- def totales(posiciones_lista=None):
- """Agrega los importes globales de la cartera."""
- posiciones_lista = posiciones_lista if posiciones_lista is not None else posiciones()
- abiertas = [p for p in posiciones_lista if p['acciones'] > 0]
- con_precio = [p for p in abiertas if p['valoracion'] is not None]
- invertido_con_precio = sum((p['invertido'] for p in con_precio), Decimal('0'))
- return {
- 'invertido': sum((p['invertido'] for p in abiertas), Decimal('0')),
- 'plusvalias': sum((p['plusvalias'] for p in posiciones_lista), Decimal('0')),
- 'dividendos': sum((p['dividendos'] for p in posiciones_lista), Decimal('0')),
- 'num_abiertas': len(abiertas),
- 'valoracion': sum((p['valoracion'] for p in con_precio), Decimal('0')),
- 'latente': (
- sum((p['latente'] for p in con_precio), Decimal('0'))
- if len(con_precio) == len(abiertas) else None
- ),
- }
- def desglose_fifo():
- """Desglosa cada venta con su coste de adquisición según FIFO.
- Devuelve una lista de filas (una por lote consumido) ordenada por fecha,
- con la plusvalía o pérdida realizada en cada una.
- """
- from collections import deque
- filas = []
- for valor in Valor.objects.all():
- lotes = deque() # pares [cantidad disponible, coste unitario efectivo]
- for op in valor.operaciones.order_by('fecha', 'pk'):
- if op.tipo == Operacion.COMPRA:
- coste_unitario = (op.cantidad * op.precio + op.comisiones) / op.cantidad
- lotes.append([op.cantidad, coste_unitario])
- elif op.tipo == Operacion.VENTA:
- por_vender = op.cantidad
- comision_unitaria = op.comisiones / op.cantidad
- while por_vender > 0 and lotes:
- disponibles, coste_lote = lotes[0]
- usadas = min(por_vender, disponibles)
- importe = usadas * (op.precio - comision_unitaria)
- coste = usadas * coste_lote
- filas.append({
- 'fecha': op.fecha,
- 'valor': valor,
- 'cantidad': usadas,
- 'precio_venta': op.precio,
- 'importe': importe.quantize(Decimal('0.01')),
- 'coste': coste.quantize(Decimal('0.01')),
- 'resultado': (importe - coste).quantize(Decimal('0.01')),
- })
- por_vender -= usadas
- if disponibles > usadas:
- lotes[0][0] = disponibles - usadas
- else:
- lotes.popleft()
- if por_vender > 0:
- # venta sin acciones suficientes registradas (dato inconsistente)
- filas.append({
- 'fecha': op.fecha,
- 'valor': valor,
- 'cantidad': por_vender,
- 'precio_venta': op.precio,
- 'importe': Decimal('0'),
- 'coste': Decimal('0'),
- 'resultado': None,
- })
- filas.sort(key=lambda f: f['fecha'])
- return filas
- def resumen_fiscal(filas=None):
- """Agrupa el desglose FIFO por año natural."""
- from collections import defaultdict
- filas = filas if filas is not None else desglose_fifo()
- por_anyo = defaultdict(lambda: Decimal('0'))
- for f in filas:
- if f['resultado'] is not None:
- por_anyo[f['fecha'].year] += f['resultado']
- return {anyo: imp.quantize(Decimal('0.01')) for anyo, imp in sorted(por_anyo.items())}
|