fin-infra 0.1.69__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 +21 -21
- fin_infra/analytics/ease.py +19 -20
- fin_infra/analytics/portfolio.py +6 -6
- fin_infra/analytics/projections.py +1 -3
- fin_infra/banking/__init__.py +27 -28
- fin_infra/banking/history.py +8 -9
- fin_infra/banking/utils.py +27 -26
- fin_infra/brokerage/__init__.py +22 -24
- 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 +2 -2
- fin_infra/cashflows/__init__.py +3 -3
- fin_infra/cashflows/core.py +1 -1
- fin_infra/categorization/add.py +2 -3
- fin_infra/categorization/engine.py +17 -14
- fin_infra/categorization/llm_layer.py +7 -6
- 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 +0 -1
- fin_infra/cli/cmds/scaffold_cmds.py +10 -11
- 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/provider.py +4 -4
- fin_infra/crypto/__init__.py +7 -9
- 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 +4 -4
- 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 +2 -3
- fin_infra/goals/milestones.py +1 -2
- fin_infra/goals/models.py +7 -11
- fin_infra/insights/__init__.py +6 -3
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +1 -1
- fin_infra/investments/add.py +23 -23
- fin_infra/investments/models.py +5 -5
- fin_infra/investments/providers/base.py +8 -9
- fin_infra/investments/providers/plaid.py +52 -26
- fin_infra/investments/providers/snaptrade.py +19 -19
- fin_infra/markets/__init__.py +5 -3
- fin_infra/models/__init__.py +10 -10
- 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 +3 -4
- fin_infra/net_worth/__init__.py +7 -0
- fin_infra/net_worth/aggregator.py +4 -2
- fin_infra/net_worth/insights.py +0 -1
- fin_infra/normalization/__init__.py +2 -2
- fin_infra/normalization/providers/exchangerate.py +5 -5
- fin_infra/obs/classifier.py +1 -1
- 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 +3 -3
- 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 +5 -4
- fin_infra/recurring/detector.py +7 -7
- fin_infra/recurring/detectors_llm.py +6 -6
- fin_infra/recurring/ease.py +2 -4
- fin_infra/recurring/insights.py +13 -13
- fin_infra/recurring/normalizer.py +1 -1
- fin_infra/recurring/normalizers.py +4 -4
- fin_infra/recurring/summary.py +13 -15
- 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/settings.py +2 -1
- fin_infra/tax/__init__.py +1 -1
- fin_infra/tax/add.py +3 -2
- fin_infra/tax/tlh.py +5 -5
- fin_infra/utils/http.py +4 -3
- fin_infra/utils/retry.py +2 -1
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -8
- fin_infra-0.1.82.dist-info/RECORD +180 -0
- fin_infra-0.1.69.dist-info/RECORD +0 -180
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
fin_infra/investments/add.py
CHANGED
|
@@ -7,7 +7,7 @@ transactions, accounts, allocation, and securities data.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from datetime import date
|
|
10
|
-
from typing import TYPE_CHECKING
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
12
|
from fastapi import HTTPException
|
|
13
13
|
from pydantic import BaseModel, Field
|
|
@@ -24,10 +24,10 @@ except ImportError:
|
|
|
24
24
|
|
|
25
25
|
from .ease import easy_investments
|
|
26
26
|
from .models import (
|
|
27
|
+
AssetAllocation,
|
|
27
28
|
Holding,
|
|
28
|
-
InvestmentTransaction,
|
|
29
29
|
InvestmentAccount,
|
|
30
|
-
|
|
30
|
+
InvestmentTransaction,
|
|
31
31
|
Security,
|
|
32
32
|
)
|
|
33
33
|
from .providers.base import InvestmentProvider
|
|
@@ -37,55 +37,55 @@ from .providers.base import InvestmentProvider
|
|
|
37
37
|
class HoldingsRequest(BaseModel):
|
|
38
38
|
"""Request model for holdings endpoint."""
|
|
39
39
|
|
|
40
|
-
access_token:
|
|
41
|
-
user_id:
|
|
42
|
-
user_secret:
|
|
43
|
-
account_ids:
|
|
40
|
+
access_token: str | None = Field(None, description="Plaid access token (Plaid only)")
|
|
41
|
+
user_id: str | None = Field(None, description="SnapTrade user ID (SnapTrade only)")
|
|
42
|
+
user_secret: str | None = Field(None, description="SnapTrade user secret (SnapTrade only)")
|
|
43
|
+
account_ids: list[str] | None = Field(None, description="Filter by specific account IDs")
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
class TransactionsRequest(BaseModel):
|
|
47
47
|
"""Request model for transactions endpoint."""
|
|
48
48
|
|
|
49
|
-
access_token:
|
|
50
|
-
user_id:
|
|
51
|
-
user_secret:
|
|
49
|
+
access_token: str | None = Field(None, description="Plaid access token (Plaid only)")
|
|
50
|
+
user_id: str | None = Field(None, description="SnapTrade user ID (SnapTrade only)")
|
|
51
|
+
user_secret: str | None = Field(None, description="SnapTrade user secret (SnapTrade only)")
|
|
52
52
|
start_date: date = Field(..., description="Start date for transactions (YYYY-MM-DD)")
|
|
53
53
|
end_date: date = Field(..., description="End date for transactions (YYYY-MM-DD)")
|
|
54
|
-
account_ids:
|
|
54
|
+
account_ids: list[str] | None = Field(None, description="Filter by specific account IDs")
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
class AccountsRequest(BaseModel):
|
|
58
58
|
"""Request model for investment accounts endpoint."""
|
|
59
59
|
|
|
60
|
-
access_token:
|
|
61
|
-
user_id:
|
|
62
|
-
user_secret:
|
|
60
|
+
access_token: str | None = Field(None, description="Plaid access token (Plaid only)")
|
|
61
|
+
user_id: str | None = Field(None, description="SnapTrade user ID (SnapTrade only)")
|
|
62
|
+
user_secret: str | None = Field(None, description="SnapTrade user secret (SnapTrade only)")
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
class AllocationRequest(BaseModel):
|
|
66
66
|
"""Request model for asset allocation endpoint."""
|
|
67
67
|
|
|
68
|
-
access_token:
|
|
69
|
-
user_id:
|
|
70
|
-
user_secret:
|
|
71
|
-
account_ids:
|
|
68
|
+
access_token: str | None = Field(None, description="Plaid access token (Plaid only)")
|
|
69
|
+
user_id: str | None = Field(None, description="SnapTrade user ID (SnapTrade only)")
|
|
70
|
+
user_secret: str | None = Field(None, description="SnapTrade user secret (SnapTrade only)")
|
|
71
|
+
account_ids: list[str] | None = Field(None, description="Filter by specific account IDs")
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
class SecuritiesRequest(BaseModel):
|
|
75
75
|
"""Request model for securities endpoint."""
|
|
76
76
|
|
|
77
|
-
access_token:
|
|
78
|
-
user_id:
|
|
79
|
-
user_secret:
|
|
77
|
+
access_token: str | None = Field(None, description="Plaid access token (Plaid only)")
|
|
78
|
+
user_id: str | None = Field(None, description="SnapTrade user ID (SnapTrade only)")
|
|
79
|
+
user_secret: str | None = Field(None, description="SnapTrade user secret (SnapTrade only)")
|
|
80
80
|
security_ids: list[str] = Field(..., description="List of security IDs to retrieve")
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
def add_investments(
|
|
84
84
|
app: FastAPI,
|
|
85
85
|
prefix: str = "/investments",
|
|
86
|
-
provider:
|
|
86
|
+
provider: InvestmentProvider | None = None,
|
|
87
87
|
include_in_schema: bool = True,
|
|
88
|
-
tags:
|
|
88
|
+
tags: list[str] | None = None,
|
|
89
89
|
) -> InvestmentProvider:
|
|
90
90
|
"""Add investment endpoints to FastAPI application.
|
|
91
91
|
|
fin_infra/investments/models.py
CHANGED
|
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
from datetime import date
|
|
21
21
|
from decimal import Decimal
|
|
22
22
|
from enum import Enum
|
|
23
|
-
from typing import TYPE_CHECKING,
|
|
23
|
+
from typing import TYPE_CHECKING, Optional
|
|
24
24
|
|
|
25
25
|
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
26
26
|
|
|
@@ -374,12 +374,12 @@ class InvestmentAccount(BaseModel):
|
|
|
374
374
|
subtype: Optional[str] = Field(None, description="Account subtype (401k, ira, brokerage)")
|
|
375
375
|
|
|
376
376
|
# Balances
|
|
377
|
-
balances:
|
|
377
|
+
balances: dict[str, Optional[Decimal]] = Field(
|
|
378
378
|
..., description="Current, available, and limit balances"
|
|
379
379
|
)
|
|
380
380
|
|
|
381
381
|
# Holdings
|
|
382
|
-
holdings:
|
|
382
|
+
holdings: list[Holding] = Field(default_factory=list, description="List of holdings in account")
|
|
383
383
|
|
|
384
384
|
if TYPE_CHECKING:
|
|
385
385
|
|
|
@@ -487,11 +487,11 @@ class AssetAllocation(BaseModel):
|
|
|
487
487
|
},
|
|
488
488
|
)
|
|
489
489
|
|
|
490
|
-
by_security_type:
|
|
490
|
+
by_security_type: dict[SecurityType, float] = Field(
|
|
491
491
|
default_factory=dict,
|
|
492
492
|
description="Percentage breakdown by security type (equity, bond, etc.)",
|
|
493
493
|
)
|
|
494
|
-
by_sector:
|
|
494
|
+
by_sector: dict[str, float] = Field(
|
|
495
495
|
default_factory=dict,
|
|
496
496
|
description="Percentage breakdown by sector (Technology, Healthcare, etc.)",
|
|
497
497
|
)
|
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from datetime import date
|
|
7
|
-
from typing import List, Optional
|
|
8
7
|
|
|
9
8
|
# Import will work once models.py is fully implemented in Task 3
|
|
10
9
|
# For now, using TYPE_CHECKING to avoid circular imports
|
|
@@ -30,8 +29,8 @@ class InvestmentProvider(ABC):
|
|
|
30
29
|
|
|
31
30
|
@abstractmethod
|
|
32
31
|
async def get_holdings(
|
|
33
|
-
self, access_token: str, account_ids:
|
|
34
|
-
) ->
|
|
32
|
+
self, access_token: str, account_ids: list[str] | None = None
|
|
33
|
+
) -> list[Holding]:
|
|
35
34
|
"""Fetch holdings for investment accounts.
|
|
36
35
|
|
|
37
36
|
Args:
|
|
@@ -54,8 +53,8 @@ class InvestmentProvider(ABC):
|
|
|
54
53
|
access_token: str,
|
|
55
54
|
start_date: date,
|
|
56
55
|
end_date: date,
|
|
57
|
-
account_ids:
|
|
58
|
-
) ->
|
|
56
|
+
account_ids: list[str] | None = None,
|
|
57
|
+
) -> list[InvestmentTransaction]:
|
|
59
58
|
"""Fetch investment transactions within date range.
|
|
60
59
|
|
|
61
60
|
Args:
|
|
@@ -77,7 +76,7 @@ class InvestmentProvider(ABC):
|
|
|
77
76
|
pass
|
|
78
77
|
|
|
79
78
|
@abstractmethod
|
|
80
|
-
async def get_securities(self, access_token: str, security_ids:
|
|
79
|
+
async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
|
|
81
80
|
"""Fetch security details (ticker, name, type, current price).
|
|
82
81
|
|
|
83
82
|
Args:
|
|
@@ -95,7 +94,7 @@ class InvestmentProvider(ABC):
|
|
|
95
94
|
pass
|
|
96
95
|
|
|
97
96
|
@abstractmethod
|
|
98
|
-
async def get_investment_accounts(self, access_token: str) ->
|
|
97
|
+
async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
|
|
99
98
|
"""Fetch investment accounts with aggregated holdings.
|
|
100
99
|
|
|
101
100
|
Args:
|
|
@@ -113,7 +112,7 @@ class InvestmentProvider(ABC):
|
|
|
113
112
|
|
|
114
113
|
# Helper methods (concrete - shared across all providers)
|
|
115
114
|
|
|
116
|
-
def calculate_allocation(self, holdings:
|
|
115
|
+
def calculate_allocation(self, holdings: list[Holding]) -> AssetAllocation:
|
|
117
116
|
"""Calculate asset allocation by security type and sector.
|
|
118
117
|
|
|
119
118
|
Groups holdings by security type (equity, bond, ETF, etc.) and calculates
|
|
@@ -183,7 +182,7 @@ class InvestmentProvider(ABC):
|
|
|
183
182
|
cash_percent=cash_percent,
|
|
184
183
|
)
|
|
185
184
|
|
|
186
|
-
def calculate_portfolio_metrics(self, holdings:
|
|
185
|
+
def calculate_portfolio_metrics(self, holdings: list[Holding]) -> dict:
|
|
187
186
|
"""Calculate total value, cost basis, unrealized gain/loss.
|
|
188
187
|
|
|
189
188
|
Aggregates holdings to calculate portfolio-level metrics.
|
|
@@ -10,21 +10,35 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from datetime import date
|
|
12
12
|
from decimal import Decimal
|
|
13
|
-
from typing import Any,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
from plaid.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
from plaid.
|
|
21
|
-
from plaid.model.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
from plaid.
|
|
27
|
-
|
|
13
|
+
from typing import Any, cast
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import plaid
|
|
17
|
+
from plaid.api import plaid_api
|
|
18
|
+
from plaid.api_client import ApiClient
|
|
19
|
+
from plaid.configuration import Configuration
|
|
20
|
+
from plaid.exceptions import ApiException
|
|
21
|
+
from plaid.model.investments_holdings_get_request import InvestmentsHoldingsGetRequest
|
|
22
|
+
from plaid.model.investments_holdings_get_response import InvestmentsHoldingsGetResponse
|
|
23
|
+
from plaid.model.investments_transactions_get_request import (
|
|
24
|
+
InvestmentsTransactionsGetRequest,
|
|
25
|
+
)
|
|
26
|
+
from plaid.model.investments_transactions_get_response import (
|
|
27
|
+
InvestmentsTransactionsGetResponse,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
HAS_PLAID = True
|
|
31
|
+
except ImportError: # pragma: no cover
|
|
32
|
+
HAS_PLAID = False
|
|
33
|
+
plaid_api = None
|
|
34
|
+
plaid = None
|
|
35
|
+
ApiClient = None
|
|
36
|
+
Configuration = None
|
|
37
|
+
ApiException = Exception
|
|
38
|
+
InvestmentsHoldingsGetRequest = Any
|
|
39
|
+
InvestmentsTransactionsGetRequest = Any
|
|
40
|
+
InvestmentsHoldingsGetResponse = Any
|
|
41
|
+
InvestmentsTransactionsGetResponse = Any
|
|
28
42
|
|
|
29
43
|
from ..models import (
|
|
30
44
|
Holding,
|
|
@@ -36,6 +50,15 @@ from ..models import (
|
|
|
36
50
|
from .base import InvestmentProvider
|
|
37
51
|
|
|
38
52
|
|
|
53
|
+
def _require_plaid() -> None:
|
|
54
|
+
"""Raise ImportError if plaid-python is not installed."""
|
|
55
|
+
if not HAS_PLAID:
|
|
56
|
+
raise ImportError(
|
|
57
|
+
"Plaid support requires the 'plaid-python' package. "
|
|
58
|
+
"Install with: pip install fin-infra[plaid] or pip install fin-infra[banking]"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
39
62
|
class PlaidInvestmentProvider(InvestmentProvider):
|
|
40
63
|
"""Plaid Investment API provider.
|
|
41
64
|
|
|
@@ -76,7 +99,10 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
76
99
|
|
|
77
100
|
Raises:
|
|
78
101
|
ValueError: If client_id or secret is missing
|
|
102
|
+
ImportError: If plaid-python is not installed
|
|
79
103
|
"""
|
|
104
|
+
_require_plaid()
|
|
105
|
+
|
|
80
106
|
if not client_id or not secret:
|
|
81
107
|
raise ValueError("client_id and secret are required for Plaid provider")
|
|
82
108
|
|
|
@@ -102,11 +128,11 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
102
128
|
"development": plaid.Environment.Sandbox, # Map development to sandbox
|
|
103
129
|
"production": plaid.Environment.Production,
|
|
104
130
|
}
|
|
105
|
-
return cast(str, hosts.get(environment.lower(), plaid.Environment.Sandbox))
|
|
131
|
+
return cast("str", hosts.get(environment.lower(), plaid.Environment.Sandbox))
|
|
106
132
|
|
|
107
133
|
async def get_holdings(
|
|
108
|
-
self, access_token: str, account_ids:
|
|
109
|
-
) ->
|
|
134
|
+
self, access_token: str, account_ids: list[str] | None = None
|
|
135
|
+
) -> list[Holding]:
|
|
110
136
|
"""Fetch investment holdings from Plaid.
|
|
111
137
|
|
|
112
138
|
Retrieves holdings with security details, quantity, cost basis, and current value.
|
|
@@ -163,8 +189,8 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
163
189
|
access_token: str,
|
|
164
190
|
start_date: date,
|
|
165
191
|
end_date: date,
|
|
166
|
-
account_ids:
|
|
167
|
-
) ->
|
|
192
|
+
account_ids: list[str] | None = None,
|
|
193
|
+
) -> list[InvestmentTransaction]:
|
|
168
194
|
"""Fetch investment transactions from Plaid.
|
|
169
195
|
|
|
170
196
|
Retrieves buy/sell/dividend transactions within the specified date range.
|
|
@@ -226,7 +252,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
226
252
|
except ApiException as e:
|
|
227
253
|
raise self._transform_error(e)
|
|
228
254
|
|
|
229
|
-
async def get_securities(self, access_token: str, security_ids:
|
|
255
|
+
async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
|
|
230
256
|
"""Fetch security details from Plaid holdings.
|
|
231
257
|
|
|
232
258
|
Note: Plaid doesn't have a dedicated securities endpoint.
|
|
@@ -264,7 +290,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
264
290
|
except ApiException as e:
|
|
265
291
|
raise self._transform_error(e)
|
|
266
292
|
|
|
267
|
-
async def get_investment_accounts(self, access_token: str) ->
|
|
293
|
+
async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
|
|
268
294
|
"""Fetch investment accounts with aggregated holdings.
|
|
269
295
|
|
|
270
296
|
Returns accounts with total value, cost basis, and unrealized P&L.
|
|
@@ -295,7 +321,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
295
321
|
}
|
|
296
322
|
|
|
297
323
|
# Group holdings by account
|
|
298
|
-
accounts_map:
|
|
324
|
+
accounts_map: dict[str, dict[str, Any]] = {}
|
|
299
325
|
for plaid_holding in response.holdings:
|
|
300
326
|
holding_dict = plaid_holding.to_dict()
|
|
301
327
|
account_id = holding_dict["account_id"]
|
|
@@ -347,7 +373,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
347
373
|
|
|
348
374
|
# Helper methods for data transformation
|
|
349
375
|
|
|
350
|
-
def _transform_security(self, plaid_security:
|
|
376
|
+
def _transform_security(self, plaid_security: dict[str, Any]) -> Security:
|
|
351
377
|
"""Transform Plaid security data to Security model."""
|
|
352
378
|
# Handle close_price - Plaid may return None for securities without recent pricing
|
|
353
379
|
close_price_raw = plaid_security.get("close_price")
|
|
@@ -368,7 +394,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
368
394
|
currency=plaid_security.get("iso_currency_code", "USD"),
|
|
369
395
|
)
|
|
370
396
|
|
|
371
|
-
def _transform_holding(self, plaid_holding:
|
|
397
|
+
def _transform_holding(self, plaid_holding: dict[str, Any], security: Security) -> Holding:
|
|
372
398
|
"""Transform Plaid holding data to Holding model."""
|
|
373
399
|
return Holding(
|
|
374
400
|
account_id=plaid_holding["account_id"],
|
|
@@ -384,7 +410,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
384
410
|
)
|
|
385
411
|
|
|
386
412
|
def _transform_transaction(
|
|
387
|
-
self, plaid_transaction:
|
|
413
|
+
self, plaid_transaction: dict[str, Any], security: Security
|
|
388
414
|
) -> InvestmentTransaction:
|
|
389
415
|
"""Transform Plaid investment transaction to InvestmentTransaction model."""
|
|
390
416
|
# Map Plaid transaction type to our enum
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from datetime import date
|
|
13
13
|
from decimal import Decimal
|
|
14
|
-
from typing import Any,
|
|
14
|
+
from typing import Any, cast
|
|
15
15
|
|
|
16
16
|
import httpx
|
|
17
17
|
|
|
@@ -93,7 +93,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
93
93
|
timeout=30.0,
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
def _auth_headers(self, user_id: str, user_secret: str) ->
|
|
96
|
+
def _auth_headers(self, user_id: str, user_secret: str) -> dict[str, str]:
|
|
97
97
|
"""Build authentication headers for SnapTrade API requests.
|
|
98
98
|
|
|
99
99
|
SECURITY: User secrets are passed in headers, NOT URL params.
|
|
@@ -115,8 +115,8 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
115
115
|
async def get_holdings(
|
|
116
116
|
self,
|
|
117
117
|
access_token: str,
|
|
118
|
-
account_ids:
|
|
119
|
-
) ->
|
|
118
|
+
account_ids: list[str] | None = None,
|
|
119
|
+
) -> list[Holding]:
|
|
120
120
|
"""Fetch investment holdings from SnapTrade.
|
|
121
121
|
|
|
122
122
|
Note: SnapTrade uses user_id + user_secret, passed as access_token in format "user_id:user_secret"
|
|
@@ -171,15 +171,15 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
171
171
|
except httpx.HTTPStatusError as e:
|
|
172
172
|
raise self._transform_error(e)
|
|
173
173
|
except Exception as e:
|
|
174
|
-
raise ValueError(f"SnapTrade API error: {
|
|
174
|
+
raise ValueError(f"SnapTrade API error: {e!s}")
|
|
175
175
|
|
|
176
176
|
async def get_transactions(
|
|
177
177
|
self,
|
|
178
178
|
access_token: str,
|
|
179
179
|
start_date: date,
|
|
180
180
|
end_date: date,
|
|
181
|
-
account_ids:
|
|
182
|
-
) ->
|
|
181
|
+
account_ids: list[str] | None = None,
|
|
182
|
+
) -> list[InvestmentTransaction]:
|
|
183
183
|
"""Fetch investment transactions from SnapTrade.
|
|
184
184
|
|
|
185
185
|
Args:
|
|
@@ -244,9 +244,9 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
244
244
|
except httpx.HTTPStatusError as e:
|
|
245
245
|
raise self._transform_error(e)
|
|
246
246
|
except Exception as e:
|
|
247
|
-
raise ValueError(f"SnapTrade API error: {
|
|
247
|
+
raise ValueError(f"SnapTrade API error: {e!s}")
|
|
248
248
|
|
|
249
|
-
async def get_securities(self, access_token: str, security_ids:
|
|
249
|
+
async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
|
|
250
250
|
"""Fetch security details from SnapTrade positions.
|
|
251
251
|
|
|
252
252
|
Note: SnapTrade doesn't have a dedicated securities endpoint.
|
|
@@ -267,7 +267,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
267
267
|
>>> for security in securities:
|
|
268
268
|
... print(f"{security.ticker_symbol}: ${security.close_price}")
|
|
269
269
|
"""
|
|
270
|
-
|
|
270
|
+
_user_id, _user_secret = self._parse_access_token(access_token)
|
|
271
271
|
|
|
272
272
|
try:
|
|
273
273
|
# Get all holdings to extract securities
|
|
@@ -282,9 +282,9 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
282
282
|
return list(securities_map.values())
|
|
283
283
|
|
|
284
284
|
except Exception as e:
|
|
285
|
-
raise ValueError(f"SnapTrade API error: {
|
|
285
|
+
raise ValueError(f"SnapTrade API error: {e!s}")
|
|
286
286
|
|
|
287
|
-
async def get_investment_accounts(self, access_token: str) ->
|
|
287
|
+
async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
|
|
288
288
|
"""Fetch investment accounts with aggregated holdings.
|
|
289
289
|
|
|
290
290
|
Returns accounts with total value, cost basis, and unrealized P&L.
|
|
@@ -356,9 +356,9 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
356
356
|
except httpx.HTTPStatusError as e:
|
|
357
357
|
raise self._transform_error(e)
|
|
358
358
|
except Exception as e:
|
|
359
|
-
raise ValueError(f"SnapTrade API error: {
|
|
359
|
+
raise ValueError(f"SnapTrade API error: {e!s}")
|
|
360
360
|
|
|
361
|
-
async def list_connections(self, access_token: str) ->
|
|
361
|
+
async def list_connections(self, access_token: str) -> list[dict[str, Any]]:
|
|
362
362
|
"""List brokerage connections for a user.
|
|
363
363
|
|
|
364
364
|
Returns which brokerages the user has connected (E*TRADE, Robinhood, etc.).
|
|
@@ -381,14 +381,14 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
381
381
|
url = f"{self.base_url}/connections"
|
|
382
382
|
response = await self.client.get(url, headers=auth_headers)
|
|
383
383
|
response.raise_for_status()
|
|
384
|
-
return cast(list[dict[str, Any]], await response.json())
|
|
384
|
+
return cast("list[dict[str, Any]]", await response.json())
|
|
385
385
|
|
|
386
386
|
except httpx.HTTPStatusError as e:
|
|
387
387
|
raise self._transform_error(e)
|
|
388
388
|
except Exception as e:
|
|
389
|
-
raise ValueError(f"SnapTrade API error: {
|
|
389
|
+
raise ValueError(f"SnapTrade API error: {e!s}")
|
|
390
390
|
|
|
391
|
-
def get_brokerage_capabilities(self, brokerage_name: str) ->
|
|
391
|
+
def get_brokerage_capabilities(self, brokerage_name: str) -> dict[str, Any]:
|
|
392
392
|
"""Get capabilities for a specific brokerage.
|
|
393
393
|
|
|
394
394
|
Important: Robinhood is READ-ONLY (no trading support).
|
|
@@ -480,7 +480,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
480
480
|
except ValueError:
|
|
481
481
|
raise ValueError("Invalid access_token format. Expected 'user_id:user_secret'")
|
|
482
482
|
|
|
483
|
-
def _transform_holding(self, snaptrade_position:
|
|
483
|
+
def _transform_holding(self, snaptrade_position: dict[str, Any], account_id: str) -> Holding:
|
|
484
484
|
"""Transform SnapTrade position data to Holding model."""
|
|
485
485
|
symbol_data = snaptrade_position.get("symbol", {})
|
|
486
486
|
|
|
@@ -510,7 +510,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
510
510
|
)
|
|
511
511
|
|
|
512
512
|
def _transform_transaction(
|
|
513
|
-
self, snaptrade_tx:
|
|
513
|
+
self, snaptrade_tx: dict[str, Any], account_id: str
|
|
514
514
|
) -> InvestmentTransaction:
|
|
515
515
|
"""Transform SnapTrade transaction to InvestmentTransaction model."""
|
|
516
516
|
symbol_data = snaptrade_tx.get("symbol", {})
|
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(
|
|
@@ -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/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
|
-
from datetime import date
|
|
3
|
+
from datetime import date
|
|
4
4
|
from decimal import Decimal
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
from pydantic import BaseModel, field_validator
|
|
8
7
|
|
|
@@ -19,8 +18,8 @@ class Transaction(BaseModel):
|
|
|
19
18
|
date: 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,6 +4,13 @@ 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
|
+
✅ STABLE: Core calculation (works with provided data)
|
|
9
|
+
✅ 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
16
|
- Currency normalization (all currencies → USD)
|