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.
- fin_infra/__init__.py +53 -3
- fin_infra/analytics/__init__.py +13 -2
- fin_infra/analytics/add.py +30 -32
- fin_infra/analytics/cash_flow.py +6 -5
- fin_infra/analytics/ease.py +19 -20
- fin_infra/analytics/portfolio.py +19 -26
- fin_infra/analytics/projections.py +1 -3
- fin_infra/analytics/rebalancing.py +2 -4
- fin_infra/analytics/savings.py +1 -1
- fin_infra/analytics/spending.py +15 -11
- fin_infra/banking/__init__.py +33 -31
- fin_infra/banking/history.py +11 -12
- fin_infra/banking/utils.py +116 -110
- fin_infra/brokerage/__init__.py +27 -27
- fin_infra/budgets/__init__.py +3 -3
- fin_infra/budgets/add.py +16 -17
- fin_infra/budgets/alerts.py +3 -3
- fin_infra/budgets/tracker.py +4 -5
- fin_infra/cashflows/__init__.py +8 -10
- fin_infra/cashflows/core.py +1 -1
- fin_infra/categorization/__init__.py +1 -1
- fin_infra/categorization/add.py +17 -19
- fin_infra/categorization/ease.py +3 -4
- fin_infra/categorization/engine.py +21 -18
- fin_infra/categorization/llm_layer.py +10 -10
- fin_infra/categorization/models.py +1 -1
- fin_infra/categorization/rules.py +2 -4
- fin_infra/categorization/taxonomy.py +2 -2
- fin_infra/chat/__init__.py +13 -22
- fin_infra/chat/planning.py +57 -1
- fin_infra/cli/cmds/scaffold_cmds.py +11 -12
- fin_infra/clients/__init__.py +23 -1
- fin_infra/clients/base.py +1 -1
- fin_infra/clients/plaid.py +2 -2
- fin_infra/compliance/__init__.py +7 -6
- fin_infra/credit/add.py +7 -7
- fin_infra/credit/experian/auth.py +3 -2
- fin_infra/credit/experian/client.py +2 -2
- fin_infra/credit/experian/provider.py +19 -19
- fin_infra/crypto/__init__.py +8 -10
- fin_infra/crypto/insights.py +5 -6
- fin_infra/documents/add.py +11 -13
- fin_infra/documents/analysis.py +9 -9
- fin_infra/documents/ease.py +18 -17
- fin_infra/documents/models.py +7 -7
- fin_infra/documents/ocr.py +8 -8
- fin_infra/documents/storage.py +23 -14
- fin_infra/exceptions.py +1 -2
- fin_infra/goals/__init__.py +8 -8
- fin_infra/goals/add.py +36 -36
- fin_infra/goals/funding.py +4 -6
- fin_infra/goals/management.py +6 -7
- fin_infra/goals/milestones.py +2 -3
- fin_infra/goals/models.py +7 -11
- fin_infra/insights/__init__.py +12 -10
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +14 -9
- fin_infra/investments/add.py +53 -73
- fin_infra/investments/ease.py +16 -13
- fin_infra/investments/models.py +135 -69
- fin_infra/investments/providers/base.py +9 -15
- fin_infra/investments/providers/plaid.py +70 -55
- fin_infra/investments/providers/snaptrade.py +35 -53
- fin_infra/markets/__init__.py +16 -11
- fin_infra/models/__init__.py +10 -10
- fin_infra/models/accounts.py +2 -1
- fin_infra/models/brokerage.py +2 -1
- fin_infra/models/candle.py +1 -0
- fin_infra/models/money.py +1 -0
- fin_infra/models/quotes.py +4 -3
- fin_infra/models/tax.py +2 -1
- fin_infra/models/transactions.py +4 -4
- fin_infra/net_worth/__init__.py +7 -0
- fin_infra/net_worth/add.py +8 -5
- fin_infra/net_worth/aggregator.py +9 -6
- fin_infra/net_worth/calculator.py +8 -6
- fin_infra/net_worth/ease.py +36 -15
- fin_infra/net_worth/insights.py +4 -5
- fin_infra/net_worth/models.py +237 -116
- fin_infra/normalization/__init__.py +17 -15
- fin_infra/normalization/providers/exchangerate.py +5 -5
- fin_infra/obs/classifier.py +3 -3
- fin_infra/providers/banking/plaid_client.py +23 -22
- fin_infra/providers/banking/teller_client.py +14 -7
- fin_infra/providers/base.py +131 -14
- fin_infra/providers/brokerage/alpaca.py +7 -7
- fin_infra/providers/credit/experian.py +5 -0
- fin_infra/providers/market/alphavantage.py +6 -11
- fin_infra/providers/market/ccxt_crypto.py +25 -4
- fin_infra/providers/market/coingecko.py +5 -6
- fin_infra/providers/market/yahoo.py +23 -8
- fin_infra/providers/tax/__init__.py +1 -1
- fin_infra/providers/tax/irs.py +1 -1
- fin_infra/providers/tax/mock.py +8 -8
- fin_infra/providers/tax/taxbit.py +1 -1
- fin_infra/recurring/__init__.py +6 -6
- fin_infra/recurring/add.py +24 -12
- fin_infra/recurring/detector.py +8 -8
- fin_infra/recurring/detectors_llm.py +14 -13
- fin_infra/recurring/ease.py +3 -5
- fin_infra/recurring/insights.py +20 -19
- fin_infra/recurring/models.py +3 -3
- fin_infra/recurring/normalizer.py +3 -2
- fin_infra/recurring/normalizers.py +11 -10
- fin_infra/recurring/summary.py +13 -15
- fin_infra/scaffold/__init__.py +1 -1
- fin_infra/scaffold/budgets.py +9 -9
- fin_infra/scaffold/goals.py +5 -5
- fin_infra/security/__init__.py +8 -8
- fin_infra/security/encryption.py +6 -6
- fin_infra/security/models.py +7 -7
- fin_infra/security/pii_filter.py +6 -6
- fin_infra/security/pii_patterns.py +1 -1
- fin_infra/security/token_store.py +3 -1
- fin_infra/settings.py +2 -1
- fin_infra/tax/__init__.py +2 -2
- fin_infra/tax/add.py +3 -2
- fin_infra/tax/tlh.py +5 -5
- fin_infra/utils/http.py +5 -3
- fin_infra/utils/retry.py +2 -1
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -9
- fin_infra-0.1.82.dist-info/RECORD +180 -0
- fin_infra-0.1.62.dist-info/RECORD +0 -180
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
- {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
|
|
1
|
+
"""fin_infra: Financial Infrastructure Toolkit.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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",
|
fin_infra/analytics/__init__.py
CHANGED
|
@@ -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",
|
fin_infra/analytics/add.py
CHANGED
|
@@ -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
|
|
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
|
|
18
|
+
from .ease import AnalyticsEngine, easy_analytics
|
|
19
19
|
from .models import (
|
|
20
|
+
BenchmarkComparison,
|
|
20
21
|
CashFlowAnalysis,
|
|
21
|
-
|
|
22
|
-
SpendingInsight,
|
|
22
|
+
GrowthProjection,
|
|
23
23
|
PersonalizedSpendingAdvice,
|
|
24
24
|
PortfolioMetrics,
|
|
25
|
-
|
|
26
|
-
|
|
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:
|
|
37
|
-
annual_contribution:
|
|
38
|
-
conservative_return:
|
|
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:
|
|
41
|
+
moderate_return: float | None = Field(
|
|
42
42
|
None, description="Moderate return rate (e.g., 0.07 = 7%)"
|
|
43
43
|
)
|
|
44
|
-
aggressive_return:
|
|
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:
|
|
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:
|
|
128
|
-
end_date:
|
|
129
|
-
period_days:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
289
|
+
benchmark: str | None = None,
|
|
292
290
|
period: str = "1y",
|
|
293
|
-
accounts:
|
|
291
|
+
accounts: list[str] | None = None,
|
|
294
292
|
) -> BenchmarkComparison:
|
|
295
293
|
"""
|
|
296
294
|
Compare portfolio to benchmark (e.g., SPY, VTI).
|
fin_infra/analytics/cash_flow.py
CHANGED
|
@@ -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,
|
|
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,
|
|
233
|
-
expenses_by_category: dict[str,
|
|
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
|
|
fin_infra/analytics/ease.py
CHANGED
|
@@ -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
|
-
|
|
30
|
-
SpendingInsight,
|
|
25
|
+
GrowthProjection,
|
|
31
26
|
PersonalizedSpendingAdvice,
|
|
32
27
|
PortfolioMetrics,
|
|
33
|
-
|
|
34
|
-
|
|
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:
|
|
97
|
-
end_date:
|
|
98
|
-
period_days:
|
|
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:
|
|
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:
|
|
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:
|
|
197
|
-
user_context:
|
|
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:
|
|
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:
|
|
252
|
+
benchmark: str | None = None,
|
|
254
253
|
period: str = "1y",
|
|
255
|
-
accounts:
|
|
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:
|
|
284
|
+
assumptions: dict | None = None,
|
|
286
285
|
) -> GrowthProjection:
|
|
287
286
|
"""Project net worth growth with scenarios.
|
|
288
287
|
|
fin_infra/analytics/portfolio.py
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
) ->
|
|
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(
|
|
577
|
-
holding.cost_basis if holding.cost_basis is not None else 0
|
|
578
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
fin_infra/analytics/savings.py
CHANGED
|
@@ -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:
|
|
80
|
+
f"Invalid period '{period}'. Must be one of: {', '.join([p.value for p in Period])}"
|
|
81
81
|
)
|
|
82
82
|
|
|
83
83
|
try:
|
fin_infra/analytics/spending.py
CHANGED
|
@@ -162,12 +162,13 @@ async def analyze_spending(
|
|
|
162
162
|
)
|
|
163
163
|
|
|
164
164
|
return SpendingInsight(
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
)
|