CtrlK
BlogDocsLog inGet started
Tessl Logo

financial-insights

Patrones para generar insights financieros inteligentes y sugerencias de ahorro contextuales. Incluye clasificación de gastos evitables vs necesarios, detección de oportunidades de ahorro, y mensajes personalizados. Usar cuando se trabaje con recomendaciones al usuario o análisis de hábitos de gasto.

Install with Tessl CLI

npx tessl i github:iru97/Savvy-ai --skill financial-insights
What are skills?

74

Does it follow best practices?

Validation for skill structure

SKILL.md
Review
Evals

Financial Insights - Savvy AI

Conocimiento especializado para generar insights financieros inteligentes y sugerencias de ahorro que respeten el contexto del usuario.

Principio Fundamental

NO todas las sugerencias de ahorro son válidas. El sistema debe entender:

❌ NUNCA sugerir: "Deja de pagar el alquiler"
❌ NUNCA sugerir: "No compres comida"
✅ SÍ sugerir: "Estás gastando $200/mes en delivery, cocinar más ahorraría $120"
✅ SÍ sugerir: "Tu suscripción de gym ($50/mes) no se ha usado en 2 meses"

Clasificación de Gastos por Reducibilidad

Matriz de Reducibilidad

enum Reducibility {
  untouchable,  // NO sugerir reducir NUNCA
  difficult,    // Sugerir con mucho cuidado
  moderate,     // Sugerir con contexto
  easy,         // Sugerir libremente
}

final Map<String, Map<String, Reducibility>> reducibilityMatrix = {
  // GASTOS FIJOS - Generalmente intocables
  'vivienda': {
    'renta': Reducibility.untouchable,
    'hipoteca': Reducibility.untouchable,
    'mantenimiento': Reducibility.difficult,
    'impuestos_prediales': Reducibility.untouchable,
  },
  'servicios': {
    'electricidad': Reducibility.moderate,  // "Apaga luces" es válido
    'agua': Reducibility.moderate,
    'gas': Reducibility.moderate,
    'internet': Reducibility.difficult,     // Necesario para trabajo
    'telefono': Reducibility.moderate,
  },
  'seguros': {
    'seguro_salud': Reducibility.untouchable,
    'seguro_auto': Reducibility.difficult,  // Legal en muchos países
    'seguro_vida': Reducibility.moderate,
    'seguro_hogar': Reducibility.moderate,
  },
  'deudas': {
    'prestamo_personal': Reducibility.untouchable,  // Obligación legal
    'tarjeta_credito': Reducibility.untouchable,
    'prestamo_auto': Reducibility.untouchable,
    'prestamo_estudiantil': Reducibility.untouchable,
  },

  // GASTOS VARIABLES - Más flexibles
  'alimentacion': {
    'supermercado': Reducibility.difficult,   // Necesario, pero optimizable
    'restaurantes': Reducibility.easy,        // ← Oportunidad de ahorro
    'cafeterias': Reducibility.easy,          // ← Oportunidad de ahorro
    'delivery': Reducibility.easy,            // ← Oportunidad de ahorro
  },
  'transporte': {
    'gasolina': Reducibility.moderate,        // Depende de si hay alternativas
    'uber_taxi': Reducibility.easy,           // ← Oportunidad de ahorro
    'transporte_publico': Reducibility.difficult,
    'estacionamiento': Reducibility.moderate,
  },
  'entretenimiento': {
    'cine': Reducibility.easy,
    'eventos': Reducibility.easy,
    'hobbies': Reducibility.easy,
    'viajes': Reducibility.easy,
  },
  'suscripciones': {
    'streaming': Reducibility.easy,           // ← Oportunidad de ahorro
    'software': Reducibility.moderate,        // Puede ser necesario para trabajo
    'gimnasio': Reducibility.easy,            // ← Si no se usa
    'membresias': Reducibility.easy,
  },
  'compras': {
    'ropa': Reducibility.easy,
    'electronicos': Reducibility.easy,
    'hogar': Reducibility.moderate,
    'cuidado_personal': Reducibility.moderate,
  },
};

Generador de Insights

Tipos de Insights

enum InsightType {
  warning,        // Alerta de gasto excesivo
  opportunity,    // Oportunidad de ahorro
  pattern,        // Patrón detectado
  achievement,    // Logro positivo
  comparison,     // Comparación temporal
  projection,     // Proyección futura
}

enum InsightPriority {
  high,    // Mostrar inmediatamente
  medium,  // Mostrar en resumen semanal
  low,     // Mostrar si el usuario pregunta
}

class FinancialInsight {
  final InsightType type;
  final InsightPriority priority;
  final String title;
  final String message;
  final String? actionableAdvice;
  final double? potentialSavings;
  final String? relatedCategoryId;
  final DateTime generatedAt;

  const FinancialInsight({
    required this.type,
    required this.priority,
    required this.title,
    required this.message,
    this.actionableAdvice,
    this.potentialSavings,
    this.relatedCategoryId,
    required this.generatedAt,
  });
}

Reglas de Generación de Insights

class InsightGenerator {
  /// Genera insights basados en transacciones del período
  static List<FinancialInsight> generate({
    required List<Transaction> transactions,
    required List<Budget> budgets,
    required Map<String, Category> categories,
    required HistoricalData history,
  }) {
    final insights = <FinancialInsight>[];

    // 1. Análisis de subcategorías
    insights.addAll(_analyzeSubcategories(transactions, categories));

    // 2. Comparación con mes anterior
    insights.addAll(_compareWithPrevious(transactions, history));

    // 3. Detección de gastos inusuales
    insights.addAll(_detectUnusualSpending(transactions, history));

    // 4. Análisis de suscripciones no usadas
    insights.addAll(_analyzeUnusedSubscriptions(transactions));

    // 5. Oportunidades de ahorro fácil
    insights.addAll(_findEasySavings(transactions, categories));

    return insights..sort((a, b) => a.priority.index.compareTo(b.priority.index));
  }

  /// Analiza subcategorías para encontrar concentración de gastos
  static List<FinancialInsight> _analyzeSubcategories(
    List<Transaction> transactions,
    Map<String, Category> categories,
  ) {
    final insights = <FinancialInsight>[];
    final bySubcategory = _groupBySubcategory(transactions);

    for (final entry in bySubcategory.entries) {
      final subcatId = entry.key;
      final total = entry.value;
      final categoryId = _getCategoryForSubcategory(subcatId, categories);
      final categoryTotal = _getCategoryTotal(categoryId, transactions);

      // Si una subcategoría es >40% de su categoría padre
      if (categoryTotal > 0 && (total / categoryTotal) > 0.4) {
        final reducibility = _getReducibility(categoryId, subcatId);

        if (reducibility == Reducibility.easy) {
          final subcatName = _getSubcategoryName(subcatId, categories);
          final potentialSaving = total * 0.3; // Sugerir reducir 30%

          insights.add(FinancialInsight(
            type: InsightType.opportunity,
            priority: InsightPriority.high,
            title: 'Oportunidad de ahorro en $subcatName',
            message: 'Gastaste \$${total.toStringAsFixed(0)} en $subcatName este mes, '
                     'que representa ${((total/categoryTotal)*100).toStringAsFixed(0)}% '
                     'de tu gasto en ${_getCategoryName(categoryId, categories)}.',
            actionableAdvice: 'Si reduces este gasto un 30%, ahorrarías '
                              '\$${potentialSaving.toStringAsFixed(0)} al mes.',
            potentialSavings: potentialSaving,
            relatedCategoryId: categoryId,
            generatedAt: DateTime.now(),
          ));
        }
      }
    }

    return insights;
  }

  /// Compara con el mes anterior
  static List<FinancialInsight> _compareWithPrevious(
    List<Transaction> current,
    HistoricalData history,
  ) {
    final insights = <FinancialInsight>[];
    final currentTotal = _totalExpenses(current);
    final previousTotal = history.previousMonthExpenses;

    if (previousTotal > 0) {
      final change = ((currentTotal - previousTotal) / previousTotal) * 100;

      if (change > 20) {
        insights.add(FinancialInsight(
          type: InsightType.warning,
          priority: InsightPriority.high,
          title: 'Gastos aumentaron ${change.toStringAsFixed(0)}%',
          message: 'Este mes has gastado \$${currentTotal.toStringAsFixed(0)}, '
                   'comparado con \$${previousTotal.toStringAsFixed(0)} el mes pasado.',
          actionableAdvice: 'Revisa en qué categorías aumentó el gasto.',
          generatedAt: DateTime.now(),
        ));
      } else if (change < -10) {
        insights.add(FinancialInsight(
          type: InsightType.achievement,
          priority: InsightPriority.medium,
          title: '¡Redujiste gastos ${change.abs().toStringAsFixed(0)}%!',
          message: 'Este mes gastaste \$${currentTotal.toStringAsFixed(0)}, '
                   '\$${(previousTotal - currentTotal).toStringAsFixed(0)} menos que el mes pasado.',
          generatedAt: DateTime.now(),
        ));
      }
    }

    return insights;
  }

  /// Detecta gastos inusualmente altos
  static List<FinancialInsight> _detectUnusualSpending(
    List<Transaction> transactions,
    HistoricalData history,
  ) {
    final insights = <FinancialInsight>[];
    final byCategory = _groupByCategory(transactions);

    for (final entry in byCategory.entries) {
      final categoryId = entry.key;
      final currentTotal = entry.value;
      final average = history.getCategoryAverage(categoryId);

      // Si es más del doble del promedio
      if (average > 0 && currentTotal > average * 2) {
        insights.add(FinancialInsight(
          type: InsightType.warning,
          priority: InsightPriority.high,
          title: 'Gasto inusual detectado',
          message: 'Tu gasto en ${_getCategoryName(categoryId)} este mes '
                   '(\$${currentTotal.toStringAsFixed(0)}) es el doble de tu promedio '
                   '(\$${average.toStringAsFixed(0)}).',
          actionableAdvice: 'Revisa si hay algún gasto extraordinario o recurrente nuevo.',
          relatedCategoryId: categoryId,
          generatedAt: DateTime.now(),
        ));
      }
    }

    return insights;
  }

  /// Encuentra oportunidades de ahorro fácil
  static List<FinancialInsight> _findEasySavings(
    List<Transaction> transactions,
    Map<String, Category> categories,
  ) {
    final insights = <FinancialInsight>[];
    final easyToReduce = <String, double>{};

    for (final t in transactions.where((t) => t.tipo == TransactionType.egreso)) {
      final reducibility = _getReducibility(t.categoriaId, t.subcategoriaId);
      if (reducibility == Reducibility.easy) {
        final key = '${t.categoriaId}:${t.subcategoriaId ?? ""}';
        easyToReduce[key] = (easyToReduce[key] ?? 0) + t.monto;
      }
    }

    // Ordenar por monto y sugerir los top 3
    final sorted = easyToReduce.entries.toList()
      ..sort((a, b) => b.value.compareTo(a.value));

    final top3 = sorted.take(3);
    double totalPotential = 0;

    for (final entry in top3) {
      totalPotential += entry.value * 0.25; // Sugerir reducir 25%
    }

    if (totalPotential > 50) { // Solo si vale la pena
      insights.add(FinancialInsight(
        type: InsightType.opportunity,
        priority: InsightPriority.medium,
        title: 'Podrías ahorrar \$${totalPotential.toStringAsFixed(0)}/mes',
        message: 'Identificamos ${top3.length} categorías donde podrías reducir '
                 'gastos sin afectar tu calidad de vida.',
        actionableAdvice: 'Enfócate en: ${top3.map((e) => e.key.split(":")[0]).join(", ")}',
        potentialSavings: totalPotential,
        generatedAt: DateTime.now(),
      ));
    }

    return insights;
  }
}

Plantillas de Mensajes

Mensajes Positivos (Logros)

final achievementTemplates = [
  '¡Excelente! Gastaste {amount} menos que el mes pasado en {category}.',
  '¡Felicitaciones! Tu ahorro aumentó {percent}% este mes.',
  'Llevas {days} días sin gastar en {category}. ¡Sigue así!',
  'Tu balance es positivo por {amount}. Considera invertir el excedente.',
];

Mensajes de Oportunidad (Neutros)

final opportunityTemplates = [
  'Gastas {amount}/mes en {subcategory}. Si cocinas {times} veces más por semana, ahorrarías {savings}.',
  'Tu gasto en {category} representa {percent}% de tus ingresos. El promedio recomendado es {recommended}%.',
  '{subscription} te cuesta {amount}/mes. ¿Lo usas regularmente?',
  'Detectamos que compras {item} frecuentemente. Considera comprar al mayoreo.',
];

Mensajes de Advertencia (Cuidado)

final warningTemplates = [
  'Tu gasto en {category} aumentó {percent}% respecto al mes pasado.',
  'Estás gastando {amount} más de lo presupuestado en {category}.',
  'A este ritmo, terminarás el mes con un balance de {projected}.',
  'Llevas {count} transacciones en {category} esta semana, más que tu promedio.',
];

Reglas de Frecuencia

class InsightFrequencyRules {
  // No repetir el mismo insight en menos de X días
  static const Map<InsightType, int> cooldownDays = {
    InsightType.warning: 3,        // Advertencias cada 3 días máximo
    InsightType.opportunity: 7,    // Oportunidades cada semana
    InsightType.pattern: 14,       // Patrones cada 2 semanas
    InsightType.achievement: 1,    // Logros pueden ser diarios
    InsightType.comparison: 7,     // Comparaciones semanales
    InsightType.projection: 3,     // Proyecciones cada 3 días
  };

  // Máximo de insights por día
  static const int maxInsightsPerDay = 3;

  // Priorizar insights con mayor potencial de ahorro
  static const double minSavingsToShow = 20.0; // Mínimo $20 de ahorro potencial
}

Integración con UI

Widget de Insights

class InsightCard extends StatelessWidget {
  final FinancialInsight insight;
  final VoidCallback? onDismiss;
  final VoidCallback? onAction;

  @override
  Widget build(BuildContext context) {
    return Card(
      color: _getBackgroundColor(insight.type),
      child: ListTile(
        leading: Icon(_getIcon(insight.type), color: _getIconColor(insight.type)),
        title: Text(insight.title),
        subtitle: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(insight.message),
            if (insight.actionableAdvice != null)
              Text(
                insight.actionableAdvice!,
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
            if (insight.potentialSavings != null)
              Chip(
                label: Text('Ahorro: \$${insight.potentialSavings!.toStringAsFixed(0)}/mes'),
                backgroundColor: Colors.green.shade100,
              ),
          ],
        ),
        trailing: IconButton(
          icon: Icon(Icons.close),
          onPressed: onDismiss,
        ),
      ),
    );
  }

  Color _getBackgroundColor(InsightType type) {
    switch (type) {
      case InsightType.warning: return Colors.orange.shade50;
      case InsightType.opportunity: return Colors.blue.shade50;
      case InsightType.achievement: return Colors.green.shade50;
      case InsightType.pattern: return Colors.purple.shade50;
      default: return Colors.grey.shade50;
    }
  }

  IconData _getIcon(InsightType type) {
    switch (type) {
      case InsightType.warning: return Icons.warning_amber;
      case InsightType.opportunity: return Icons.lightbulb_outline;
      case InsightType.achievement: return Icons.emoji_events;
      case InsightType.pattern: return Icons.trending_up;
      case InsightType.comparison: return Icons.compare_arrows;
      case InsightType.projection: return Icons.timeline;
    }
  }
}

Ejemplos de Insights Generados

Caso 1: Usuario gasta mucho en delivery

{
  "type": "opportunity",
  "priority": "high",
  "title": "Oportunidad de ahorro en Delivery",
  "message": "Gastaste $320 en Delivery este mes, que representa 65% de tu gasto en Alimentación.",
  "actionableAdvice": "Si reduces este gasto un 30%, ahorrarías $96 al mes. Considera cocinar 2 días más por semana.",
  "potentialSavings": 96.0,
  "relatedCategoryId": "alimentacion"
}

Caso 2: Suscripción no usada

{
  "type": "opportunity",
  "priority": "medium",
  "title": "¿Usas tu suscripción de Gimnasio?",
  "message": "Pagas $50/mes por el gimnasio pero no hemos detectado gastos relacionados (estacionamiento, suplementos, ropa deportiva) en los últimos 60 días.",
  "actionableAdvice": "Si no lo usas, cancelar te ahorraría $600 al año.",
  "potentialSavings": 50.0
}

Caso 3: Logro positivo

{
  "type": "achievement",
  "priority": "medium",
  "title": "¡Redujiste gastos en Entretenimiento!",
  "message": "Este mes gastaste $80 en Entretenimiento, $120 menos que tu promedio de $200.",
  "actionableAdvice": null,
  "potentialSavings": null
}

Notas de Implementación

  1. Nunca ser condescendiente - Los mensajes deben ser informativos, no regañones
  2. Respetar privacidad - No mencionar comercios específicos sin consentimiento
  3. Contextualizar - Un gasto alto en diciembre puede ser normal (regalos)
  4. Evitar fatiga - Máximo 3 insights por día
  5. Celebrar logros - Balance entre advertencias y felicitaciones
Repository
iru97/Savvy-ai
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.