fin-infra 0.1.82__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fin_infra/analytics/__init__.py +2 -2
- fin_infra/analytics/add.py +3 -3
- fin_infra/analytics/cash_flow.py +3 -3
- fin_infra/analytics/models.py +5 -5
- fin_infra/analytics/portfolio.py +12 -12
- fin_infra/analytics/spending.py +4 -5
- fin_infra/banking/history.py +4 -4
- fin_infra/brokerage/__init__.py +7 -7
- fin_infra/budgets/alerts.py +2 -2
- fin_infra/budgets/ease.py +1 -2
- fin_infra/budgets/models.py +1 -2
- fin_infra/budgets/templates.py +4 -4
- fin_infra/budgets/tracker.py +3 -3
- fin_infra/categorization/__init__.py +1 -1
- fin_infra/categorization/ease.py +3 -3
- fin_infra/categorization/engine.py +1 -1
- fin_infra/categorization/llm_layer.py +6 -4
- fin_infra/categorization/models.py +3 -4
- fin_infra/chat/planning.py +1 -1
- fin_infra/cli/cmds/scaffold_cmds.py +6 -6
- fin_infra/credit/experian/parser.py +5 -5
- fin_infra/crypto/__init__.py +2 -2
- fin_infra/documents/models.py +2 -3
- fin_infra/goals/management.py +3 -3
- fin_infra/goals/milestones.py +6 -6
- fin_infra/goals/models.py +2 -2
- fin_infra/investments/__init__.py +2 -2
- fin_infra/investments/ease.py +2 -2
- fin_infra/investments/models.py +24 -26
- fin_infra/investments/providers/base.py +4 -4
- fin_infra/investments/scaffold_templates/README.md +17 -17
- fin_infra/markets/__init__.py +2 -2
- fin_infra/models/accounts.py +4 -5
- fin_infra/models/transactions.py +2 -2
- fin_infra/net_worth/__init__.py +6 -6
- fin_infra/net_worth/aggregator.py +1 -1
- fin_infra/net_worth/calculator.py +1 -1
- fin_infra/net_worth/insights.py +7 -7
- fin_infra/normalization/__init__.py +2 -2
- fin_infra/normalization/currency_converter.py +7 -8
- fin_infra/normalization/models.py +9 -10
- fin_infra/normalization/providers/static_mappings.py +1 -1
- fin_infra/normalization/symbol_resolver.py +3 -4
- fin_infra/obs/classifier.py +2 -2
- fin_infra/providers/brokerage/alpaca.py +1 -1
- fin_infra/recurring/add.py +1 -1
- fin_infra/recurring/detectors_llm.py +5 -5
- fin_infra/recurring/ease.py +4 -4
- fin_infra/recurring/insights.py +15 -14
- fin_infra/recurring/normalizer.py +6 -6
- fin_infra/recurring/normalizers.py +27 -26
- fin_infra/scaffold/goals.py +4 -4
- fin_infra/security/add.py +1 -2
- fin_infra/security/audit.py +6 -7
- fin_infra/security/pii_filter.py +10 -10
- fin_infra/security/token_store.py +2 -3
- fin_infra/tax/add.py +2 -2
- fin_infra/tax/tlh.py +5 -5
- fin_infra/utils/__init__.py +15 -1
- fin_infra/utils/deprecation.py +161 -0
- {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/METADATA +17 -9
- {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/RECORD +65 -64
- {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/entry_points.txt +0 -0
fin_infra/analytics/__init__.py
CHANGED
|
@@ -8,8 +8,8 @@ This module provides comprehensive financial analytics capabilities including:
|
|
|
8
8
|
- Growth projections (net worth forecasting with scenarios)
|
|
9
9
|
|
|
10
10
|
Feature Status:
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
[OK] STABLE: Core calculation functions (all analytics work with provided data)
|
|
12
|
+
[!] INTEGRATION: Auto-fetching from providers requires setup:
|
|
13
13
|
- Banking provider for transaction data
|
|
14
14
|
- Brokerage provider for investment data
|
|
15
15
|
- Categorization for expense categorization
|
fin_infra/analytics/add.py
CHANGED
|
@@ -239,9 +239,9 @@ def add_analytics(
|
|
|
239
239
|
|
|
240
240
|
Note:
|
|
241
241
|
Real holdings provide:
|
|
242
|
-
- Accurate cost basis
|
|
243
|
-
- Security types
|
|
244
|
-
- Current values
|
|
242
|
+
- Accurate cost basis -> real profit/loss
|
|
243
|
+
- Security types -> precise asset allocation
|
|
244
|
+
- Current values -> live portfolio tracking
|
|
245
245
|
"""
|
|
246
246
|
# If with_holdings requested and investment provider available
|
|
247
247
|
if with_holdings:
|
fin_infra/analytics/cash_flow.py
CHANGED
|
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
9
|
from decimal import Decimal
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
from ..models import Transaction
|
|
13
13
|
from .models import CashFlowAnalysis
|
|
@@ -17,7 +17,7 @@ async def calculate_cash_flow(
|
|
|
17
17
|
user_id: str,
|
|
18
18
|
start_date: str | datetime,
|
|
19
19
|
end_date: str | datetime,
|
|
20
|
-
accounts:
|
|
20
|
+
accounts: list[str] | None = None,
|
|
21
21
|
*,
|
|
22
22
|
banking_provider=None,
|
|
23
23
|
categorization_provider=None,
|
|
@@ -117,7 +117,7 @@ async def calculate_cash_flow(
|
|
|
117
117
|
async def forecast_cash_flow(
|
|
118
118
|
user_id: str,
|
|
119
119
|
months: int = 6,
|
|
120
|
-
assumptions:
|
|
120
|
+
assumptions: dict[str, Any] | None = None,
|
|
121
121
|
*,
|
|
122
122
|
recurring_provider=None,
|
|
123
123
|
) -> list[CashFlowAnalysis]:
|
fin_infra/analytics/models.py
CHANGED
|
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from enum import Enum
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
from pydantic import BaseModel, ConfigDict, Field
|
|
13
13
|
|
|
@@ -72,7 +72,7 @@ class SavingsRateData(BaseModel):
|
|
|
72
72
|
expenses: float = Field(..., description="Total expenses for period")
|
|
73
73
|
period: Period = Field(..., description="Period type")
|
|
74
74
|
definition: SavingsDefinition = Field(..., description="Calculation method used")
|
|
75
|
-
trend:
|
|
75
|
+
trend: TrendDirection | None = Field(None, description="Trend over time")
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
class SpendingAnomaly(BaseModel):
|
|
@@ -132,7 +132,7 @@ class PersonalizedSpendingAdvice(BaseModel):
|
|
|
132
132
|
alerts: list[str] = Field(
|
|
133
133
|
default_factory=list, description="Urgent spending issues requiring attention"
|
|
134
134
|
)
|
|
135
|
-
estimated_monthly_savings:
|
|
135
|
+
estimated_monthly_savings: float | None = Field(
|
|
136
136
|
None, description="Potential monthly savings if recommendations followed"
|
|
137
137
|
)
|
|
138
138
|
|
|
@@ -183,7 +183,7 @@ class BenchmarkComparison(BaseModel):
|
|
|
183
183
|
benchmark_return_percent: float = Field(..., description="Benchmark return percentage")
|
|
184
184
|
benchmark_symbol: str = Field(..., description="Benchmark ticker (e.g., SPY)")
|
|
185
185
|
alpha: float = Field(..., description="Portfolio alpha (excess return)")
|
|
186
|
-
beta:
|
|
186
|
+
beta: float | None = Field(None, description="Portfolio beta (volatility vs benchmark)")
|
|
187
187
|
period: str = Field(..., description="Comparison period (1y, 3y, 5y, etc.)")
|
|
188
188
|
|
|
189
189
|
|
|
@@ -213,6 +213,6 @@ class GrowthProjection(BaseModel):
|
|
|
213
213
|
assumptions: dict[str, Any] = Field(
|
|
214
214
|
default_factory=dict, description="Assumptions used (inflation, returns, etc.)"
|
|
215
215
|
)
|
|
216
|
-
confidence_intervals:
|
|
216
|
+
confidence_intervals: dict[str, tuple[float, float]] | None = Field(
|
|
217
217
|
None, description="95% confidence intervals by scenario"
|
|
218
218
|
)
|
fin_infra/analytics/portfolio.py
CHANGED
|
@@ -527,10 +527,10 @@ def portfolio_metrics_with_holdings(holdings: list) -> PortfolioMetrics:
|
|
|
527
527
|
PortfolioMetrics with real portfolio analysis
|
|
528
528
|
|
|
529
529
|
Real Data Advantages:
|
|
530
|
-
- Actual cost basis
|
|
531
|
-
- Real security types
|
|
532
|
-
- Current market values
|
|
533
|
-
- No mock data
|
|
530
|
+
- Actual cost basis -> accurate P/L calculations
|
|
531
|
+
- Real security types -> precise asset allocation
|
|
532
|
+
- Current market values -> live portfolio value
|
|
533
|
+
- No mock data -> production-ready analytics
|
|
534
534
|
|
|
535
535
|
Limitations:
|
|
536
536
|
- Day/YTD/MTD returns require historical snapshots (not in holdings)
|
|
@@ -652,7 +652,7 @@ def calculate_day_change_with_snapshot(
|
|
|
652
652
|
Function matches holdings by account_id + security_id for accurate tracking
|
|
653
653
|
of individual position changes (accounts for buys/sells, not just price moves).
|
|
654
654
|
"""
|
|
655
|
-
# Build lookup map for previous snapshot: (account_id, security_id)
|
|
655
|
+
# Build lookup map for previous snapshot: (account_id, security_id) -> value
|
|
656
656
|
previous_map = {}
|
|
657
657
|
for holding in previous_snapshot:
|
|
658
658
|
key = (holding.account_id, holding.security.security_id)
|
|
@@ -703,13 +703,13 @@ def _calculate_allocation_from_holdings(
|
|
|
703
703
|
list[AssetAllocation] with asset_class, value, and percentage
|
|
704
704
|
|
|
705
705
|
Asset Class Mapping:
|
|
706
|
-
- equity
|
|
707
|
-
- etf
|
|
708
|
-
- mutual_fund
|
|
709
|
-
- bond
|
|
710
|
-
- cash
|
|
711
|
-
- derivative
|
|
712
|
-
- other
|
|
706
|
+
- equity -> Stocks
|
|
707
|
+
- etf -> Stocks (equity ETFs grouped with stocks)
|
|
708
|
+
- mutual_fund -> Bonds (conservative assumption)
|
|
709
|
+
- bond -> Bonds
|
|
710
|
+
- cash -> Cash
|
|
711
|
+
- derivative -> Other
|
|
712
|
+
- other -> Other
|
|
713
713
|
"""
|
|
714
714
|
from collections import defaultdict
|
|
715
715
|
|
fin_infra/analytics/spending.py
CHANGED
|
@@ -45,7 +45,6 @@ Examples:
|
|
|
45
45
|
from collections import defaultdict
|
|
46
46
|
from datetime import timedelta
|
|
47
47
|
from decimal import Decimal
|
|
48
|
-
from typing import Optional
|
|
49
48
|
|
|
50
49
|
from fin_infra.analytics.models import (
|
|
51
50
|
PersonalizedSpendingAdvice,
|
|
@@ -60,7 +59,7 @@ async def analyze_spending(
|
|
|
60
59
|
user_id: str,
|
|
61
60
|
*,
|
|
62
61
|
period: str = "30d",
|
|
63
|
-
categories:
|
|
62
|
+
categories: list[str] | None = None,
|
|
64
63
|
banking_provider=None,
|
|
65
64
|
categorization_provider=None,
|
|
66
65
|
) -> SpendingInsight:
|
|
@@ -426,7 +425,7 @@ def _generate_mock_transactions(days: int) -> list[Transaction]:
|
|
|
426
425
|
async def generate_spending_insights(
|
|
427
426
|
spending_insight: SpendingInsight,
|
|
428
427
|
*,
|
|
429
|
-
user_context:
|
|
428
|
+
user_context: dict | None = None,
|
|
430
429
|
llm_provider=None,
|
|
431
430
|
) -> "PersonalizedSpendingAdvice":
|
|
432
431
|
"""Generate personalized spending insights using LLM.
|
|
@@ -521,7 +520,7 @@ async def generate_spending_insights(
|
|
|
521
520
|
|
|
522
521
|
def _build_spending_insights_prompt(
|
|
523
522
|
spending_insight: SpendingInsight,
|
|
524
|
-
user_context:
|
|
523
|
+
user_context: dict | None = None,
|
|
525
524
|
) -> str:
|
|
526
525
|
"""Build LLM prompt with financial context.
|
|
527
526
|
|
|
@@ -621,7 +620,7 @@ Be specific, encouraging, and actionable. Focus on realistic savings, not extrem
|
|
|
621
620
|
|
|
622
621
|
def _generate_rule_based_insights(
|
|
623
622
|
spending_insight: SpendingInsight,
|
|
624
|
-
user_context:
|
|
623
|
+
user_context: dict | None = None,
|
|
625
624
|
) -> "PersonalizedSpendingAdvice":
|
|
626
625
|
"""Generate rule-based insights when LLM is unavailable.
|
|
627
626
|
|
fin_infra/banking/history.py
CHANGED
|
@@ -4,7 +4,7 @@ This module provides functionality to record and retrieve historical account bal
|
|
|
4
4
|
snapshots over time. This enables balance trend analysis, sparklines, and time-series
|
|
5
5
|
visualizations in fintech dashboards.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[!] WARNING: This module uses IN-MEMORY storage by default. All data is LOST on restart.
|
|
8
8
|
For production use, integrate with svc-infra SQL database or set FIN_INFRA_STORAGE_BACKEND.
|
|
9
9
|
|
|
10
10
|
Features:
|
|
@@ -57,7 +57,7 @@ __all__ = [
|
|
|
57
57
|
_logger = logging.getLogger(__name__)
|
|
58
58
|
|
|
59
59
|
# In-memory storage for testing (will be replaced with SQL database in production)
|
|
60
|
-
#
|
|
60
|
+
# [!] WARNING: All data is LOST on restart when using in-memory storage!
|
|
61
61
|
_balance_snapshots: list[BalanceSnapshot] = []
|
|
62
62
|
_production_warning_logged = False
|
|
63
63
|
|
|
@@ -73,7 +73,7 @@ def _check_in_memory_warning() -> None:
|
|
|
73
73
|
|
|
74
74
|
if env in ("production", "staging") and storage_backend == "memory":
|
|
75
75
|
_logger.warning(
|
|
76
|
-
"
|
|
76
|
+
"[!] CRITICAL: Balance history using IN-MEMORY storage in %s environment! "
|
|
77
77
|
"All balance snapshots will be LOST on restart. "
|
|
78
78
|
"Set FIN_INFRA_STORAGE_BACKEND=sql for production persistence.",
|
|
79
79
|
env,
|
|
@@ -114,7 +114,7 @@ def record_balance_snapshot(
|
|
|
114
114
|
This function stores a point-in-time balance record for trend analysis.
|
|
115
115
|
In production, this would write to a SQL database via svc-infra.
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
[!] WARNING: Uses in-memory storage by default. Data is LOST on restart!
|
|
118
118
|
|
|
119
119
|
Args:
|
|
120
120
|
account_id: Account identifier
|
fin_infra/brokerage/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Brokerage module - easy setup for trading operations.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[!] **TRADING WARNING**: This module provides real trading capabilities.
|
|
4
4
|
Always use paper trading mode for development and testing.
|
|
5
5
|
Live trading requires explicit opt-in and involves real financial risk.
|
|
6
6
|
|
|
@@ -51,11 +51,11 @@ def easy_brokerage(
|
|
|
51
51
|
) -> BrokerageProvider:
|
|
52
52
|
"""Create a brokerage provider with paper/live trading support.
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
[!] **SAFETY**: Defaults to paper trading mode. Live trading requires explicit mode="live".
|
|
55
55
|
|
|
56
56
|
Auto-detects provider based on environment variables:
|
|
57
|
-
1. If ALPACA_API_KEY and ALPACA_API_SECRET are set
|
|
58
|
-
2. Otherwise
|
|
57
|
+
1. If ALPACA_API_KEY and ALPACA_API_SECRET are set -> Alpaca
|
|
58
|
+
2. Otherwise -> Raises error (credentials required)
|
|
59
59
|
|
|
60
60
|
Args:
|
|
61
61
|
provider: Provider name ("alpaca"). If None, defaults to alpaca.
|
|
@@ -138,7 +138,7 @@ def add_brokerage(
|
|
|
138
138
|
) -> BrokerageProvider:
|
|
139
139
|
"""Wire brokerage provider to FastAPI app with routes and safety checks.
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
[!] **TRADING WARNING**: This mounts trading API endpoints.
|
|
142
142
|
Always use paper trading mode for development.
|
|
143
143
|
Live trading requires explicit mode="live" and proper safeguards.
|
|
144
144
|
|
|
@@ -280,7 +280,7 @@ def add_brokerage(
|
|
|
280
280
|
async def submit_order(order_request: OrderRequest):
|
|
281
281
|
"""Submit a new order.
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
[!] **TRADING WARNING**: This endpoint executes real trades in live mode.
|
|
284
284
|
"""
|
|
285
285
|
try:
|
|
286
286
|
order = brokerage_provider.submit_order(
|
|
@@ -447,7 +447,7 @@ def add_brokerage(
|
|
|
447
447
|
add_prefixed_docs(
|
|
448
448
|
app,
|
|
449
449
|
prefix=prefix,
|
|
450
|
-
title="Brokerage" + (" (Paper Trading)" if mode == "paper" else "
|
|
450
|
+
title="Brokerage" + (" (Paper Trading)" if mode == "paper" else " [!] LIVE"),
|
|
451
451
|
auto_exclude_from_root=True,
|
|
452
452
|
visible_envs=None, # Show in all environments
|
|
453
453
|
)
|
fin_infra/budgets/alerts.py
CHANGED
|
@@ -35,7 +35,7 @@ Example:
|
|
|
35
35
|
from __future__ import annotations
|
|
36
36
|
|
|
37
37
|
from datetime import datetime
|
|
38
|
-
from typing import TYPE_CHECKING
|
|
38
|
+
from typing import TYPE_CHECKING
|
|
39
39
|
|
|
40
40
|
from fin_infra.budgets.models import (
|
|
41
41
|
AlertSeverity,
|
|
@@ -51,7 +51,7 @@ if TYPE_CHECKING:
|
|
|
51
51
|
async def check_budget_alerts(
|
|
52
52
|
budget_id: str,
|
|
53
53
|
tracker: BudgetTracker,
|
|
54
|
-
thresholds:
|
|
54
|
+
thresholds: dict[str, float] | None = None,
|
|
55
55
|
) -> list[BudgetAlert]:
|
|
56
56
|
"""
|
|
57
57
|
Check budget for alerts (overspending, approaching limits, unusual patterns).
|
fin_infra/budgets/ease.py
CHANGED
|
@@ -12,7 +12,6 @@ Generic Design:
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import os
|
|
15
|
-
from typing import Optional
|
|
16
15
|
|
|
17
16
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
18
17
|
|
|
@@ -20,7 +19,7 @@ from fin_infra.budgets.tracker import BudgetTracker
|
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
def easy_budgets(
|
|
23
|
-
db_url:
|
|
22
|
+
db_url: str | None = None,
|
|
24
23
|
pool_size: int = 5,
|
|
25
24
|
max_overflow: int = 10,
|
|
26
25
|
pool_pre_ping: bool = True,
|
fin_infra/budgets/models.py
CHANGED
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from enum import Enum
|
|
11
|
-
from typing import Optional
|
|
12
11
|
|
|
13
12
|
from pydantic import BaseModel, ConfigDict, Field
|
|
14
13
|
|
|
@@ -296,7 +295,7 @@ class BudgetAlert(BaseModel):
|
|
|
296
295
|
"""
|
|
297
296
|
|
|
298
297
|
budget_id: str = Field(..., description="Budget identifier")
|
|
299
|
-
category:
|
|
298
|
+
category: str | None = Field(None, description="Category triggering alert")
|
|
300
299
|
alert_type: AlertType = Field(..., description="Type of alert")
|
|
301
300
|
threshold: float = Field(..., description="Threshold that triggered alert")
|
|
302
301
|
message: str = Field(..., description="Human-readable alert message")
|
fin_infra/budgets/templates.py
CHANGED
|
@@ -16,7 +16,7 @@ Generic Design:
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
from datetime import datetime
|
|
19
|
-
from typing import TYPE_CHECKING
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
20
|
|
|
21
21
|
from fin_infra.budgets.models import Budget, BudgetPeriod, BudgetType
|
|
22
22
|
|
|
@@ -177,9 +177,9 @@ async def apply_template(
|
|
|
177
177
|
template_name: str,
|
|
178
178
|
total_income: float,
|
|
179
179
|
tracker: BudgetTracker,
|
|
180
|
-
budget_name:
|
|
181
|
-
start_date:
|
|
182
|
-
custom_template:
|
|
180
|
+
budget_name: str | None = None,
|
|
181
|
+
start_date: datetime | None = None,
|
|
182
|
+
custom_template: BudgetTemplate | None = None,
|
|
183
183
|
) -> Budget:
|
|
184
184
|
"""Apply a budget template to create a new budget.
|
|
185
185
|
|
fin_infra/budgets/tracker.py
CHANGED
|
@@ -36,7 +36,7 @@ from __future__ import annotations
|
|
|
36
36
|
|
|
37
37
|
import uuid
|
|
38
38
|
from datetime import datetime, timedelta
|
|
39
|
-
from typing import TYPE_CHECKING
|
|
39
|
+
from typing import TYPE_CHECKING
|
|
40
40
|
|
|
41
41
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
42
42
|
|
|
@@ -116,7 +116,7 @@ class BudgetTracker:
|
|
|
116
116
|
type: str, # BudgetType value
|
|
117
117
|
period: str, # BudgetPeriod value
|
|
118
118
|
categories: dict[str, float],
|
|
119
|
-
start_date:
|
|
119
|
+
start_date: datetime | None = None,
|
|
120
120
|
rollover_enabled: bool = False,
|
|
121
121
|
) -> Budget:
|
|
122
122
|
"""
|
|
@@ -205,7 +205,7 @@ class BudgetTracker:
|
|
|
205
205
|
async def get_budgets(
|
|
206
206
|
self,
|
|
207
207
|
user_id: str,
|
|
208
|
-
type:
|
|
208
|
+
type: str | None = None,
|
|
209
209
|
) -> list[Budget]:
|
|
210
210
|
"""
|
|
211
211
|
Get all budgets for a user.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Transaction categorization module.
|
|
3
3
|
|
|
4
4
|
Provides ML-based categorization of merchant transactions into 56 categories
|
|
5
|
-
using a hybrid approach (exact match
|
|
5
|
+
using a hybrid approach (exact match -> regex -> sklearn Naive Bayes -> LLM).
|
|
6
6
|
|
|
7
7
|
Basic usage:
|
|
8
8
|
from fin_infra.categorization import categorize
|
fin_infra/categorization/ease.py
CHANGED
|
@@ -5,7 +5,7 @@ Provides one-line setup with sensible defaults.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Literal
|
|
8
|
+
from typing import Literal
|
|
9
9
|
|
|
10
10
|
from .engine import CategorizationEngine
|
|
11
11
|
|
|
@@ -21,10 +21,10 @@ def easy_categorization(
|
|
|
21
21
|
taxonomy: str = "mx",
|
|
22
22
|
enable_ml: bool = False,
|
|
23
23
|
confidence_threshold: float = 0.6,
|
|
24
|
-
model_path:
|
|
24
|
+
model_path: Path | None = None,
|
|
25
25
|
# LLM-specific parameters (V2)
|
|
26
26
|
llm_provider: Literal["google", "openai", "anthropic", "none"] = "google",
|
|
27
|
-
llm_model:
|
|
27
|
+
llm_model: str | None = None,
|
|
28
28
|
llm_confidence_threshold: float = 0.6,
|
|
29
29
|
llm_cache_ttl: int = 86400, # 24 hours
|
|
30
30
|
llm_max_cost_per_day: float = 0.10, # $0.10/day
|
|
@@ -197,7 +197,7 @@ class LLMCategorizer:
|
|
|
197
197
|
self._track_cost()
|
|
198
198
|
|
|
199
199
|
logger.info(
|
|
200
|
-
f"LLM categorized '{merchant_name}'
|
|
200
|
+
f"LLM categorized '{merchant_name}' -> {prediction.category} "
|
|
201
201
|
f"(confidence={prediction.confidence:.2f})"
|
|
202
202
|
)
|
|
203
203
|
|
|
@@ -252,7 +252,7 @@ class LLMCategorizer:
|
|
|
252
252
|
# Format few-shot examples
|
|
253
253
|
examples_text = "\n\n".join(
|
|
254
254
|
[
|
|
255
|
-
f'Merchant: "{merchant}"\n
|
|
255
|
+
f'Merchant: "{merchant}"\n-> Category: "{category}"\n-> Reasoning: "{reasoning}"'
|
|
256
256
|
for merchant, category, reasoning in FEW_SHOT_EXAMPLES
|
|
257
257
|
]
|
|
258
258
|
)
|
|
@@ -300,6 +300,8 @@ Return JSON with category, confidence, and reasoning."""
|
|
|
300
300
|
def _get_cache_key(self, merchant_name: str) -> str:
|
|
301
301
|
"""Generate stable cache key from merchant name."""
|
|
302
302
|
normalized = merchant_name.lower().strip()
|
|
303
|
+
# Security: B324 skip justified - MD5 used for cache key generation only,
|
|
304
|
+
# not for security. We need deterministic hashing for cache lookups.
|
|
303
305
|
hash_value = hashlib.md5(normalized.encode()).hexdigest()
|
|
304
306
|
return f"llm_category:{hash_value}"
|
|
305
307
|
|
|
@@ -338,10 +340,10 @@ Return JSON with category, confidence, and reasoning."""
|
|
|
338
340
|
|
|
339
341
|
def reset_daily_cost(self):
|
|
340
342
|
"""Reset daily cost counter (called at midnight UTC)."""
|
|
341
|
-
logger.info(f"Resetting daily cost: ${self.daily_cost:.5f}
|
|
343
|
+
logger.info(f"Resetting daily cost: ${self.daily_cost:.5f} -> $0.00")
|
|
342
344
|
self.daily_cost = 0.0
|
|
343
345
|
|
|
344
346
|
def reset_monthly_cost(self):
|
|
345
347
|
"""Reset monthly cost counter (called on 1st of month)."""
|
|
346
|
-
logger.info(f"Resetting monthly cost: ${self.monthly_cost:.5f}
|
|
348
|
+
logger.info(f"Resetting monthly cost: ${self.monthly_cost:.5f} -> $0.00")
|
|
347
349
|
self.monthly_cost = 0.0
|
|
@@ -4,7 +4,6 @@ Pydantic models for transaction categorization.
|
|
|
4
4
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Optional
|
|
8
7
|
|
|
9
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
10
9
|
|
|
@@ -34,7 +33,7 @@ class CategoryPrediction(BaseModel):
|
|
|
34
33
|
default_factory=list,
|
|
35
34
|
description="Alternative predictions (category, confidence)",
|
|
36
35
|
)
|
|
37
|
-
reasoning:
|
|
36
|
+
reasoning: str | None = Field(None, description="Explanation of prediction (for LLM)")
|
|
38
37
|
|
|
39
38
|
model_config = ConfigDict(
|
|
40
39
|
json_schema_extra={
|
|
@@ -100,7 +99,7 @@ class CategorizationRequest(BaseModel):
|
|
|
100
99
|
"""Request to categorize a merchant."""
|
|
101
100
|
|
|
102
101
|
merchant_name: str = Field(..., description="Merchant name to categorize")
|
|
103
|
-
user_id:
|
|
102
|
+
user_id: str | None = Field(None, description="User ID for personalized overrides")
|
|
104
103
|
include_alternatives: bool = Field(default=False, description="Include alternative predictions")
|
|
105
104
|
min_confidence: float = Field(
|
|
106
105
|
default=0.0,
|
|
@@ -152,7 +151,7 @@ class CategoryStats(BaseModel):
|
|
|
152
151
|
total_categories: int = Field(..., description="Total number of categories")
|
|
153
152
|
categories_by_group: dict[str, int] = Field(..., description="Category counts by group")
|
|
154
153
|
total_rules: int = Field(..., description="Total number of rules")
|
|
155
|
-
cache_hit_rate:
|
|
154
|
+
cache_hit_rate: float | None = Field(None, description="Cache hit rate (0-1)")
|
|
156
155
|
|
|
157
156
|
model_config = ConfigDict(
|
|
158
157
|
json_schema_extra={
|
fin_infra/chat/planning.py
CHANGED
|
@@ -173,7 +173,7 @@ Answer: "To assess your retirement progress, I need more information: (1) What's
|
|
|
173
173
|
Follow-ups: ["I want to retire at 65 with $1.5M", "How much should I save monthly?", "What's a realistic retirement goal?"]
|
|
174
174
|
Sources: []
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
[!] This is AI-generated advice. Not a substitute for a certified financial advisor.
|
|
177
177
|
Verify calculations independently. For personalized advice, consult a professional."""
|
|
178
178
|
|
|
179
179
|
|
|
@@ -102,7 +102,7 @@ def cmd_scaffold(
|
|
|
102
102
|
# Validate required parameters
|
|
103
103
|
if dest_dir is None:
|
|
104
104
|
typer.secho(
|
|
105
|
-
"
|
|
105
|
+
"[X] Error: --dest-dir is required",
|
|
106
106
|
fg=typer.colors.RED,
|
|
107
107
|
err=True,
|
|
108
108
|
)
|
|
@@ -137,7 +137,7 @@ def cmd_scaffold(
|
|
|
137
137
|
)
|
|
138
138
|
else:
|
|
139
139
|
typer.secho(
|
|
140
|
-
f"
|
|
140
|
+
f"[X] Unknown domain: {domain}. Must be one of: budgets, goals",
|
|
141
141
|
fg=typer.colors.RED,
|
|
142
142
|
err=True,
|
|
143
143
|
)
|
|
@@ -145,7 +145,7 @@ def cmd_scaffold(
|
|
|
145
145
|
|
|
146
146
|
# Display results
|
|
147
147
|
typer.echo("")
|
|
148
|
-
typer.secho("
|
|
148
|
+
typer.secho(" Scaffold Results:", bold=True)
|
|
149
149
|
typer.echo("")
|
|
150
150
|
|
|
151
151
|
files = result.get("files", [])
|
|
@@ -157,7 +157,7 @@ def cmd_scaffold(
|
|
|
157
157
|
action = file_info.get("action", "unknown")
|
|
158
158
|
|
|
159
159
|
if action == "wrote":
|
|
160
|
-
typer.secho(f"
|
|
160
|
+
typer.secho(f" [OK] Created: {path}", fg=typer.colors.GREEN)
|
|
161
161
|
wrote_count += 1
|
|
162
162
|
elif action == "skipped":
|
|
163
163
|
reason = file_info.get("reason", "unknown")
|
|
@@ -168,7 +168,7 @@ def cmd_scaffold(
|
|
|
168
168
|
|
|
169
169
|
# Summary
|
|
170
170
|
typer.echo("")
|
|
171
|
-
typer.secho(f"
|
|
171
|
+
typer.secho(f" Done! Created {wrote_count} file(s), skipped {skipped_count}.", bold=True)
|
|
172
172
|
typer.echo("")
|
|
173
173
|
|
|
174
174
|
# Next steps
|
|
@@ -187,7 +187,7 @@ def cmd_scaffold(
|
|
|
187
187
|
}
|
|
188
188
|
route_prefix = prefix_map.get(domain, f"/{domain}")
|
|
189
189
|
|
|
190
|
-
typer.secho("
|
|
190
|
+
typer.secho(" Next Steps:", bold=True)
|
|
191
191
|
typer.echo("")
|
|
192
192
|
typer.echo(" 1. Review generated files and customize as needed")
|
|
193
193
|
typer.echo(" 2. Run migrations:")
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Response parsers for Experian API data to fin_infra models.
|
|
2
2
|
|
|
3
3
|
Converts Experian API JSON responses to typed Pydantic models:
|
|
4
|
-
- parse_credit_score(): dict
|
|
5
|
-
- parse_credit_report(): dict
|
|
6
|
-
- parse_account(): dict
|
|
7
|
-
- parse_inquiry(): dict
|
|
8
|
-
- parse_public_record(): dict
|
|
4
|
+
- parse_credit_score(): dict -> CreditScore
|
|
5
|
+
- parse_credit_report(): dict -> CreditReport
|
|
6
|
+
- parse_account(): dict -> CreditAccount
|
|
7
|
+
- parse_inquiry(): dict -> CreditInquiry
|
|
8
|
+
- parse_public_record(): dict -> PublicRecord
|
|
9
9
|
|
|
10
10
|
Example:
|
|
11
11
|
>>> data = await client.get_credit_score("user123")
|
fin_infra/crypto/__init__.py
CHANGED
|
@@ -29,8 +29,8 @@ def easy_crypto(
|
|
|
29
29
|
"""Create a crypto data provider with zero or minimal configuration.
|
|
30
30
|
|
|
31
31
|
Auto-detects provider based on environment variables:
|
|
32
|
-
1. If COINGECKO_API_KEY is set
|
|
33
|
-
2. Otherwise
|
|
32
|
+
1. If COINGECKO_API_KEY is set -> CoinGecko Pro
|
|
33
|
+
2. Otherwise -> CoinGecko Free (no key needed)
|
|
34
34
|
|
|
35
35
|
Args:
|
|
36
36
|
provider: Provider name ("coingecko"). If None, defaults to coingecko.
|
fin_infra/documents/models.py
CHANGED
|
@@ -31,7 +31,6 @@ from __future__ import annotations
|
|
|
31
31
|
|
|
32
32
|
from datetime import datetime
|
|
33
33
|
from enum import Enum
|
|
34
|
-
from typing import Optional
|
|
35
34
|
|
|
36
35
|
from pydantic import BaseModel, ConfigDict, Field
|
|
37
36
|
from svc_infra.documents import Document as BaseDocument
|
|
@@ -111,8 +110,8 @@ class FinancialDocument(BaseDocument):
|
|
|
111
110
|
|
|
112
111
|
# Financial-specific fields
|
|
113
112
|
type: DocumentType = Field(..., description="Document type category (financial-specific)")
|
|
114
|
-
tax_year:
|
|
115
|
-
form_type:
|
|
113
|
+
tax_year: int | None = Field(None, description="Tax year for tax documents (e.g., 2024)")
|
|
114
|
+
form_type: str | None = Field(None, description="Tax form type (W-2, 1099-INT, 1040, etc.)")
|
|
116
115
|
|
|
117
116
|
|
|
118
117
|
# Backward compatibility: alias for existing code
|
fin_infra/goals/management.py
CHANGED
|
@@ -182,7 +182,7 @@ Your response: {
|
|
|
182
182
|
"confidence": 0.94
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
[!] This is AI-generated advice. Not a substitute for a certified financial advisor.
|
|
186
186
|
Verify calculations independently. For personalized advice, consult a professional."""
|
|
187
187
|
|
|
188
188
|
GOAL_PROGRESS_SYSTEM_PROMPT = """You are a financial advisor reviewing goal progress.
|
|
@@ -237,7 +237,7 @@ Your response: {
|
|
|
237
237
|
"projected_completion_date": "2029-06-01",
|
|
238
238
|
"variance_from_target_days": -365,
|
|
239
239
|
"course_corrections": [
|
|
240
|
-
"
|
|
240
|
+
"[!] 12 months behind! Current $1,000/month payment needs to increase to $1,500/month",
|
|
241
241
|
"Emergency: reduce expenses by $500/month (cancel subscriptions, cut entertainment)",
|
|
242
242
|
"Contact debt counselor for consolidation or negotiation options",
|
|
243
243
|
"Consider side income: gig work, selling unused items ($500/month target)",
|
|
@@ -246,7 +246,7 @@ Your response: {
|
|
|
246
246
|
"confidence": 0.95
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
[!] This is AI-generated advice. Not a substitute for a certified financial advisor.
|
|
250
250
|
Verify calculations independently. For personalized advice, consult a professional."""
|
|
251
251
|
|
|
252
252
|
|