fin-infra 0.1.62__py3-none-any.whl → 0.1.82__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.
Files changed (126) hide show
  1. fin_infra/__init__.py +53 -3
  2. fin_infra/analytics/__init__.py +13 -2
  3. fin_infra/analytics/add.py +30 -32
  4. fin_infra/analytics/cash_flow.py +6 -5
  5. fin_infra/analytics/ease.py +19 -20
  6. fin_infra/analytics/portfolio.py +19 -26
  7. fin_infra/analytics/projections.py +1 -3
  8. fin_infra/analytics/rebalancing.py +2 -4
  9. fin_infra/analytics/savings.py +1 -1
  10. fin_infra/analytics/spending.py +15 -11
  11. fin_infra/banking/__init__.py +33 -31
  12. fin_infra/banking/history.py +11 -12
  13. fin_infra/banking/utils.py +116 -110
  14. fin_infra/brokerage/__init__.py +27 -27
  15. fin_infra/budgets/__init__.py +3 -3
  16. fin_infra/budgets/add.py +16 -17
  17. fin_infra/budgets/alerts.py +3 -3
  18. fin_infra/budgets/tracker.py +4 -5
  19. fin_infra/cashflows/__init__.py +8 -10
  20. fin_infra/cashflows/core.py +1 -1
  21. fin_infra/categorization/__init__.py +1 -1
  22. fin_infra/categorization/add.py +17 -19
  23. fin_infra/categorization/ease.py +3 -4
  24. fin_infra/categorization/engine.py +21 -18
  25. fin_infra/categorization/llm_layer.py +10 -10
  26. fin_infra/categorization/models.py +1 -1
  27. fin_infra/categorization/rules.py +2 -4
  28. fin_infra/categorization/taxonomy.py +2 -2
  29. fin_infra/chat/__init__.py +13 -22
  30. fin_infra/chat/planning.py +57 -1
  31. fin_infra/cli/cmds/scaffold_cmds.py +11 -12
  32. fin_infra/clients/__init__.py +23 -1
  33. fin_infra/clients/base.py +1 -1
  34. fin_infra/clients/plaid.py +2 -2
  35. fin_infra/compliance/__init__.py +7 -6
  36. fin_infra/credit/add.py +7 -7
  37. fin_infra/credit/experian/auth.py +3 -2
  38. fin_infra/credit/experian/client.py +2 -2
  39. fin_infra/credit/experian/provider.py +19 -19
  40. fin_infra/crypto/__init__.py +8 -10
  41. fin_infra/crypto/insights.py +5 -6
  42. fin_infra/documents/add.py +11 -13
  43. fin_infra/documents/analysis.py +9 -9
  44. fin_infra/documents/ease.py +18 -17
  45. fin_infra/documents/models.py +7 -7
  46. fin_infra/documents/ocr.py +8 -8
  47. fin_infra/documents/storage.py +23 -14
  48. fin_infra/exceptions.py +1 -2
  49. fin_infra/goals/__init__.py +8 -8
  50. fin_infra/goals/add.py +36 -36
  51. fin_infra/goals/funding.py +4 -6
  52. fin_infra/goals/management.py +6 -7
  53. fin_infra/goals/milestones.py +2 -3
  54. fin_infra/goals/models.py +7 -11
  55. fin_infra/insights/__init__.py +12 -10
  56. fin_infra/insights/aggregator.py +1 -1
  57. fin_infra/investments/__init__.py +14 -9
  58. fin_infra/investments/add.py +53 -73
  59. fin_infra/investments/ease.py +16 -13
  60. fin_infra/investments/models.py +135 -69
  61. fin_infra/investments/providers/base.py +9 -15
  62. fin_infra/investments/providers/plaid.py +70 -55
  63. fin_infra/investments/providers/snaptrade.py +35 -53
  64. fin_infra/markets/__init__.py +16 -11
  65. fin_infra/models/__init__.py +10 -10
  66. fin_infra/models/accounts.py +2 -1
  67. fin_infra/models/brokerage.py +2 -1
  68. fin_infra/models/candle.py +1 -0
  69. fin_infra/models/money.py +1 -0
  70. fin_infra/models/quotes.py +4 -3
  71. fin_infra/models/tax.py +2 -1
  72. fin_infra/models/transactions.py +4 -4
  73. fin_infra/net_worth/__init__.py +7 -0
  74. fin_infra/net_worth/add.py +8 -5
  75. fin_infra/net_worth/aggregator.py +9 -6
  76. fin_infra/net_worth/calculator.py +8 -6
  77. fin_infra/net_worth/ease.py +36 -15
  78. fin_infra/net_worth/insights.py +4 -5
  79. fin_infra/net_worth/models.py +237 -116
  80. fin_infra/normalization/__init__.py +17 -15
  81. fin_infra/normalization/providers/exchangerate.py +5 -5
  82. fin_infra/obs/classifier.py +3 -3
  83. fin_infra/providers/banking/plaid_client.py +23 -22
  84. fin_infra/providers/banking/teller_client.py +14 -7
  85. fin_infra/providers/base.py +131 -14
  86. fin_infra/providers/brokerage/alpaca.py +7 -7
  87. fin_infra/providers/credit/experian.py +5 -0
  88. fin_infra/providers/market/alphavantage.py +6 -11
  89. fin_infra/providers/market/ccxt_crypto.py +25 -4
  90. fin_infra/providers/market/coingecko.py +5 -6
  91. fin_infra/providers/market/yahoo.py +23 -8
  92. fin_infra/providers/tax/__init__.py +1 -1
  93. fin_infra/providers/tax/irs.py +1 -1
  94. fin_infra/providers/tax/mock.py +8 -8
  95. fin_infra/providers/tax/taxbit.py +1 -1
  96. fin_infra/recurring/__init__.py +6 -6
  97. fin_infra/recurring/add.py +24 -12
  98. fin_infra/recurring/detector.py +8 -8
  99. fin_infra/recurring/detectors_llm.py +14 -13
  100. fin_infra/recurring/ease.py +3 -5
  101. fin_infra/recurring/insights.py +20 -19
  102. fin_infra/recurring/models.py +3 -3
  103. fin_infra/recurring/normalizer.py +3 -2
  104. fin_infra/recurring/normalizers.py +11 -10
  105. fin_infra/recurring/summary.py +13 -15
  106. fin_infra/scaffold/__init__.py +1 -1
  107. fin_infra/scaffold/budgets.py +9 -9
  108. fin_infra/scaffold/goals.py +5 -5
  109. fin_infra/security/__init__.py +8 -8
  110. fin_infra/security/encryption.py +6 -6
  111. fin_infra/security/models.py +7 -7
  112. fin_infra/security/pii_filter.py +6 -6
  113. fin_infra/security/pii_patterns.py +1 -1
  114. fin_infra/security/token_store.py +3 -1
  115. fin_infra/settings.py +2 -1
  116. fin_infra/tax/__init__.py +2 -2
  117. fin_infra/tax/add.py +3 -2
  118. fin_infra/tax/tlh.py +5 -5
  119. fin_infra/utils/http.py +5 -3
  120. fin_infra/utils/retry.py +2 -1
  121. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -9
  122. fin_infra-0.1.82.dist-info/RECORD +180 -0
  123. fin_infra-0.1.62.dist-info/RECORD +0 -180
  124. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
  125. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
  126. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
fin_infra/__init__.py CHANGED
@@ -1,9 +1,45 @@
1
- """fin_infra: Financial infrastructure toolkit.
1
+ """fin_infra: Financial Infrastructure Toolkit.
2
2
 
3
- Public surface is intentionally small at this stage. Import from submodules for
4
- specific domains (clients, models, markets, credit).
3
+ A comprehensive financial infrastructure library providing:
4
+ - Banking integration (Plaid, Teller, MX)
5
+ - Brokerage integration (Alpaca, Interactive Brokers)
6
+ - Market data (stocks, crypto, forex)
7
+ - Credit scores (Experian, Equifax, TransUnion)
8
+ - Financial calculations (NPV, IRR, PMT, FV, PV)
9
+ - Portfolio analytics (returns, allocation, benchmarking)
10
+ - Transaction categorization (rule-based + ML)
11
+ - Budget management and cash flow analysis
12
+ - Net worth tracking and goal management
13
+
14
+ Example:
15
+ from fin_infra.banking import easy_banking
16
+ from fin_infra.markets import easy_market
17
+
18
+ banking = easy_banking()
19
+ market = easy_market()
20
+ quote = market.quote("AAPL")
5
21
  """
6
22
 
23
+ from __future__ import annotations
24
+
25
+ # Core modules - can be imported as `from fin_infra import banking`
26
+ from . import (
27
+ analytics,
28
+ banking,
29
+ brokerage,
30
+ budgets,
31
+ cashflows,
32
+ categorization,
33
+ credit,
34
+ crypto,
35
+ investments,
36
+ markets,
37
+ net_worth,
38
+ recurring,
39
+ tax,
40
+ )
41
+
42
+ # Base exceptions
7
43
  from .exceptions import (
8
44
  FinInfraError,
9
45
  ProviderError,
@@ -14,6 +50,20 @@ from .version import __version__
14
50
 
15
51
  __all__ = [
16
52
  "__version__",
53
+ # Core modules
54
+ "analytics",
55
+ "banking",
56
+ "brokerage",
57
+ "budgets",
58
+ "cashflows",
59
+ "categorization",
60
+ "credit",
61
+ "crypto",
62
+ "investments",
63
+ "markets",
64
+ "net_worth",
65
+ "recurring",
66
+ "tax",
17
67
  # Base errors
18
68
  "FinInfraError",
19
69
  "ProviderError",
@@ -7,6 +7,16 @@ This module provides comprehensive financial analytics capabilities including:
7
7
  - Portfolio analytics (returns, allocation, benchmarking)
8
8
  - Growth projections (net worth forecasting with scenarios)
9
9
 
10
+ Feature Status:
11
+ ✅ STABLE: Core calculation functions (all analytics work with provided data)
12
+ ⚠️ INTEGRATION: Auto-fetching from providers requires setup:
13
+ - Banking provider for transaction data
14
+ - Brokerage provider for investment data
15
+ - Categorization for expense categorization
16
+
17
+ When providers aren't configured, functions accept data directly or return
18
+ sensible placeholder values for testing/development.
19
+
10
20
  Serves multiple use cases:
11
21
  - Personal finance apps (cash flow, savings tracking)
12
22
  - Wealth management platforms (portfolio analytics, projections)
@@ -45,10 +55,11 @@ Dependencies:
45
55
 
46
56
  from __future__ import annotations
47
57
 
48
- # Import actual implementations
49
- from .ease import easy_analytics, AnalyticsEngine
50
58
  from .add import add_analytics
51
59
 
60
+ # Import actual implementations
61
+ from .ease import AnalyticsEngine, easy_analytics
62
+
52
63
  __all__ = [
53
64
  "easy_analytics",
54
65
  "add_analytics",
@@ -7,7 +7,7 @@ MUST use svc-infra dual routers (user_router) - NEVER generic APIRouter.
7
7
  from __future__ import annotations
8
8
 
9
9
  from datetime import datetime
10
- from typing import TYPE_CHECKING, Optional
10
+ from typing import TYPE_CHECKING
11
11
 
12
12
  from fastapi import HTTPException, Query
13
13
  from pydantic import BaseModel, Field
@@ -15,15 +15,15 @@ from pydantic import BaseModel, Field
15
15
  if TYPE_CHECKING:
16
16
  from fastapi import FastAPI
17
17
 
18
- from .ease import easy_analytics, AnalyticsEngine
18
+ from .ease import AnalyticsEngine, easy_analytics
19
19
  from .models import (
20
+ BenchmarkComparison,
20
21
  CashFlowAnalysis,
21
- SavingsRateData,
22
- SpendingInsight,
22
+ GrowthProjection,
23
23
  PersonalizedSpendingAdvice,
24
24
  PortfolioMetrics,
25
- BenchmarkComparison,
26
- GrowthProjection,
25
+ SavingsRateData,
26
+ SpendingInsight,
27
27
  )
28
28
 
29
29
 
@@ -33,15 +33,15 @@ class NetWorthForecastRequest(BaseModel):
33
33
 
34
34
  user_id: str = Field(..., description="User identifier")
35
35
  years: int = Field(default=30, ge=1, le=50, description="Projection years (1-50)")
36
- initial_net_worth: Optional[float] = Field(None, description="Override initial net worth")
37
- annual_contribution: Optional[float] = Field(None, description="Annual savings contribution")
38
- conservative_return: Optional[float] = Field(
36
+ initial_net_worth: float | None = Field(None, description="Override initial net worth")
37
+ annual_contribution: float | None = Field(None, description="Annual savings contribution")
38
+ conservative_return: float | None = Field(
39
39
  None, description="Conservative return rate (e.g., 0.05 = 5%)"
40
40
  )
41
- moderate_return: Optional[float] = Field(
41
+ moderate_return: float | None = Field(
42
42
  None, description="Moderate return rate (e.g., 0.07 = 7%)"
43
43
  )
44
- aggressive_return: Optional[float] = Field(
44
+ aggressive_return: float | None = Field(
45
45
  None, description="Aggressive return rate (e.g., 0.10 = 10%)"
46
46
  )
47
47
 
@@ -49,7 +49,7 @@ class NetWorthForecastRequest(BaseModel):
49
49
  def add_analytics(
50
50
  app: FastAPI,
51
51
  prefix: str = "/analytics",
52
- provider: Optional[AnalyticsEngine] = None,
52
+ provider: AnalyticsEngine | None = None,
53
53
  include_in_schema: bool = True,
54
54
  ) -> AnalyticsEngine:
55
55
  """Add analytics endpoints to FastAPI application.
@@ -124,9 +124,9 @@ def add_analytics(
124
124
  )
125
125
  async def get_cash_flow(
126
126
  user_id: str,
127
- start_date: Optional[datetime] = None,
128
- end_date: Optional[datetime] = None,
129
- period_days: Optional[int] = None,
127
+ start_date: datetime | None = None,
128
+ end_date: datetime | None = None,
129
+ period_days: int | None = None,
130
130
  ) -> CashFlowAnalysis:
131
131
  """
132
132
  Calculate cash flow analysis for a user.
@@ -164,7 +164,7 @@ def add_analytics(
164
164
  )
165
165
  async def get_spending_insights(
166
166
  user_id: str,
167
- period_days: Optional[int] = None,
167
+ period_days: int | None = None,
168
168
  include_trends: bool = True,
169
169
  ) -> SpendingInsight:
170
170
  """
@@ -186,7 +186,7 @@ def add_analytics(
186
186
  )
187
187
  async def get_spending_advice(
188
188
  user_id: str,
189
- period_days: Optional[int] = None,
189
+ period_days: int | None = None,
190
190
  ) -> PersonalizedSpendingAdvice:
191
191
  """
192
192
  Generate personalized spending advice using AI.
@@ -206,14 +206,12 @@ def add_analytics(
206
206
  )
207
207
  async def get_portfolio_metrics(
208
208
  user_id: str,
209
- accounts: Optional[list[str]] = None,
209
+ accounts: list[str] | None = None,
210
210
  with_holdings: bool = Query(
211
- False,
212
- description="Use real holdings data from investment provider for accurate P/L"
211
+ False, description="Use real holdings data from investment provider for accurate P/L"
213
212
  ),
214
- access_token: Optional[str] = Query(
215
- None,
216
- description="Investment provider access token (required if with_holdings=true)"
213
+ access_token: str | None = Query(
214
+ None, description="Investment provider access token (required if with_holdings=true)"
217
215
  ),
218
216
  ) -> PortfolioMetrics:
219
217
  """
@@ -249,31 +247,31 @@ def add_analytics(
249
247
  if with_holdings:
250
248
  # Check if investment provider is available on app state
251
249
  investment_provider = getattr(app.state, "investment_provider", None)
252
-
250
+
253
251
  if investment_provider and access_token:
254
252
  try:
255
253
  # Fetch real holdings from investment provider
256
254
  from fin_infra.analytics.portfolio import portfolio_metrics_with_holdings
257
-
255
+
258
256
  holdings = await investment_provider.get_holdings(
259
257
  access_token=access_token,
260
258
  account_ids=accounts,
261
259
  )
262
-
260
+
263
261
  # Calculate metrics from real holdings
264
262
  return portfolio_metrics_with_holdings(holdings)
265
-
263
+
266
264
  except Exception as e:
267
265
  # Fall back to balance-only calculation on error
268
266
  # Log error but don't fail the request
269
267
  import logging
268
+
270
269
  logging.warning(f"Failed to fetch holdings, falling back to balance-only: {e}")
271
270
  elif with_holdings and not access_token:
272
271
  raise HTTPException(
273
- status_code=400,
274
- detail="access_token required when with_holdings=true"
272
+ status_code=400, detail="access_token required when with_holdings=true"
275
273
  )
276
-
274
+
277
275
  # Default: Use balance-only calculation (existing behavior)
278
276
  return await provider.portfolio_metrics(
279
277
  user_id,
@@ -288,9 +286,9 @@ def add_analytics(
288
286
  )
289
287
  async def get_benchmark_comparison(
290
288
  user_id: str,
291
- benchmark: Optional[str] = None,
289
+ benchmark: str | None = None,
292
290
  period: str = "1y",
293
- accounts: Optional[list[str]] = None,
291
+ accounts: list[str] | None = None,
294
292
  ) -> BenchmarkComparison:
295
293
  """
296
294
  Compare portfolio to benchmark (e.g., SPY, VTI).
@@ -6,6 +6,7 @@ Provides income vs expense analysis, breakdowns by source/category, and forecast
6
6
  from __future__ import annotations
7
7
 
8
8
  from datetime import datetime, timedelta
9
+ from decimal import Decimal
9
10
  from typing import Any, Optional
10
11
 
11
12
  from ..models import Transaction
@@ -219,7 +220,7 @@ async def forecast_cash_flow(
219
220
  def _categorize_transactions(
220
221
  transactions: list[Transaction],
221
222
  categorization_provider=None,
222
- ) -> tuple[dict[str, float], dict[str, float]]:
223
+ ) -> tuple[dict[str, Decimal], dict[str, Decimal]]:
223
224
  """Helper to categorize transactions into income sources and expense categories.
224
225
 
225
226
  Args:
@@ -229,19 +230,19 @@ def _categorize_transactions(
229
230
  Returns:
230
231
  Tuple of (income_by_source, expenses_by_category) dicts
231
232
  """
232
- income_by_source: dict[str, float] = {}
233
- expenses_by_category: dict[str, float] = {}
233
+ income_by_source: dict[str, Decimal] = {}
234
+ expenses_by_category: dict[str, Decimal] = {}
234
235
 
235
236
  for txn in transactions:
236
237
  if txn.amount > 0:
237
238
  # Income transaction
238
239
  source = _determine_income_source(txn)
239
- income_by_source[source] = income_by_source.get(source, 0) + txn.amount
240
+ income_by_source[source] = income_by_source.get(source, Decimal(0)) + txn.amount
240
241
  else:
241
242
  # Expense transaction
242
243
  category = _get_expense_category(txn, categorization_provider)
243
244
  amount = abs(txn.amount)
244
- expenses_by_category[category] = expenses_by_category.get(category, 0) + amount
245
+ expenses_by_category[category] = expenses_by_category.get(category, Decimal(0)) + amount
245
246
 
246
247
  return income_by_source, expenses_by_category
247
248
 
@@ -16,23 +16,22 @@ Typical usage:
16
16
  portfolio = await analytics.portfolio_metrics(user_id="user123")
17
17
  """
18
18
 
19
- from typing import Optional
20
19
  from datetime import datetime, timedelta
21
20
 
22
21
  from .cash_flow import calculate_cash_flow
23
- from .savings import calculate_savings_rate, SavingsDefinition
24
- from .spending import analyze_spending, generate_spending_insights
25
- from .portfolio import calculate_portfolio_metrics, compare_to_benchmark
26
- from .projections import project_net_worth, calculate_compound_interest
27
22
  from .models import (
23
+ BenchmarkComparison,
28
24
  CashFlowAnalysis,
29
- SavingsRateData,
30
- SpendingInsight,
25
+ GrowthProjection,
31
26
  PersonalizedSpendingAdvice,
32
27
  PortfolioMetrics,
33
- BenchmarkComparison,
34
- GrowthProjection,
28
+ SavingsRateData,
29
+ SpendingInsight,
35
30
  )
31
+ from .portfolio import calculate_portfolio_metrics, compare_to_benchmark
32
+ from .projections import calculate_compound_interest, project_net_worth
33
+ from .savings import SavingsDefinition, calculate_savings_rate
34
+ from .spending import analyze_spending, generate_spending_insights
36
35
 
37
36
 
38
37
  class AnalyticsEngine:
@@ -93,9 +92,9 @@ class AnalyticsEngine:
93
92
  self,
94
93
  user_id: str,
95
94
  *,
96
- start_date: Optional[datetime] = None,
97
- end_date: Optional[datetime] = None,
98
- period_days: Optional[int] = None,
95
+ start_date: datetime | None = None,
96
+ end_date: datetime | None = None,
97
+ period_days: int | None = None,
99
98
  ) -> CashFlowAnalysis:
100
99
  """Analyze cash flow (income vs expenses).
101
100
 
@@ -129,7 +128,7 @@ class AnalyticsEngine:
129
128
  self,
130
129
  user_id: str,
131
130
  *,
132
- definition: Optional[str | SavingsDefinition] = None,
131
+ definition: str | SavingsDefinition | None = None,
133
132
  period: str = "monthly",
134
133
  ) -> SavingsRateData:
135
134
  """Calculate savings rate.
@@ -163,7 +162,7 @@ class AnalyticsEngine:
163
162
  self,
164
163
  user_id: str,
165
164
  *,
166
- period_days: Optional[int] = None,
165
+ period_days: int | None = None,
167
166
  include_trends: bool = True,
168
167
  ) -> SpendingInsight:
169
168
  """Analyze spending patterns and generate insights.
@@ -193,8 +192,8 @@ class AnalyticsEngine:
193
192
  self,
194
193
  user_id: str,
195
194
  *,
196
- period_days: Optional[int] = None,
197
- user_context: Optional[dict] = None,
195
+ period_days: int | None = None,
196
+ user_context: dict | None = None,
198
197
  ) -> PersonalizedSpendingAdvice:
199
198
  """Generate AI-powered personalized spending advice.
200
199
 
@@ -228,7 +227,7 @@ class AnalyticsEngine:
228
227
  self,
229
228
  user_id: str,
230
229
  *,
231
- accounts: Optional[list[str]] = None,
230
+ accounts: list[str] | None = None,
232
231
  ) -> PortfolioMetrics:
233
232
  """Calculate portfolio performance metrics.
234
233
 
@@ -250,9 +249,9 @@ class AnalyticsEngine:
250
249
  self,
251
250
  user_id: str,
252
251
  *,
253
- benchmark: Optional[str] = None,
252
+ benchmark: str | None = None,
254
253
  period: str = "1y",
255
- accounts: Optional[list[str]] = None,
254
+ accounts: list[str] | None = None,
256
255
  ) -> BenchmarkComparison:
257
256
  """Compare portfolio to benchmark index.
258
257
 
@@ -282,7 +281,7 @@ class AnalyticsEngine:
282
281
  user_id: str,
283
282
  *,
284
283
  years: int = 30,
285
- assumptions: Optional[dict] = None,
284
+ assumptions: dict | None = None,
286
285
  ) -> GrowthProjection:
287
286
  """Project net worth growth with scenarios.
288
287
 
@@ -35,7 +35,6 @@ Examples:
35
35
  """
36
36
 
37
37
  from datetime import datetime
38
- from typing import Optional
39
38
 
40
39
  from fin_infra.analytics.models import (
41
40
  AssetAllocation,
@@ -47,7 +46,7 @@ from fin_infra.analytics.models import (
47
46
  async def calculate_portfolio_metrics(
48
47
  user_id: str,
49
48
  *,
50
- accounts: Optional[list[str]] = None,
49
+ accounts: list[str] | None = None,
51
50
  brokerage_provider=None,
52
51
  market_provider=None,
53
52
  ) -> PortfolioMetrics:
@@ -131,7 +130,7 @@ async def compare_to_benchmark(
131
130
  *,
132
131
  benchmark: str = "SPY",
133
132
  period: str = "1y",
134
- accounts: Optional[list[str]] = None,
133
+ accounts: list[str] | None = None,
135
134
  brokerage_provider=None,
136
135
  market_provider=None,
137
136
  ) -> BenchmarkComparison:
@@ -221,7 +220,7 @@ async def compare_to_benchmark(
221
220
 
222
221
  def _generate_mock_holdings(
223
222
  user_id: str,
224
- accounts: Optional[list[str]] = None,
223
+ accounts: list[str] | None = None,
225
224
  ) -> list[dict]:
226
225
  """Generate mock portfolio holdings for testing.
227
226
 
@@ -415,7 +414,7 @@ def _parse_benchmark_period(period: str) -> int:
415
414
  def _calculate_portfolio_return(
416
415
  user_id: str,
417
416
  period_days: int,
418
- accounts: Optional[list[str]] = None,
417
+ accounts: list[str] | None = None,
419
418
  ) -> tuple[float, float]:
420
419
  """Calculate portfolio return for specified period.
421
420
 
@@ -483,7 +482,7 @@ def _calculate_beta(
483
482
  user_id: str,
484
483
  benchmark: str,
485
484
  period_days: int,
486
- ) -> Optional[float]:
485
+ ) -> float | None:
487
486
  """Calculate portfolio beta (volatility relative to benchmark).
488
487
 
489
488
  Beta = Covariance(portfolio_returns, benchmark_returns) / Variance(benchmark_returns)
@@ -565,25 +564,18 @@ def portfolio_metrics_with_holdings(holdings: list) -> PortfolioMetrics:
565
564
  >>> metrics = portfolio_metrics_with_holdings(holdings)
566
565
  """
567
566
  # Import here to avoid circular dependency
568
- from decimal import Decimal
569
567
 
570
568
  # Calculate total portfolio value and cost basis
571
- total_value = float(sum(
572
- holding.institution_value
573
- for holding in holdings
574
- ))
569
+ total_value = float(sum(holding.institution_value for holding in holdings))
575
570
 
576
- total_cost_basis = float(sum(
577
- holding.cost_basis if holding.cost_basis is not None else 0
578
- for holding in holdings
579
- ))
571
+ total_cost_basis = float(
572
+ sum(holding.cost_basis if holding.cost_basis is not None else 0 for holding in holdings)
573
+ )
580
574
 
581
575
  # Calculate total return (P/L)
582
576
  total_return_dollars = total_value - total_cost_basis
583
577
  total_return_percent = (
584
- (total_return_dollars / total_cost_basis * 100.0)
585
- if total_cost_basis > 0
586
- else 0.0
578
+ (total_return_dollars / total_cost_basis * 100.0) if total_cost_basis > 0 else 0.0
587
579
  )
588
580
 
589
581
  # Calculate asset allocation from real security types
@@ -685,9 +677,7 @@ def calculate_day_change_with_snapshot(
685
677
  # Calculate day change
686
678
  day_change_dollars = current_total - previous_total
687
679
  day_change_percent = (
688
- (day_change_dollars / previous_total * 100.0)
689
- if previous_total > 0
690
- else 0.0
680
+ (day_change_dollars / previous_total * 100.0) if previous_total > 0 else 0.0
691
681
  )
692
682
 
693
683
  return {
@@ -722,6 +712,7 @@ def _calculate_allocation_from_holdings(
722
712
  - other → Other
723
713
  """
724
714
  from collections import defaultdict
715
+
725
716
  from .models import AssetAllocation
726
717
 
727
718
  if total_value == 0:
@@ -739,18 +730,20 @@ def _calculate_allocation_from_holdings(
739
730
  }
740
731
 
741
732
  # Sum values by asset class
742
- allocation_values = defaultdict(float)
733
+ allocation_values: dict[str, float] = defaultdict(float)
743
734
  for holding in holdings:
744
- security_type = holding.security.type.value if hasattr(holding.security.type, 'value') else holding.security.type
735
+ security_type = (
736
+ holding.security.type.value
737
+ if hasattr(holding.security.type, "value")
738
+ else holding.security.type
739
+ )
745
740
  asset_class = type_to_class.get(security_type, "Other")
746
741
  allocation_values[asset_class] += float(holding.institution_value)
747
742
 
748
743
  # Convert to list of AssetAllocation objects
749
744
  allocation_list = [
750
745
  AssetAllocation(
751
- asset_class=asset_class,
752
- value=value,
753
- percentage=round((value / total_value) * 100.0, 2)
746
+ asset_class=asset_class, value=value, percentage=round((value / total_value) * 100.0, 2)
754
747
  )
755
748
  for asset_class, value in allocation_values.items()
756
749
  ]
@@ -19,12 +19,10 @@ Typical usage:
19
19
  )
20
20
  """
21
21
 
22
- from typing import Optional
23
22
  import math
24
23
 
25
24
  from fin_infra.analytics.models import GrowthProjection, Scenario
26
25
 
27
-
28
26
  # ============================================================================
29
27
  # Public API
30
28
  # ============================================================================
@@ -34,7 +32,7 @@ async def project_net_worth(
34
32
  user_id: str,
35
33
  *,
36
34
  years: int = 30,
37
- assumptions: Optional[dict] = None,
35
+ assumptions: dict | None = None,
38
36
  net_worth_provider=None,
39
37
  cash_flow_provider=None,
40
38
  ) -> GrowthProjection:
@@ -329,13 +329,11 @@ def _generate_trade_reasoning(
329
329
 
330
330
  if action == "buy":
331
331
  return (
332
- f"Buy {symbol} to increase {asset_class} allocation "
333
- f"by {diff_pct:.1f}% towards target"
332
+ f"Buy {symbol} to increase {asset_class} allocation by {diff_pct:.1f}% towards target"
334
333
  )
335
334
  else:
336
335
  return (
337
- f"Sell {symbol} to decrease {asset_class} allocation "
338
- f"by {diff_pct:.1f}% towards target"
336
+ f"Sell {symbol} to decrease {asset_class} allocation by {diff_pct:.1f}% towards target"
339
337
  )
340
338
 
341
339
 
@@ -77,7 +77,7 @@ async def calculate_savings_rate(
77
77
  period_enum = Period(period)
78
78
  except ValueError:
79
79
  raise ValueError(
80
- f"Invalid period '{period}'. Must be one of: " f"{', '.join([p.value for p in Period])}"
80
+ f"Invalid period '{period}'. Must be one of: {', '.join([p.value for p in Period])}"
81
81
  )
82
82
 
83
83
  try:
@@ -162,12 +162,13 @@ async def analyze_spending(
162
162
  )
163
163
 
164
164
  return SpendingInsight(
165
- top_merchants=top_merchants,
166
- category_breakdown=dict(category_totals),
165
+ # Convert Decimal to float for model compatibility (intentional for Pydantic field types)
166
+ top_merchants=[(m, float(v)) for m, v in top_merchants],
167
+ category_breakdown={k: float(v) for k, v in category_totals.items()},
167
168
  spending_trends=spending_trends,
168
169
  anomalies=anomalies,
169
170
  period_days=days,
170
- total_spending=total_spending,
171
+ total_spending=float(total_spending) if total_spending else 0.0,
171
172
  )
172
173
 
173
174
 
@@ -291,10 +292,10 @@ async def _calculate_spending_trends(
291
292
  # In reality, would fetch historical data
292
293
  previous_amount = current_amount * Decimal("0.9")
293
294
 
294
- change_percent = (
295
- ((current_amount - previous_amount) / previous_amount) * 100
295
+ change_percent: float = (
296
+ float((current_amount - previous_amount) / previous_amount) * 100
296
297
  if previous_amount > 0
297
- else 0
298
+ else 0.0
298
299
  )
299
300
 
300
301
  # Threshold for "stable" is within 5%
@@ -342,8 +343,10 @@ async def _detect_spending_anomalies(
342
343
  # In reality, would calculate from historical data
343
344
  average_amount = current_amount * Decimal("0.8")
344
345
 
345
- deviation_percent = (
346
- ((current_amount - average_amount) / average_amount) * 100 if average_amount > 0 else 0
346
+ deviation_percent: float = (
347
+ float((current_amount - average_amount) / average_amount) * 100
348
+ if average_amount > 0
349
+ else 0.0
347
350
  )
348
351
 
349
352
  # Detect anomalies based on deviation
@@ -359,9 +362,10 @@ async def _detect_spending_anomalies(
359
362
  anomalies.append(
360
363
  SpendingAnomaly(
361
364
  category=category,
362
- current_amount=current_amount,
363
- average_amount=average_amount,
364
- deviation_percent=deviation_percent,
365
+ # Convert Decimal to float for model compatibility (intentional for Pydantic field types)
366
+ current_amount=float(current_amount),
367
+ average_amount=float(average_amount),
368
+ deviation_percent=float(deviation_percent),
365
369
  severity=severity,
366
370
  )
367
371
  )