fin-infra 0.1.62__py3-none-any.whl → 0.1.69__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/add.py +9 -11
- fin_infra/analytics/cash_flow.py +6 -5
- fin_infra/analytics/portfolio.py +13 -20
- 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 +8 -5
- fin_infra/banking/history.py +3 -3
- fin_infra/banking/utils.py +93 -88
- fin_infra/brokerage/__init__.py +5 -3
- fin_infra/budgets/tracker.py +2 -3
- fin_infra/cashflows/__init__.py +6 -8
- fin_infra/categorization/__init__.py +1 -1
- fin_infra/categorization/add.py +15 -16
- fin_infra/categorization/ease.py +3 -4
- fin_infra/categorization/engine.py +4 -4
- fin_infra/categorization/llm_layer.py +5 -6
- fin_infra/categorization/models.py +1 -1
- fin_infra/chat/__init__.py +7 -16
- fin_infra/chat/planning.py +57 -0
- fin_infra/cli/cmds/scaffold_cmds.py +1 -1
- fin_infra/compliance/__init__.py +3 -3
- fin_infra/credit/add.py +3 -2
- fin_infra/credit/experian/auth.py +3 -2
- fin_infra/credit/experian/client.py +2 -2
- fin_infra/credit/experian/provider.py +16 -16
- fin_infra/crypto/__init__.py +1 -1
- fin_infra/crypto/insights.py +1 -3
- fin_infra/documents/add.py +5 -5
- fin_infra/documents/ease.py +4 -3
- fin_infra/documents/models.py +3 -3
- fin_infra/documents/ocr.py +1 -1
- fin_infra/documents/storage.py +2 -1
- fin_infra/exceptions.py +1 -1
- fin_infra/goals/add.py +2 -2
- fin_infra/goals/management.py +6 -6
- fin_infra/goals/milestones.py +2 -2
- fin_infra/insights/__init__.py +7 -8
- fin_infra/investments/__init__.py +13 -8
- fin_infra/investments/add.py +39 -59
- fin_infra/investments/ease.py +16 -13
- fin_infra/investments/models.py +130 -64
- fin_infra/investments/providers/base.py +3 -8
- fin_infra/investments/providers/plaid.py +23 -34
- fin_infra/investments/providers/snaptrade.py +22 -40
- fin_infra/markets/__init__.py +11 -8
- fin_infra/models/accounts.py +2 -1
- fin_infra/models/transactions.py +3 -2
- fin_infra/net_worth/add.py +8 -5
- fin_infra/net_worth/aggregator.py +5 -4
- fin_infra/net_worth/calculator.py +8 -6
- fin_infra/net_worth/ease.py +36 -15
- fin_infra/net_worth/insights.py +4 -4
- fin_infra/net_worth/models.py +237 -116
- fin_infra/normalization/__init__.py +15 -13
- fin_infra/normalization/providers/exchangerate.py +3 -3
- fin_infra/obs/classifier.py +2 -2
- fin_infra/providers/banking/plaid_client.py +20 -19
- fin_infra/providers/banking/teller_client.py +13 -7
- fin_infra/providers/base.py +105 -13
- fin_infra/providers/brokerage/alpaca.py +7 -7
- fin_infra/providers/credit/experian.py +5 -0
- fin_infra/providers/market/ccxt_crypto.py +8 -3
- fin_infra/providers/tax/mock.py +3 -3
- fin_infra/recurring/add.py +20 -9
- fin_infra/recurring/detector.py +1 -1
- fin_infra/recurring/detectors_llm.py +10 -9
- fin_infra/recurring/ease.py +1 -1
- fin_infra/recurring/insights.py +9 -8
- fin_infra/recurring/models.py +3 -3
- fin_infra/recurring/normalizer.py +3 -2
- fin_infra/recurring/normalizers.py +9 -8
- fin_infra/scaffold/__init__.py +1 -1
- fin_infra/security/encryption.py +2 -2
- fin_infra/security/pii_patterns.py +1 -1
- fin_infra/security/token_store.py +3 -1
- fin_infra/tax/__init__.py +1 -1
- fin_infra/utils/http.py +3 -2
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/METADATA +1 -2
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/RECORD +83 -83
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/entry_points.txt +0 -0
fin_infra/markets/__init__.py
CHANGED
|
@@ -93,7 +93,7 @@ def easy_market(
|
|
|
93
93
|
|
|
94
94
|
else:
|
|
95
95
|
raise ValueError(
|
|
96
|
-
f"Unknown market data provider: {provider_name}.
|
|
96
|
+
f"Unknown market data provider: {provider_name}. Supported: alphavantage, yahoo"
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
|
|
@@ -181,19 +181,19 @@ def add_market_data(
|
|
|
181
181
|
- docs/adr/0004-market-data-integration.md: Architecture decisions
|
|
182
182
|
"""
|
|
183
183
|
from fastapi import HTTPException, Query
|
|
184
|
-
from typing import TYPE_CHECKING, Optional
|
|
185
184
|
|
|
186
185
|
# Import svc-infra public router (no auth required for market data)
|
|
187
186
|
from svc_infra.api.fastapi.dual.public import public_router
|
|
188
187
|
|
|
189
|
-
if TYPE_CHECKING:
|
|
190
|
-
from fastapi import FastAPI
|
|
191
|
-
|
|
192
188
|
# Create market provider instance (or use the provided one)
|
|
193
189
|
if isinstance(provider, MarketDataProvider):
|
|
194
190
|
market = provider
|
|
195
191
|
else:
|
|
196
|
-
|
|
192
|
+
# Cast provider to Literal type for type checker
|
|
193
|
+
provider_literal: Literal["alphavantage", "yahoo"] | None = (
|
|
194
|
+
provider if provider in ("alphavantage", "yahoo", None) else None # type: ignore[assignment]
|
|
195
|
+
)
|
|
196
|
+
market = easy_market(provider=provider_literal, **config)
|
|
197
197
|
|
|
198
198
|
# Create router (public - no auth required)
|
|
199
199
|
router = public_router(prefix=prefix, tags=["Market Data"])
|
|
@@ -223,14 +223,17 @@ def add_market_data(
|
|
|
223
223
|
try:
|
|
224
224
|
candles = market.history(symbol, period=period, interval=interval)
|
|
225
225
|
# Convert to dicts if they're Pydantic models
|
|
226
|
-
candles_list = []
|
|
226
|
+
candles_list: list[dict] = []
|
|
227
227
|
for candle in candles:
|
|
228
228
|
if hasattr(candle, "model_dump"):
|
|
229
229
|
candles_list.append(candle.model_dump())
|
|
230
230
|
elif hasattr(candle, "dict"):
|
|
231
231
|
candles_list.append(candle.dict())
|
|
232
232
|
else:
|
|
233
|
-
|
|
233
|
+
# Cast to dict for type compatibility
|
|
234
|
+
candles_list.append(
|
|
235
|
+
dict(candle) if hasattr(candle, "__iter__") else {"data": candle}
|
|
236
|
+
)
|
|
234
237
|
return {"candles": candles_list}
|
|
235
238
|
except Exception as e:
|
|
236
239
|
raise HTTPException(status_code=400, detail=str(e))
|
fin_infra/models/accounts.py
CHANGED
|
@@ -18,10 +18,11 @@ class AccountType(str, Enum):
|
|
|
18
18
|
|
|
19
19
|
class Account(BaseModel):
|
|
20
20
|
"""Financial account model.
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
Uses Decimal for balance fields to prevent floating-point precision errors
|
|
23
23
|
in financial calculations (e.g., $0.01 + $0.02 != $0.03 with float).
|
|
24
24
|
"""
|
|
25
|
+
|
|
25
26
|
id: str
|
|
26
27
|
name: str
|
|
27
28
|
type: AccountType
|
fin_infra/models/transactions.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from datetime import date
|
|
3
|
+
from datetime import date # noqa: F401 - used in type annotation
|
|
4
4
|
from decimal import Decimal
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
@@ -9,10 +9,11 @@ from pydantic import BaseModel, field_validator
|
|
|
9
9
|
|
|
10
10
|
class Transaction(BaseModel):
|
|
11
11
|
"""Financial transaction model.
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
Uses Decimal for amount to prevent floating-point precision errors
|
|
14
14
|
in financial calculations (e.g., $0.01 + $0.02 != $0.03 with float).
|
|
15
15
|
"""
|
|
16
|
+
|
|
16
17
|
id: str
|
|
17
18
|
account_id: str
|
|
18
19
|
date: date
|
fin_infra/net_worth/add.py
CHANGED
|
@@ -36,13 +36,16 @@ tracker = add_net_worth_tracking(app)
|
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
38
|
from datetime import datetime, timedelta
|
|
39
|
+
from typing import Any
|
|
39
40
|
|
|
40
41
|
from fastapi import FastAPI, HTTPException, Query
|
|
41
42
|
|
|
42
43
|
from fin_infra.net_worth.ease import NetWorthTracker, easy_net_worth
|
|
43
44
|
from fin_infra.net_worth.models import (
|
|
45
|
+
AssetDetail,
|
|
44
46
|
ConversationResponse,
|
|
45
47
|
GoalProgressResponse,
|
|
48
|
+
LiabilityDetail,
|
|
46
49
|
NetWorthResponse,
|
|
47
50
|
SnapshotHistoryResponse,
|
|
48
51
|
)
|
|
@@ -188,8 +191,8 @@ def add_net_worth_tracking(
|
|
|
188
191
|
# Persistence: Asset/liability details stored in snapshot JSON fields or separate tables.
|
|
189
192
|
# Generate with: fin-infra scaffold net_worth --dest-dir app/models/
|
|
190
193
|
# For now, create empty lists for testing/examples.
|
|
191
|
-
asset_details = []
|
|
192
|
-
liability_details = []
|
|
194
|
+
asset_details: list[AssetDetail] = []
|
|
195
|
+
liability_details: list[LiabilityDetail] = []
|
|
193
196
|
|
|
194
197
|
# Calculate breakdowns
|
|
195
198
|
asset_allocation = calculate_asset_allocation(asset_details)
|
|
@@ -508,7 +511,7 @@ def add_net_worth_tracking(
|
|
|
508
511
|
snapshot = await tracker.calculate_net_worth(user_id=user_id, access_token=access_token)
|
|
509
512
|
|
|
510
513
|
# Get goals for context (if goal_tracker available)
|
|
511
|
-
goals = []
|
|
514
|
+
goals: list[Any] = []
|
|
512
515
|
if tracker.goal_tracker:
|
|
513
516
|
# TODO: Implement get_goals() method
|
|
514
517
|
pass
|
|
@@ -657,10 +660,10 @@ def add_net_worth_tracking(
|
|
|
657
660
|
|
|
658
661
|
try:
|
|
659
662
|
# Get current snapshot
|
|
660
|
-
|
|
663
|
+
await tracker.calculate_net_worth(user_id=user_id, access_token=access_token)
|
|
661
664
|
|
|
662
665
|
# Get historical snapshots for progress tracking
|
|
663
|
-
|
|
666
|
+
await tracker.get_snapshots(user_id=user_id, days=90)
|
|
664
667
|
|
|
665
668
|
# Persistence: Goal retrieval via scaffolded goals repository.
|
|
666
669
|
# Generate with: fin-infra scaffold goals --dest-dir app/models/
|
|
@@ -213,16 +213,17 @@ class NetWorthAggregator:
|
|
|
213
213
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
214
214
|
|
|
215
215
|
# Aggregate results (skip failed providers)
|
|
216
|
-
all_assets = []
|
|
217
|
-
all_liabilities = []
|
|
218
|
-
actual_providers = []
|
|
216
|
+
all_assets: list[AssetDetail] = []
|
|
217
|
+
all_liabilities: list[LiabilityDetail] = []
|
|
218
|
+
actual_providers: list[str] = []
|
|
219
219
|
|
|
220
220
|
for i, result in enumerate(results):
|
|
221
|
-
if isinstance(result,
|
|
221
|
+
if isinstance(result, BaseException):
|
|
222
222
|
# Log error but continue (graceful degradation)
|
|
223
223
|
print(f"Provider {providers_used[i]} failed: {result}")
|
|
224
224
|
continue
|
|
225
225
|
|
|
226
|
+
# result is now tuple[list[AssetDetail], list[LiabilityDetail]]
|
|
226
227
|
assets, liabilities = result
|
|
227
228
|
all_assets.extend(assets)
|
|
228
229
|
all_liabilities.extend(liabilities)
|
|
@@ -30,7 +30,6 @@ print(f"Net Worth: ${net_worth:,.2f}") # $55,000.00
|
|
|
30
30
|
```
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
|
|
34
33
|
from fin_infra.net_worth.models import (
|
|
35
34
|
AssetAllocation,
|
|
36
35
|
AssetCategory,
|
|
@@ -100,18 +99,19 @@ def calculate_net_worth(
|
|
|
100
99
|
|
|
101
100
|
Returns:
|
|
102
101
|
Net worth in base currency
|
|
103
|
-
|
|
102
|
+
|
|
104
103
|
Raises:
|
|
105
104
|
ValueError: If assets or liabilities contain non-base currencies and no
|
|
106
105
|
exchange rate conversion is available. This prevents silent data loss.
|
|
107
106
|
"""
|
|
108
107
|
import logging
|
|
108
|
+
|
|
109
109
|
logger = logging.getLogger(__name__)
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
# Collect any non-base currency items for error reporting
|
|
112
112
|
non_base_assets: list[tuple[str, str, float]] = []
|
|
113
113
|
non_base_liabilities: list[tuple[str, str, float]] = []
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
# Sum all assets (use market_value if available, otherwise balance)
|
|
116
116
|
total_assets = 0.0
|
|
117
117
|
for asset in assets:
|
|
@@ -130,7 +130,9 @@ def calculate_net_worth(
|
|
|
130
130
|
for liability in liabilities:
|
|
131
131
|
# Check for non-base currency
|
|
132
132
|
if liability.currency != base_currency:
|
|
133
|
-
non_base_liabilities.append(
|
|
133
|
+
non_base_liabilities.append(
|
|
134
|
+
(liability.name or liability.account_id, liability.currency, liability.balance)
|
|
135
|
+
)
|
|
134
136
|
continue
|
|
135
137
|
|
|
136
138
|
total_liabilities += liability.balance
|
|
@@ -143,7 +145,7 @@ def calculate_net_worth(
|
|
|
143
145
|
items_msg.append(f"Assets: {non_base_assets}")
|
|
144
146
|
if non_base_liabilities:
|
|
145
147
|
items_msg.append(f"Liabilities: {non_base_liabilities}")
|
|
146
|
-
|
|
148
|
+
|
|
147
149
|
error_msg = (
|
|
148
150
|
f"Cannot calculate net worth: found accounts in non-{base_currency} currencies. "
|
|
149
151
|
f"Currency conversion not yet implemented. {'; '.join(items_msg)}. "
|
fin_infra/net_worth/ease.py
CHANGED
|
@@ -121,6 +121,15 @@ class NetWorthTracker:
|
|
|
121
121
|
self.goal_tracker = goal_tracker
|
|
122
122
|
self.conversation = conversation
|
|
123
123
|
|
|
124
|
+
# Configuration set by easy_net_worth(); declared here for type checkers.
|
|
125
|
+
self.snapshot_schedule: str = "daily"
|
|
126
|
+
self.change_threshold_percent: float = 5.0
|
|
127
|
+
self.change_threshold_amount: float = 10000.0
|
|
128
|
+
self.enable_llm: bool = False
|
|
129
|
+
self.llm_provider: str | None = None
|
|
130
|
+
self.llm_model: str | None = None
|
|
131
|
+
self.config: dict[str, Any] = {}
|
|
132
|
+
|
|
124
133
|
async def calculate_net_worth(
|
|
125
134
|
self,
|
|
126
135
|
user_id: str,
|
|
@@ -368,12 +377,20 @@ def easy_net_worth(
|
|
|
368
377
|
|
|
369
378
|
if enable_llm:
|
|
370
379
|
try:
|
|
371
|
-
from ai_infra.llm import LLM
|
|
380
|
+
from ai_infra.llm.llm import LLM
|
|
372
381
|
except ImportError:
|
|
373
382
|
raise ImportError(
|
|
374
|
-
"LLM features require ai-infra package.
|
|
383
|
+
"LLM features require ai-infra package. Install with: pip install ai-infra"
|
|
375
384
|
)
|
|
376
385
|
|
|
386
|
+
cache = None
|
|
387
|
+
try:
|
|
388
|
+
from svc_infra.cache import get_cache
|
|
389
|
+
|
|
390
|
+
cache = get_cache()
|
|
391
|
+
except Exception:
|
|
392
|
+
cache = None
|
|
393
|
+
|
|
377
394
|
# Determine default model
|
|
378
395
|
default_models = {
|
|
379
396
|
"google": "gemini-2.0-flash-exp",
|
|
@@ -384,7 +401,7 @@ def easy_net_worth(
|
|
|
384
401
|
|
|
385
402
|
if not model_name:
|
|
386
403
|
raise ValueError(
|
|
387
|
-
f"Unknown llm_provider: {llm_provider}.
|
|
404
|
+
f"Unknown llm_provider: {llm_provider}. Use 'google', 'openai', or 'anthropic'"
|
|
388
405
|
)
|
|
389
406
|
|
|
390
407
|
# Create shared LLM instance
|
|
@@ -416,18 +433,22 @@ def easy_net_worth(
|
|
|
416
433
|
# goals.management not yet implemented, skip
|
|
417
434
|
pass
|
|
418
435
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
436
|
+
if cache is not None:
|
|
437
|
+
try:
|
|
438
|
+
from fin_infra.conversation import FinancialPlanningConversation
|
|
439
|
+
|
|
440
|
+
conversation = FinancialPlanningConversation(
|
|
441
|
+
llm=llm,
|
|
442
|
+
cache=cache, # Required for context storage
|
|
443
|
+
provider=llm_provider,
|
|
444
|
+
model_name=model_name,
|
|
445
|
+
)
|
|
446
|
+
except ImportError:
|
|
447
|
+
# conversation module not yet implemented, skip
|
|
448
|
+
pass
|
|
449
|
+
except Exception:
|
|
450
|
+
# Cache not configured or other runtime issue; skip optional conversation wiring.
|
|
451
|
+
pass
|
|
431
452
|
|
|
432
453
|
# Create tracker
|
|
433
454
|
tracker = NetWorthTracker(
|
fin_infra/net_worth/insights.py
CHANGED
|
@@ -479,13 +479,13 @@ class NetWorthInsightsGenerator:
|
|
|
479
479
|
|
|
480
480
|
user_prompt = f"""Analyze wealth trends:
|
|
481
481
|
|
|
482
|
-
Current net worth: ${current[
|
|
483
|
-
Previous net worth: ${previous[
|
|
482
|
+
Current net worth: ${current["total_net_worth"]:,.0f}
|
|
483
|
+
Previous net worth: ${previous["total_net_worth"]:,.0f}
|
|
484
484
|
Period: {period}
|
|
485
485
|
Change: ${change_amount:,.0f} ({change_percent:.1%})
|
|
486
486
|
|
|
487
|
-
Assets: ${current[
|
|
488
|
-
Liabilities: ${current[
|
|
487
|
+
Assets: ${current["total_assets"]:,.0f}
|
|
488
|
+
Liabilities: ${current["total_liabilities"]:,.0f}
|
|
489
489
|
|
|
490
490
|
Identify drivers of change, risk factors, and recommendations."""
|
|
491
491
|
|