fin-infra 0.1.69__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/__init__.py +53 -3
- fin_infra/analytics/__init__.py +13 -2
- fin_infra/analytics/add.py +24 -24
- fin_infra/analytics/cash_flow.py +3 -3
- fin_infra/analytics/ease.py +19 -20
- fin_infra/analytics/models.py +5 -5
- fin_infra/analytics/portfolio.py +18 -18
- fin_infra/analytics/projections.py +1 -3
- fin_infra/analytics/spending.py +4 -5
- fin_infra/banking/__init__.py +27 -28
- fin_infra/banking/history.py +12 -13
- fin_infra/banking/utils.py +27 -26
- fin_infra/brokerage/__init__.py +29 -31
- fin_infra/budgets/__init__.py +3 -3
- fin_infra/budgets/add.py +16 -17
- fin_infra/budgets/alerts.py +4 -4
- 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 +4 -4
- fin_infra/cashflows/__init__.py +3 -3
- fin_infra/cashflows/core.py +1 -1
- fin_infra/categorization/__init__.py +1 -1
- fin_infra/categorization/add.py +2 -3
- fin_infra/categorization/ease.py +3 -3
- fin_infra/categorization/engine.py +18 -15
- fin_infra/categorization/llm_layer.py +13 -10
- fin_infra/categorization/models.py +3 -4
- fin_infra/categorization/rules.py +2 -4
- fin_infra/categorization/taxonomy.py +2 -2
- fin_infra/chat/__init__.py +6 -6
- fin_infra/chat/planning.py +1 -2
- fin_infra/cli/cmds/scaffold_cmds.py +16 -17
- 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 +5 -4
- fin_infra/credit/add.py +6 -7
- fin_infra/credit/experian/auth.py +2 -2
- fin_infra/credit/experian/client.py +1 -1
- fin_infra/credit/experian/parser.py +5 -5
- fin_infra/credit/experian/provider.py +4 -4
- fin_infra/crypto/__init__.py +9 -11
- fin_infra/crypto/insights.py +4 -3
- fin_infra/documents/add.py +6 -8
- fin_infra/documents/analysis.py +9 -9
- fin_infra/documents/ease.py +14 -14
- fin_infra/documents/models.py +5 -6
- fin_infra/documents/ocr.py +7 -7
- fin_infra/documents/storage.py +21 -13
- fin_infra/exceptions.py +0 -1
- 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 +5 -6
- fin_infra/goals/milestones.py +7 -8
- fin_infra/goals/models.py +9 -13
- fin_infra/insights/__init__.py +6 -3
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +3 -3
- fin_infra/investments/add.py +23 -23
- fin_infra/investments/ease.py +2 -2
- fin_infra/investments/models.py +27 -29
- fin_infra/investments/providers/base.py +12 -13
- fin_infra/investments/providers/plaid.py +52 -26
- fin_infra/investments/providers/snaptrade.py +19 -19
- fin_infra/investments/scaffold_templates/README.md +17 -17
- fin_infra/markets/__init__.py +7 -5
- fin_infra/models/__init__.py +10 -10
- fin_infra/models/accounts.py +4 -5
- 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 -5
- fin_infra/net_worth/__init__.py +8 -1
- fin_infra/net_worth/aggregator.py +5 -3
- fin_infra/net_worth/calculator.py +1 -1
- fin_infra/net_worth/insights.py +7 -8
- fin_infra/normalization/__init__.py +4 -4
- fin_infra/normalization/currency_converter.py +7 -8
- fin_infra/normalization/models.py +9 -10
- fin_infra/normalization/providers/exchangerate.py +5 -5
- fin_infra/normalization/providers/static_mappings.py +1 -1
- fin_infra/normalization/symbol_resolver.py +3 -4
- fin_infra/obs/classifier.py +3 -3
- fin_infra/providers/banking/plaid_client.py +5 -5
- fin_infra/providers/banking/teller_client.py +7 -6
- fin_infra/providers/base.py +27 -2
- fin_infra/providers/brokerage/alpaca.py +4 -4
- fin_infra/providers/market/alphavantage.py +6 -11
- fin_infra/providers/market/ccxt_crypto.py +19 -3
- 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 +5 -5
- fin_infra/providers/tax/taxbit.py +1 -1
- fin_infra/recurring/__init__.py +6 -6
- fin_infra/recurring/add.py +6 -5
- fin_infra/recurring/detector.py +7 -7
- fin_infra/recurring/detectors_llm.py +10 -10
- fin_infra/recurring/ease.py +6 -8
- fin_infra/recurring/insights.py +25 -24
- fin_infra/recurring/normalizer.py +7 -7
- fin_infra/recurring/normalizers.py +31 -30
- fin_infra/recurring/summary.py +13 -15
- fin_infra/scaffold/budgets.py +9 -9
- fin_infra/scaffold/goals.py +9 -9
- fin_infra/security/__init__.py +8 -8
- fin_infra/security/add.py +1 -2
- fin_infra/security/audit.py +6 -7
- fin_infra/security/encryption.py +6 -6
- fin_infra/security/models.py +7 -7
- fin_infra/security/pii_filter.py +16 -16
- fin_infra/security/token_store.py +2 -3
- fin_infra/settings.py +2 -1
- fin_infra/tax/__init__.py +1 -1
- fin_infra/tax/add.py +5 -4
- fin_infra/tax/tlh.py +10 -10
- fin_infra/utils/__init__.py +15 -1
- fin_infra/utils/deprecation.py +161 -0
- fin_infra/utils/http.py +4 -3
- fin_infra/utils/retry.py +2 -1
- {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/METADATA +30 -16
- fin_infra-0.4.0.dist-info/RECORD +181 -0
- fin_infra-0.1.69.dist-info/RECORD +0 -180
- {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -18,15 +18,15 @@ These templates generate production-ready persistence code for investment holdin
|
|
|
18
18
|
## Why Historical Snapshots?
|
|
19
19
|
|
|
20
20
|
**Investment data providers (Plaid, SnapTrade, etc.) only provide current/live data:**
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
21
|
+
- [X] No historical portfolio values from past dates
|
|
22
|
+
- [X] No historical performance metrics
|
|
23
|
+
- [X] Cannot answer "What was my portfolio worth 3 months ago?"
|
|
24
24
|
|
|
25
25
|
**Solution: Store periodic snapshots in your database**
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
26
|
+
- [OK] Track portfolio value changes over time
|
|
27
|
+
- [OK] Calculate performance metrics (returns, growth)
|
|
28
|
+
- [OK] Show trend charts and historical analysis
|
|
29
|
+
- [OK] Works even if user disconnects provider
|
|
30
30
|
|
|
31
31
|
## Template Variables
|
|
32
32
|
|
|
@@ -148,12 +148,12 @@ async def capture_holdings_snapshot(
|
|
|
148
148
|
holdings_data: list[dict], # From Plaid/SnapTrade API
|
|
149
149
|
) -> None:
|
|
150
150
|
"""Capture current holdings as a snapshot for historical tracking."""
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
# Calculate aggregated metrics
|
|
153
153
|
total_value = sum(Decimal(str(h.get("institution_value", 0))) for h in holdings_data)
|
|
154
154
|
total_cost_basis = sum(Decimal(str(h.get("cost_basis", 0))) for h in holdings_data if h.get("cost_basis"))
|
|
155
155
|
total_unrealized_gain_loss = sum(Decimal(str(h.get("unrealized_gain_loss", 0))) for h in holdings_data if h.get("unrealized_gain_loss"))
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
# Create snapshot
|
|
158
158
|
service = create_holding_snapshot_service(session)
|
|
159
159
|
snapshot = await service.create(HoldingSnapshotCreate(
|
|
@@ -167,7 +167,7 @@ async def capture_holdings_snapshot(
|
|
|
167
167
|
provider="plaid", # or "snaptrade"
|
|
168
168
|
notes="Automatic daily snapshot"
|
|
169
169
|
))
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
await session.commit()
|
|
172
172
|
```
|
|
173
173
|
|
|
@@ -181,18 +181,18 @@ async def daily_holdings_snapshot():
|
|
|
181
181
|
"""Capture holdings snapshots for all users with investment accounts."""
|
|
182
182
|
from sqlalchemy import select
|
|
183
183
|
from my_app.models.user import User
|
|
184
|
-
|
|
184
|
+
|
|
185
185
|
async with AsyncSession(engine) as session:
|
|
186
186
|
# Get all users with Plaid/SnapTrade connections
|
|
187
187
|
stmt = select(User).where(User.banking_providers.isnot(None))
|
|
188
188
|
result = await session.execute(stmt)
|
|
189
189
|
users = result.scalars().all()
|
|
190
|
-
|
|
190
|
+
|
|
191
191
|
for user in users:
|
|
192
192
|
try:
|
|
193
193
|
# Fetch current holdings from provider
|
|
194
194
|
holdings = await fetch_holdings_from_provider(user)
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
# Create snapshot
|
|
197
197
|
await capture_holdings_snapshot(session, user.id, holdings)
|
|
198
198
|
except Exception as e:
|
|
@@ -299,10 +299,10 @@ async def get_portfolio_performance_data(user_id: str):
|
|
|
299
299
|
"""Get data for portfolio performance dashboard."""
|
|
300
300
|
async with AsyncSession(engine) as session:
|
|
301
301
|
repo = create_holding_snapshot_service(session)
|
|
302
|
-
|
|
302
|
+
|
|
303
303
|
# Get last 12 months trend
|
|
304
304
|
snapshots = await repo.get_trend(user_id=user_id, months=12)
|
|
305
|
-
|
|
305
|
+
|
|
306
306
|
# Calculate YTD performance
|
|
307
307
|
today = date.today()
|
|
308
308
|
year_start = date(today.year, 1, 1)
|
|
@@ -311,10 +311,10 @@ async def get_portfolio_performance_data(user_id: str):
|
|
|
311
311
|
start_date=year_start,
|
|
312
312
|
end_date=today
|
|
313
313
|
)
|
|
314
|
-
|
|
314
|
+
|
|
315
315
|
# Get latest snapshot
|
|
316
316
|
latest = await repo.get_latest(user_id=user_id)
|
|
317
|
-
|
|
317
|
+
|
|
318
318
|
return {
|
|
319
319
|
"current_value": latest.total_value if latest else 0,
|
|
320
320
|
"ytd_return": ytd_performance["percent_return"],
|
fin_infra/markets/__init__.py
CHANGED
|
@@ -20,7 +20,10 @@ if TYPE_CHECKING:
|
|
|
20
20
|
from fastapi import FastAPI
|
|
21
21
|
|
|
22
22
|
from ..providers.base import MarketDataProvider
|
|
23
|
-
|
|
23
|
+
|
|
24
|
+
# Deprecated: MarketDataClient alias for backward compatibility
|
|
25
|
+
# Use MarketDataProvider instead
|
|
26
|
+
MarketDataClient = MarketDataProvider # type: ignore[misc]
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
def easy_market(
|
|
@@ -30,8 +33,8 @@ def easy_market(
|
|
|
30
33
|
"""Create a market data provider with zero or minimal configuration.
|
|
31
34
|
|
|
32
35
|
Auto-detects provider based on environment variables:
|
|
33
|
-
1. If ALPHA_VANTAGE_API_KEY or ALPHAVANTAGE_API_KEY is set
|
|
34
|
-
2. Otherwise
|
|
36
|
+
1. If ALPHA_VANTAGE_API_KEY or ALPHAVANTAGE_API_KEY is set -> Alpha Vantage
|
|
37
|
+
2. Otherwise -> Yahoo Finance (no key needed)
|
|
35
38
|
|
|
36
39
|
Args:
|
|
37
40
|
provider: Provider name ("alphavantage" or "yahoo").
|
|
@@ -98,7 +101,7 @@ def easy_market(
|
|
|
98
101
|
|
|
99
102
|
|
|
100
103
|
def add_market_data(
|
|
101
|
-
app:
|
|
104
|
+
app: FastAPI,
|
|
102
105
|
*,
|
|
103
106
|
provider: str | MarketDataProvider | None = None,
|
|
104
107
|
prefix: str = "/market",
|
|
@@ -178,7 +181,6 @@ def add_market_data(
|
|
|
178
181
|
See Also:
|
|
179
182
|
- easy_market(): For standalone provider usage without FastAPI
|
|
180
183
|
- docs/market-data.md: API documentation and examples
|
|
181
|
-
- docs/adr/0004-market-data-integration.md: Architecture decisions
|
|
182
184
|
"""
|
|
183
185
|
from fastapi import HTTPException, Query
|
|
184
186
|
|
fin_infra/models/__init__.py
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
from .accounts import Account, AccountType
|
|
2
|
-
from .transactions import Transaction
|
|
3
|
-
from .quotes import Quote
|
|
4
|
-
from .money import Money
|
|
5
|
-
from .candle import Candle
|
|
6
|
-
from .brokerage import Order, Position, PortfolioHistory
|
|
7
2
|
from .brokerage import Account as BrokerageAccount # Avoid name conflict
|
|
3
|
+
from .brokerage import Order, PortfolioHistory, Position
|
|
4
|
+
from .candle import Candle
|
|
5
|
+
from .money import Money
|
|
6
|
+
from .quotes import Quote
|
|
8
7
|
from .tax import (
|
|
8
|
+
CryptoTaxReport,
|
|
9
|
+
CryptoTransaction,
|
|
9
10
|
TaxDocument,
|
|
10
|
-
TaxFormW2,
|
|
11
|
-
TaxForm1099INT,
|
|
12
|
-
TaxForm1099DIV,
|
|
13
11
|
TaxForm1099B,
|
|
12
|
+
TaxForm1099DIV,
|
|
13
|
+
TaxForm1099INT,
|
|
14
14
|
TaxForm1099MISC,
|
|
15
|
-
|
|
16
|
-
CryptoTaxReport,
|
|
15
|
+
TaxFormW2,
|
|
17
16
|
TaxLiability,
|
|
18
17
|
)
|
|
18
|
+
from .transactions import Transaction
|
|
19
19
|
|
|
20
20
|
__all__ = [
|
|
21
21
|
"Account",
|
fin_infra/models/accounts.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from decimal import Decimal
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
from pydantic import BaseModel, field_validator
|
|
8
7
|
|
|
@@ -26,11 +25,11 @@ class Account(BaseModel):
|
|
|
26
25
|
id: str
|
|
27
26
|
name: str
|
|
28
27
|
type: AccountType
|
|
29
|
-
mask:
|
|
28
|
+
mask: str | None = None
|
|
30
29
|
currency: str = "USD"
|
|
31
|
-
institution:
|
|
32
|
-
balance_available:
|
|
33
|
-
balance_current:
|
|
30
|
+
institution: str | None = None
|
|
31
|
+
balance_available: Decimal | None = None
|
|
32
|
+
balance_current: Decimal | None = None
|
|
34
33
|
|
|
35
34
|
@field_validator("balance_available", "balance_current", mode="before")
|
|
36
35
|
@classmethod
|
fin_infra/models/brokerage.py
CHANGED
fin_infra/models/candle.py
CHANGED
fin_infra/models/money.py
CHANGED
fin_infra/models/quotes.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from datetime import
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
4
|
from decimal import Decimal
|
|
5
|
+
|
|
5
6
|
from pydantic import BaseModel, field_validator
|
|
6
7
|
|
|
7
8
|
|
|
@@ -16,5 +17,5 @@ class Quote(BaseModel):
|
|
|
16
17
|
def _ensure_tzaware(cls, v: datetime) -> datetime:
|
|
17
18
|
# Normalize to timezone-aware (UTC) for consistency
|
|
18
19
|
if v.tzinfo is None:
|
|
19
|
-
return v.replace(tzinfo=
|
|
20
|
-
return v.astimezone(
|
|
20
|
+
return v.replace(tzinfo=UTC)
|
|
21
|
+
return v.astimezone(UTC)
|
fin_infra/models/tax.py
CHANGED
fin_infra/models/transactions.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import datetime
|
|
4
4
|
from decimal import Decimal
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
from pydantic import BaseModel, field_validator
|
|
8
7
|
|
|
@@ -16,11 +15,11 @@ class Transaction(BaseModel):
|
|
|
16
15
|
|
|
17
16
|
id: str
|
|
18
17
|
account_id: str
|
|
19
|
-
date: date
|
|
18
|
+
date: datetime.date
|
|
20
19
|
amount: Decimal
|
|
21
20
|
currency: str = "USD"
|
|
22
|
-
description:
|
|
23
|
-
category:
|
|
21
|
+
description: str | None = None
|
|
22
|
+
category: str | None = None
|
|
24
23
|
|
|
25
24
|
@field_validator("amount", mode="before")
|
|
26
25
|
@classmethod
|
fin_infra/net_worth/__init__.py
CHANGED
|
@@ -4,9 +4,16 @@ Net Worth Tracking Module
|
|
|
4
4
|
Calculates net worth by aggregating balances from multiple financial providers
|
|
5
5
|
(banking, brokerage, crypto) with historical snapshots and change detection.
|
|
6
6
|
|
|
7
|
+
**Feature Status**:
|
|
8
|
+
[OK] STABLE: Core calculation (works with provided data)
|
|
9
|
+
[OK] STABLE: Banking integration (Plaid, Teller)
|
|
10
|
+
[!] INTEGRATION: Brokerage integration (requires provider setup)
|
|
11
|
+
[!] INTEGRATION: Crypto integration (requires provider setup)
|
|
12
|
+
[!] INTEGRATION: Currency conversion (pass exchange_rate manually)
|
|
13
|
+
|
|
7
14
|
**Key Features**:
|
|
8
15
|
- Multi-provider aggregation (banking + brokerage + crypto)
|
|
9
|
-
- Currency normalization (all currencies
|
|
16
|
+
- Currency normalization (all currencies -> USD)
|
|
10
17
|
- Historical snapshots (daily at midnight UTC)
|
|
11
18
|
- Change detection (>5% or >$10k triggers webhook)
|
|
12
19
|
- Asset allocation breakdown (pie charts)
|
|
@@ -30,6 +30,7 @@ print(f"Net Worth: ${snapshot.total_net_worth:,.2f}")
|
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
import asyncio
|
|
33
|
+
import logging
|
|
33
34
|
import uuid
|
|
34
35
|
from datetime import datetime
|
|
35
36
|
from typing import Any
|
|
@@ -47,6 +48,8 @@ from fin_infra.net_worth.models import (
|
|
|
47
48
|
NetWorthSnapshot,
|
|
48
49
|
)
|
|
49
50
|
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
50
53
|
|
|
51
54
|
class NetWorthAggregator:
|
|
52
55
|
"""
|
|
@@ -56,7 +59,7 @@ class NetWorthAggregator:
|
|
|
56
59
|
- Multi-provider support (banking, brokerage, crypto)
|
|
57
60
|
- Parallel account fetching (faster performance)
|
|
58
61
|
- Graceful error handling (continue if one provider fails)
|
|
59
|
-
- Currency normalization (all
|
|
62
|
+
- Currency normalization (all -> base currency)
|
|
60
63
|
- Market value calculation (stocks/crypto)
|
|
61
64
|
|
|
62
65
|
**Example**:
|
|
@@ -219,8 +222,7 @@ class NetWorthAggregator:
|
|
|
219
222
|
|
|
220
223
|
for i, result in enumerate(results):
|
|
221
224
|
if isinstance(result, BaseException):
|
|
222
|
-
|
|
223
|
-
print(f"Provider {providers_used[i]} failed: {result}")
|
|
225
|
+
logger.warning("Provider %s failed: %s", providers_used[i], result)
|
|
224
226
|
continue
|
|
225
227
|
|
|
226
228
|
# result is now tuple[list[AssetDetail], list[LiabilityDetail]]
|
|
@@ -3,7 +3,7 @@ Net Worth Calculator Module
|
|
|
3
3
|
|
|
4
4
|
Provides core calculation functions for net worth tracking:
|
|
5
5
|
- Net worth calculation (assets - liabilities)
|
|
6
|
-
- Currency normalization (all currencies
|
|
6
|
+
- Currency normalization (all currencies -> base currency)
|
|
7
7
|
- Asset allocation breakdown
|
|
8
8
|
- Change detection (amount + percentage)
|
|
9
9
|
|
fin_infra/net_worth/insights.py
CHANGED
|
@@ -31,7 +31,6 @@ from typing import Any
|
|
|
31
31
|
|
|
32
32
|
from pydantic import BaseModel, Field
|
|
33
33
|
|
|
34
|
-
|
|
35
34
|
# ============================================================================
|
|
36
35
|
# Pydantic Schemas (Structured Output)
|
|
37
36
|
# ============================================================================
|
|
@@ -168,7 +167,7 @@ Be specific with numbers. Cite percentage changes and dollar amounts.
|
|
|
168
167
|
Focus on actionable insights, not generic advice.
|
|
169
168
|
|
|
170
169
|
Example 1:
|
|
171
|
-
User: Net worth: $500k
|
|
170
|
+
User: Net worth: $500k -> $575k over 6 months. Assets: +$65k (investments +$60k, savings +$5k). Liabilities: -$10k (new mortgage).
|
|
172
171
|
Response: {
|
|
173
172
|
"summary": "Net worth increased 15% ($75k) over 6 months, driven primarily by strong investment performance.",
|
|
174
173
|
"period": "6 months",
|
|
@@ -192,7 +191,7 @@ Response: {
|
|
|
192
191
|
}
|
|
193
192
|
|
|
194
193
|
Example 2:
|
|
195
|
-
User: Net worth: $100k
|
|
194
|
+
User: Net worth: $100k -> $95k over 3 months. Assets: -$2k (market down). Liabilities: +$3k (credit card debt).
|
|
196
195
|
Response: {
|
|
197
196
|
"summary": "Net worth decreased 5% ($5k) over 3 months due to market decline and rising credit card debt.",
|
|
198
197
|
"period": "3 months",
|
|
@@ -216,7 +215,7 @@ Response: {
|
|
|
216
215
|
"confidence": 0.89
|
|
217
216
|
}
|
|
218
217
|
|
|
219
|
-
|
|
218
|
+
[!] This is AI-generated advice. Not a substitute for a certified financial advisor.
|
|
220
219
|
Verify calculations independently. For personalized advice, consult a professional."""
|
|
221
220
|
|
|
222
221
|
DEBT_REDUCTION_SYSTEM_PROMPT = """You are a debt counselor using the avalanche method (highest APR first).
|
|
@@ -270,7 +269,7 @@ Response: {
|
|
|
270
269
|
"confidence": 0.98
|
|
271
270
|
}
|
|
272
271
|
|
|
273
|
-
|
|
272
|
+
[!] This is AI-generated advice. Not a substitute for a certified financial advisor.
|
|
274
273
|
Verify calculations independently. For personalized advice, consult a professional."""
|
|
275
274
|
|
|
276
275
|
GOAL_RECOMMENDATION_SYSTEM_PROMPT = """You are a financial planner validating goals and suggesting paths.
|
|
@@ -323,7 +322,7 @@ Response: {
|
|
|
323
322
|
"confidence": 0.89
|
|
324
323
|
}
|
|
325
324
|
|
|
326
|
-
|
|
325
|
+
[!] This is AI-generated advice. Not a substitute for a certified financial advisor.
|
|
327
326
|
Verify calculations independently. For personalized advice, consult a professional."""
|
|
328
327
|
|
|
329
328
|
ASSET_ALLOCATION_SYSTEM_PROMPT = """You are a portfolio advisor recommending asset allocation.
|
|
@@ -334,7 +333,7 @@ Given current allocation, age, and risk tolerance:
|
|
|
334
333
|
3. Provide specific rebalancing steps
|
|
335
334
|
|
|
336
335
|
Rule of thumb:
|
|
337
|
-
- Stock allocation = 100 - age (e.g., age 35
|
|
336
|
+
- Stock allocation = 100 - age (e.g., age 35 -> 65% stocks)
|
|
338
337
|
- Bonds for stability (increases with age)
|
|
339
338
|
- Cash for emergency fund (3-6 months expenses)
|
|
340
339
|
|
|
@@ -366,7 +365,7 @@ Response: {
|
|
|
366
365
|
"confidence": 0.91
|
|
367
366
|
}
|
|
368
367
|
|
|
369
|
-
|
|
368
|
+
[!] This is AI-generated advice. Not a substitute for a certified financial advisor.
|
|
370
369
|
Verify calculations independently. For personalized advice, consult a professional."""
|
|
371
370
|
|
|
372
371
|
|
|
@@ -54,7 +54,7 @@ def easy_normalization(
|
|
|
54
54
|
Example:
|
|
55
55
|
>>> from fin_infra.normalization import easy_normalization
|
|
56
56
|
>>> resolver, converter = easy_normalization()
|
|
57
|
-
>>> ticker = await resolver.to_ticker("037833100") # CUSIP
|
|
57
|
+
>>> ticker = await resolver.to_ticker("037833100") # CUSIP -> AAPL
|
|
58
58
|
>>> eur = await converter.convert(100, "USD", "EUR") # 92.0
|
|
59
59
|
"""
|
|
60
60
|
global _resolver_instance, _converter_instance
|
|
@@ -107,7 +107,7 @@ def add_normalization(
|
|
|
107
107
|
>>> resolver, converter = add_normalization(app)
|
|
108
108
|
>>>
|
|
109
109
|
>>> # Routes available:
|
|
110
|
-
>>> # GET /normalize/symbol/037833100
|
|
110
|
+
>>> # GET /normalize/symbol/037833100 -> {"ticker": "AAPL", ...}
|
|
111
111
|
>>> # GET /normalize/convert?amount=100&from_currency=USD&to_currency=EUR
|
|
112
112
|
|
|
113
113
|
Integration with svc-infra:
|
|
@@ -116,11 +116,11 @@ def add_normalization(
|
|
|
116
116
|
- Scoped docs at {prefix}/docs for standalone documentation
|
|
117
117
|
"""
|
|
118
118
|
# Import FastAPI dependencies
|
|
119
|
-
from fastapi import
|
|
119
|
+
from fastapi import HTTPException, Query
|
|
120
|
+
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
120
121
|
|
|
121
122
|
# Import svc-infra public router (no auth - utility endpoints)
|
|
122
123
|
from svc_infra.api.fastapi.dual.public import public_router
|
|
123
|
-
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
124
124
|
|
|
125
125
|
# Get normalization services
|
|
126
126
|
resolver, converter = easy_normalization(api_key=api_key)
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from datetime import date as DateType
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
from fin_infra.exceptions import CurrencyNotSupportedError, ExchangeRateAPIError
|
|
8
7
|
from fin_infra.normalization.models import CurrencyConversionResult
|
|
@@ -25,7 +24,7 @@ class CurrencyConverter:
|
|
|
25
24
|
Supports 160+ currencies including crypto (BTC, ETH).
|
|
26
25
|
"""
|
|
27
26
|
|
|
28
|
-
def __init__(self, api_key:
|
|
27
|
+
def __init__(self, api_key: str | None = None):
|
|
29
28
|
"""
|
|
30
29
|
Initialize currency converter.
|
|
31
30
|
|
|
@@ -39,7 +38,7 @@ class CurrencyConverter:
|
|
|
39
38
|
amount: float,
|
|
40
39
|
from_currency: str,
|
|
41
40
|
to_currency: str,
|
|
42
|
-
date:
|
|
41
|
+
date: DateType | None = None,
|
|
43
42
|
) -> float:
|
|
44
43
|
"""
|
|
45
44
|
Convert amount from one currency to another.
|
|
@@ -72,14 +71,14 @@ class CurrencyConverter:
|
|
|
72
71
|
except ExchangeRateAPIError as e:
|
|
73
72
|
logger.error(f"Failed to convert {from_currency} to {to_currency}: {e}")
|
|
74
73
|
raise CurrencyNotSupportedError(
|
|
75
|
-
f"Conversion failed: {from_currency}
|
|
74
|
+
f"Conversion failed: {from_currency} -> {to_currency}"
|
|
76
75
|
) from e
|
|
77
76
|
|
|
78
77
|
async def get_rate(
|
|
79
78
|
self,
|
|
80
79
|
from_currency: str,
|
|
81
80
|
to_currency: str,
|
|
82
|
-
date:
|
|
81
|
+
date: DateType | None = None,
|
|
83
82
|
) -> float:
|
|
84
83
|
"""
|
|
85
84
|
Get exchange rate between two currencies.
|
|
@@ -109,9 +108,9 @@ class CurrencyConverter:
|
|
|
109
108
|
return rate_data.rate
|
|
110
109
|
|
|
111
110
|
except ExchangeRateAPIError as e:
|
|
112
|
-
logger.error(f"Failed to get rate {from_currency}
|
|
111
|
+
logger.error(f"Failed to get rate {from_currency} -> {to_currency}: {e}")
|
|
113
112
|
raise CurrencyNotSupportedError(
|
|
114
|
-
f"Rate not available: {from_currency}
|
|
113
|
+
f"Rate not available: {from_currency} -> {to_currency}"
|
|
115
114
|
) from e
|
|
116
115
|
|
|
117
116
|
async def get_rates(self, base_currency: str = "USD") -> dict[str, float]:
|
|
@@ -142,7 +141,7 @@ class CurrencyConverter:
|
|
|
142
141
|
amount: float,
|
|
143
142
|
from_currency: str,
|
|
144
143
|
to_currency: str,
|
|
145
|
-
date:
|
|
144
|
+
date: DateType | None = None,
|
|
146
145
|
) -> CurrencyConversionResult:
|
|
147
146
|
"""
|
|
148
147
|
Convert amount with detailed result information.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Data models for normalization module."""
|
|
2
2
|
|
|
3
3
|
from datetime import date as DateType
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
from pydantic import BaseModel, Field
|
|
7
6
|
|
|
@@ -11,12 +10,12 @@ class SymbolMetadata(BaseModel):
|
|
|
11
10
|
|
|
12
11
|
ticker: str = Field(..., description="Standard ticker symbol")
|
|
13
12
|
name: str = Field(..., description="Company or asset name")
|
|
14
|
-
exchange:
|
|
15
|
-
cusip:
|
|
16
|
-
isin:
|
|
17
|
-
sector:
|
|
18
|
-
industry:
|
|
19
|
-
market_cap:
|
|
13
|
+
exchange: str | None = Field(None, description="Primary exchange (e.g., NASDAQ, NYSE)")
|
|
14
|
+
cusip: str | None = Field(None, description="CUSIP identifier")
|
|
15
|
+
isin: str | None = Field(None, description="ISIN identifier")
|
|
16
|
+
sector: str | None = Field(None, description="Business sector")
|
|
17
|
+
industry: str | None = Field(None, description="Industry classification")
|
|
18
|
+
market_cap: float | None = Field(None, description="Market capitalization in USD")
|
|
20
19
|
asset_type: str = Field(default="stock", description="Asset type: stock, etf, crypto, forex")
|
|
21
20
|
|
|
22
21
|
|
|
@@ -26,8 +25,8 @@ class ExchangeRate(BaseModel):
|
|
|
26
25
|
from_currency: str = Field(..., description="Source currency code (e.g., USD)")
|
|
27
26
|
to_currency: str = Field(..., description="Target currency code (e.g., EUR)")
|
|
28
27
|
rate: float = Field(..., description="Exchange rate (1 from_currency = rate to_currency)")
|
|
29
|
-
date:
|
|
30
|
-
timestamp:
|
|
28
|
+
date: DateType | None = Field(None, description="Rate date (None = current)")
|
|
29
|
+
timestamp: int | None = Field(None, description="Unix timestamp of rate")
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
class CurrencyConversionResult(BaseModel):
|
|
@@ -38,4 +37,4 @@ class CurrencyConversionResult(BaseModel):
|
|
|
38
37
|
to_currency: str = Field(..., description="Target currency")
|
|
39
38
|
converted: float = Field(..., description="Converted amount")
|
|
40
39
|
rate: float = Field(..., description="Exchange rate used")
|
|
41
|
-
date:
|
|
40
|
+
date: DateType | None = Field(None, description="Rate date")
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from datetime import date as DateType
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import cast
|
|
6
6
|
|
|
7
7
|
import httpx
|
|
8
8
|
|
|
@@ -19,7 +19,7 @@ __all__ = [
|
|
|
19
19
|
class ExchangeRateClient:
|
|
20
20
|
"""Client for exchangerate-api.io API."""
|
|
21
21
|
|
|
22
|
-
def __init__(self, api_key:
|
|
22
|
+
def __init__(self, api_key: str | None = None):
|
|
23
23
|
"""
|
|
24
24
|
Initialize exchange rate client.
|
|
25
25
|
|
|
@@ -66,10 +66,10 @@ class ExchangeRateClient:
|
|
|
66
66
|
raise ExchangeRateAPIError(
|
|
67
67
|
f"API returned error: {data.get('error-type', 'unknown')}"
|
|
68
68
|
)
|
|
69
|
-
return cast(dict[str, float], data["conversion_rates"])
|
|
69
|
+
return cast("dict[str, float]", data["conversion_rates"])
|
|
70
70
|
else:
|
|
71
71
|
# Free tier response format
|
|
72
|
-
return cast(dict[str, float], data["rates"])
|
|
72
|
+
return cast("dict[str, float]", data["rates"])
|
|
73
73
|
|
|
74
74
|
except httpx.HTTPError as e:
|
|
75
75
|
raise ExchangeRateAPIError(f"HTTP error fetching rates: {e}")
|
|
@@ -77,7 +77,7 @@ class ExchangeRateClient:
|
|
|
77
77
|
raise ExchangeRateAPIError(f"Invalid API response: {e}")
|
|
78
78
|
|
|
79
79
|
async def get_rate(
|
|
80
|
-
self, from_currency: str, to_currency: str, date:
|
|
80
|
+
self, from_currency: str, to_currency: str, date: DateType | None = None
|
|
81
81
|
) -> ExchangeRate:
|
|
82
82
|
"""
|
|
83
83
|
Get exchange rate between two currencies.
|
|
@@ -109,7 +109,7 @@ TICKER_TO_ISIN = {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
# Provider-specific symbol normalization
|
|
112
|
-
# Maps provider-specific format
|
|
112
|
+
# Maps provider-specific format -> standard ticker
|
|
113
113
|
PROVIDER_SYMBOL_MAP = {
|
|
114
114
|
"yahoo": {
|
|
115
115
|
# Yahoo Finance uses dashes for crypto
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Symbol resolver for converting between ticker formats."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
from fin_infra.exceptions import SymbolNotFoundError
|
|
7
6
|
from fin_infra.normalization.models import SymbolMetadata
|
|
@@ -231,9 +230,9 @@ class SymbolResolver:
|
|
|
231
230
|
def add_mapping(
|
|
232
231
|
self,
|
|
233
232
|
ticker: str,
|
|
234
|
-
cusip:
|
|
235
|
-
isin:
|
|
236
|
-
metadata:
|
|
233
|
+
cusip: str | None = None,
|
|
234
|
+
isin: str | None = None,
|
|
235
|
+
metadata: dict | None = None,
|
|
237
236
|
):
|
|
238
237
|
"""
|
|
239
238
|
Add or override a symbol mapping (useful for custom symbols).
|
fin_infra/obs/classifier.py
CHANGED
|
@@ -37,7 +37,7 @@ Usage:
|
|
|
37
37
|
|
|
38
38
|
from __future__ import annotations
|
|
39
39
|
|
|
40
|
-
from
|
|
40
|
+
from collections.abc import Callable
|
|
41
41
|
|
|
42
42
|
# Financial capability prefix patterns (extensible)
|
|
43
43
|
FINANCIAL_ROUTE_PREFIXES = (
|
|
@@ -63,8 +63,8 @@ def financial_route_classifier(route_path: str, method: str) -> str:
|
|
|
63
63
|
svc-infra's add_observability route_classifier parameter.
|
|
64
64
|
|
|
65
65
|
Classification Logic:
|
|
66
|
-
- Financial routes (e.g., /banking/*, /market/*)
|
|
67
|
-
- All other routes
|
|
66
|
+
- Financial routes (e.g., /banking/*, /market/*) -> "financial"
|
|
67
|
+
- All other routes -> "public"
|
|
68
68
|
|
|
69
69
|
This allows Grafana dashboards to split metrics by route class:
|
|
70
70
|
- Filter by route_class="financial" for financial provider SLOs
|