fin-infra 0.1.82__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.
Files changed (65) hide show
  1. fin_infra/analytics/__init__.py +2 -2
  2. fin_infra/analytics/add.py +3 -3
  3. fin_infra/analytics/cash_flow.py +3 -3
  4. fin_infra/analytics/models.py +5 -5
  5. fin_infra/analytics/portfolio.py +12 -12
  6. fin_infra/analytics/spending.py +4 -5
  7. fin_infra/banking/history.py +4 -4
  8. fin_infra/brokerage/__init__.py +7 -7
  9. fin_infra/budgets/alerts.py +2 -2
  10. fin_infra/budgets/ease.py +1 -2
  11. fin_infra/budgets/models.py +1 -2
  12. fin_infra/budgets/templates.py +4 -4
  13. fin_infra/budgets/tracker.py +3 -3
  14. fin_infra/categorization/__init__.py +1 -1
  15. fin_infra/categorization/ease.py +3 -3
  16. fin_infra/categorization/engine.py +1 -1
  17. fin_infra/categorization/llm_layer.py +6 -4
  18. fin_infra/categorization/models.py +3 -4
  19. fin_infra/chat/planning.py +1 -1
  20. fin_infra/cli/cmds/scaffold_cmds.py +6 -6
  21. fin_infra/credit/experian/parser.py +5 -5
  22. fin_infra/crypto/__init__.py +2 -2
  23. fin_infra/documents/models.py +2 -3
  24. fin_infra/goals/management.py +3 -3
  25. fin_infra/goals/milestones.py +6 -6
  26. fin_infra/goals/models.py +2 -2
  27. fin_infra/investments/__init__.py +2 -2
  28. fin_infra/investments/ease.py +2 -2
  29. fin_infra/investments/models.py +24 -26
  30. fin_infra/investments/providers/base.py +4 -4
  31. fin_infra/investments/scaffold_templates/README.md +17 -17
  32. fin_infra/markets/__init__.py +2 -2
  33. fin_infra/models/accounts.py +4 -5
  34. fin_infra/models/transactions.py +2 -2
  35. fin_infra/net_worth/__init__.py +6 -6
  36. fin_infra/net_worth/aggregator.py +1 -1
  37. fin_infra/net_worth/calculator.py +1 -1
  38. fin_infra/net_worth/insights.py +7 -7
  39. fin_infra/normalization/__init__.py +2 -2
  40. fin_infra/normalization/currency_converter.py +7 -8
  41. fin_infra/normalization/models.py +9 -10
  42. fin_infra/normalization/providers/static_mappings.py +1 -1
  43. fin_infra/normalization/symbol_resolver.py +3 -4
  44. fin_infra/obs/classifier.py +2 -2
  45. fin_infra/providers/brokerage/alpaca.py +1 -1
  46. fin_infra/recurring/add.py +1 -1
  47. fin_infra/recurring/detectors_llm.py +5 -5
  48. fin_infra/recurring/ease.py +4 -4
  49. fin_infra/recurring/insights.py +15 -14
  50. fin_infra/recurring/normalizer.py +6 -6
  51. fin_infra/recurring/normalizers.py +27 -26
  52. fin_infra/scaffold/goals.py +4 -4
  53. fin_infra/security/add.py +1 -2
  54. fin_infra/security/audit.py +6 -7
  55. fin_infra/security/pii_filter.py +10 -10
  56. fin_infra/security/token_store.py +2 -3
  57. fin_infra/tax/add.py +2 -2
  58. fin_infra/tax/tlh.py +5 -5
  59. fin_infra/utils/__init__.py +15 -1
  60. fin_infra/utils/deprecation.py +161 -0
  61. {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/METADATA +17 -9
  62. {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/RECORD +65 -64
  63. {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/LICENSE +0 -0
  64. {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/WHEEL +0 -0
  65. {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/entry_points.txt +0 -0
@@ -22,7 +22,7 @@ Example:
22
22
  # Check which milestones have been reached
23
23
  reached = check_milestones("goal_123")
24
24
  for m in reached:
25
- print(f"🎉 Milestone reached: {m['description']}")
25
+ print(f" Milestone reached: {m['description']}")
26
26
  """
27
27
 
28
28
  from datetime import datetime
@@ -124,7 +124,7 @@ def check_milestones(goal_id: str) -> list[dict[str, Any]]:
124
124
 
125
125
  reached = check_milestones("goal_123")
126
126
  if reached:
127
- print(f"🎉 {len(reached)} milestones reached!")
127
+ print(f" {len(reached)} milestones reached!")
128
128
  for m in reached:
129
129
  print(f" - {m['description']}: ${m['amount']:,.0f}")
130
130
 
@@ -184,17 +184,17 @@ def get_celebration_message(milestone: dict[str, Any]) -> str:
184
184
 
185
185
  Example:
186
186
  message = get_celebration_message(milestone)
187
- # "🎉 Milestone reached! You've hit $25,000 - 25% to target!"
187
+ # " Milestone reached! You've hit $25,000 - 25% to target!"
188
188
  """
189
189
  amount = milestone["amount"]
190
190
  description = milestone["description"]
191
191
 
192
192
  messages = [
193
- f"🎉 Milestone reached! You've hit ${amount:,.0f} - {description}!",
193
+ f" Milestone reached! You've hit ${amount:,.0f} - {description}!",
194
194
  f"🎊 Congratulations! ${amount:,.0f} milestone achieved - {description}",
195
195
  f"🌟 Great progress! You reached ${amount:,.0f} - {description}",
196
- f"💪 Keep going! ${amount:,.0f} milestone completed - {description}",
197
- f"🚀 Amazing! You hit ${amount:,.0f} - {description}",
196
+ f" Keep going! ${amount:,.0f} milestone completed - {description}",
197
+ f" Amazing! You hit ${amount:,.0f} - {description}",
198
198
  ]
199
199
 
200
200
  # Use amount to pick consistent message for same milestone
fin_infra/goals/models.py CHANGED
@@ -93,8 +93,8 @@ class FundingSource(BaseModel):
93
93
 
94
94
  Supports split allocation:
95
95
  - Multiple accounts can fund one goal (e.g., savings + checking)
96
- - One account can fund multiple goals (e.g., savings emergency + vacation)
97
- - Allocation percentages must sum to 100% per account
96
+ - One account can fund multiple goals (e.g., savings -> emergency + vacation)
97
+ - Allocation percentages must sum to <=100% per account
98
98
  """
99
99
 
100
100
  goal_id: str = Field(..., description="Goal identifier")
@@ -74,8 +74,8 @@ def easy_investments(
74
74
  InvestmentProvider instance for fetching holdings, transactions, securities.
75
75
 
76
76
  Environment detection order:
77
- 1. If PLAID_CLIENT_ID set Plaid
78
- 2. If SNAPTRADE_CLIENT_ID set SnapTrade
77
+ 1. If PLAID_CLIENT_ID set -> Plaid
78
+ 2. If SNAPTRADE_CLIENT_ID set -> SnapTrade
79
79
  3. Default: Plaid (most common)
80
80
 
81
81
  Examples:
@@ -8,13 +8,13 @@ and SnapTrade (retail brokerages) for maximum coverage.
8
8
  from __future__ import annotations
9
9
 
10
10
  import os
11
- from typing import Any, Literal, Optional
11
+ from typing import Any, Literal
12
12
 
13
13
  from .providers.base import InvestmentProvider
14
14
 
15
15
 
16
16
  def easy_investments(
17
- provider: Optional[Literal["plaid", "snaptrade"]] = None,
17
+ provider: Literal["plaid", "snaptrade"] | None = None,
18
18
  **config: Any,
19
19
  ) -> InvestmentProvider:
20
20
  """Create investment provider with auto-configuration.
@@ -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, Optional
23
+ from typing import TYPE_CHECKING
24
24
 
25
25
  from pydantic import BaseModel, ConfigDict, Field, computed_field
26
26
 
@@ -121,22 +121,20 @@ class Security(BaseModel):
121
121
 
122
122
  # Identifiers (at least one required for matching)
123
123
  security_id: str = Field(..., description="Provider-specific security ID")
124
- cusip: Optional[str] = Field(None, description="CUSIP identifier (US securities)")
125
- isin: Optional[str] = Field(None, description="ISIN identifier (international)")
126
- sedol: Optional[str] = Field(None, description="SEDOL identifier (UK securities)")
127
- ticker_symbol: Optional[str] = Field(None, description="Trading symbol (AAPL, GOOGL)")
124
+ cusip: str | None = Field(None, description="CUSIP identifier (US securities)")
125
+ isin: str | None = Field(None, description="ISIN identifier (international)")
126
+ sedol: str | None = Field(None, description="SEDOL identifier (UK securities)")
127
+ ticker_symbol: str | None = Field(None, description="Trading symbol (AAPL, GOOGL)")
128
128
 
129
129
  # Basic info
130
130
  name: str = Field(..., description="Security name")
131
131
  type: SecurityType = Field(..., description="Security type (equity, etf, bond, etc.)")
132
- sector: Optional[str] = Field(
133
- None, description="Sector classification (Technology, Healthcare)"
134
- )
132
+ sector: str | None = Field(None, description="Sector classification (Technology, Healthcare)")
135
133
 
136
134
  # Market data
137
- close_price: Optional[Decimal] = Field(None, ge=0, description="Latest closing price")
138
- close_price_as_of: Optional[date] = Field(None, description="Date of close_price")
139
- exchange: Optional[str] = Field(None, description="Exchange (NASDAQ, NYSE, etc.)")
135
+ close_price: Decimal | None = Field(None, ge=0, description="Latest closing price")
136
+ close_price_as_of: date | None = Field(None, description="Date of close_price")
137
+ exchange: str | None = Field(None, description="Exchange (NASDAQ, NYSE, etc.)")
140
138
  currency: str = Field("USD", description="Currency code (USD, EUR, etc.)")
141
139
 
142
140
 
@@ -198,26 +196,26 @@ class Holding(BaseModel):
198
196
  institution_value: Decimal = Field(
199
197
  ..., ge=0, description="Current market value (quantity × price)"
200
198
  )
201
- cost_basis: Optional[Decimal] = Field(
199
+ cost_basis: Decimal | None = Field(
202
200
  None, ge=0, description="Total cost basis (original purchase price)"
203
201
  )
204
202
 
205
203
  # Additional data
206
204
  currency: str = Field("USD", description="Currency code")
207
- unofficial_currency_code: Optional[str] = Field(None, description="For crypto/alt currencies")
208
- as_of_date: Optional[date] = Field(None, description="Date of pricing data")
205
+ unofficial_currency_code: str | None = Field(None, description="For crypto/alt currencies")
206
+ as_of_date: date | None = Field(None, description="Date of pricing data")
209
207
 
210
208
  if TYPE_CHECKING:
211
209
 
212
210
  @property
213
- def unrealized_gain_loss(self) -> Optional[Decimal]:
211
+ def unrealized_gain_loss(self) -> Decimal | None:
214
212
  """Calculate unrealized gain/loss (current value - cost basis)."""
215
213
  if self.cost_basis is None:
216
214
  return None
217
215
  return self.institution_value - self.cost_basis
218
216
 
219
217
  @property
220
- def unrealized_gain_loss_percent(self) -> Optional[Decimal]:
218
+ def unrealized_gain_loss_percent(self) -> Decimal | None:
221
219
  """Calculate unrealized gain/loss percentage."""
222
220
  if self.cost_basis is None or self.cost_basis == 0:
223
221
  return None
@@ -228,7 +226,7 @@ class Holding(BaseModel):
228
226
 
229
227
  @computed_field
230
228
  @property
231
- def unrealized_gain_loss(self) -> Optional[Decimal]:
229
+ def unrealized_gain_loss(self) -> Decimal | None:
232
230
  """Calculate unrealized gain/loss (current value - cost basis)."""
233
231
  if self.cost_basis is None:
234
232
  return None
@@ -236,7 +234,7 @@ class Holding(BaseModel):
236
234
 
237
235
  @computed_field
238
236
  @property
239
- def unrealized_gain_loss_percent(self) -> Optional[Decimal]:
237
+ def unrealized_gain_loss_percent(self) -> Decimal | None:
240
238
  """Calculate unrealized gain/loss percentage."""
241
239
  if self.cost_basis is None or self.cost_basis == 0:
242
240
  return None
@@ -304,17 +302,17 @@ class InvestmentTransaction(BaseModel):
304
302
  transaction_type: TransactionType = Field(
305
303
  ..., alias="type", description="Transaction type (buy, sell, dividend)"
306
304
  )
307
- subtype: Optional[str] = Field(None, description="Provider-specific subtype")
305
+ subtype: str | None = Field(None, description="Provider-specific subtype")
308
306
 
309
307
  # Amounts
310
308
  quantity: Decimal = Field(..., description="Number of shares (0 for fees/dividends)")
311
309
  amount: Decimal = Field(..., description="Transaction amount (negative for purchases)")
312
- price: Optional[Decimal] = Field(None, ge=0, description="Price per share")
313
- fees: Optional[Decimal] = Field(None, ge=0, description="Transaction fees")
310
+ price: Decimal | None = Field(None, ge=0, description="Price per share")
311
+ fees: Decimal | None = Field(None, ge=0, description="Transaction fees")
314
312
 
315
313
  # Additional data
316
314
  currency: str = Field("USD", description="Currency code")
317
- unofficial_currency_code: Optional[str] = Field(None, description="For crypto/alt currencies")
315
+ unofficial_currency_code: str | None = Field(None, description="For crypto/alt currencies")
318
316
 
319
317
 
320
318
  class InvestmentAccount(BaseModel):
@@ -371,10 +369,10 @@ class InvestmentAccount(BaseModel):
371
369
  account_id: str = Field(..., description="Account identifier")
372
370
  name: str = Field(..., description="Account name (Fidelity 401k)")
373
371
  type: str = Field(..., description="Account type (investment)")
374
- subtype: Optional[str] = Field(None, description="Account subtype (401k, ira, brokerage)")
372
+ subtype: str | None = Field(None, description="Account subtype (401k, ira, brokerage)")
375
373
 
376
374
  # Balances
377
- balances: dict[str, Optional[Decimal]] = Field(
375
+ balances: dict[str, Decimal | None] = Field(
378
376
  ..., description="Current, available, and limit balances"
379
377
  )
380
378
 
@@ -405,7 +403,7 @@ class InvestmentAccount(BaseModel):
405
403
  return holdings_value - self.total_cost_basis
406
404
 
407
405
  @property
408
- def total_unrealized_gain_loss_percent(self) -> Optional[Decimal]:
406
+ def total_unrealized_gain_loss_percent(self) -> Decimal | None:
409
407
  """Calculate total unrealized P&L percentage."""
410
408
  if self.total_cost_basis == 0:
411
409
  return None
@@ -439,7 +437,7 @@ class InvestmentAccount(BaseModel):
439
437
 
440
438
  @computed_field
441
439
  @property
442
- def total_unrealized_gain_loss_percent(self) -> Optional[Decimal]:
440
+ def total_unrealized_gain_loss_percent(self) -> Decimal | None:
443
441
  """Calculate total unrealized P&L percentage."""
444
442
  if self.total_cost_basis == 0:
445
443
  return None
@@ -236,10 +236,10 @@ class InvestmentProvider(ABC):
236
236
  Standardized SecurityType enum value
237
237
 
238
238
  Example mappings:
239
- Plaid: "equity" SecurityType.equity
240
- Plaid: "mutual fund" SecurityType.mutual_fund
241
- SnapTrade: "cs" SecurityType.equity (common stock)
242
- SnapTrade: "etf" SecurityType.etf
239
+ Plaid: "equity" -> SecurityType.equity
240
+ Plaid: "mutual fund" -> SecurityType.mutual_fund
241
+ SnapTrade: "cs" -> SecurityType.equity (common stock)
242
+ SnapTrade: "etf" -> SecurityType.etf
243
243
 
244
244
  Note:
245
245
  Override in provider-specific implementations for custom mappings.
@@ -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
- - No historical portfolio values from past dates
22
- - No historical performance metrics
23
- - Cannot answer "What was my portfolio worth 3 months ago?"
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
- - Track portfolio value changes over time
27
- - Calculate performance metrics (returns, growth)
28
- - Show trend charts and historical analysis
29
- - Works even if user disconnects provider
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"],
@@ -33,8 +33,8 @@ def easy_market(
33
33
  """Create a market data provider with zero or minimal configuration.
34
34
 
35
35
  Auto-detects provider based on environment variables:
36
- 1. If ALPHA_VANTAGE_API_KEY or ALPHAVANTAGE_API_KEY is set Alpha Vantage
37
- 2. Otherwise Yahoo Finance (no key needed)
36
+ 1. If ALPHA_VANTAGE_API_KEY or ALPHAVANTAGE_API_KEY is set -> Alpha Vantage
37
+ 2. Otherwise -> Yahoo Finance (no key needed)
38
38
 
39
39
  Args:
40
40
  provider: Provider name ("alphavantage" or "yahoo").
@@ -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: Optional[str] = None
28
+ mask: str | None = None
30
29
  currency: str = "USD"
31
- institution: Optional[str] = None
32
- balance_available: Optional[Decimal] = None
33
- balance_current: Optional[Decimal] = None
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
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import date
3
+ import datetime
4
4
  from decimal import Decimal
5
5
 
6
6
  from pydantic import BaseModel, field_validator
@@ -15,7 +15,7 @@ class Transaction(BaseModel):
15
15
 
16
16
  id: str
17
17
  account_id: str
18
- date: date
18
+ date: datetime.date
19
19
  amount: Decimal
20
20
  currency: str = "USD"
21
21
  description: str | None = None
@@ -5,15 +5,15 @@ Calculates net worth by aggregating balances from multiple financial providers
5
5
  (banking, brokerage, crypto) with historical snapshots and change detection.
6
6
 
7
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)
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
13
 
14
14
  **Key Features**:
15
15
  - Multi-provider aggregation (banking + brokerage + crypto)
16
- - Currency normalization (all currencies USD)
16
+ - Currency normalization (all currencies -> USD)
17
17
  - Historical snapshots (daily at midnight UTC)
18
18
  - Change detection (>5% or >$10k triggers webhook)
19
19
  - Asset allocation breakdown (pie charts)
@@ -59,7 +59,7 @@ class NetWorthAggregator:
59
59
  - Multi-provider support (banking, brokerage, crypto)
60
60
  - Parallel account fetching (faster performance)
61
61
  - Graceful error handling (continue if one provider fails)
62
- - Currency normalization (all base currency)
62
+ - Currency normalization (all -> base currency)
63
63
  - Market value calculation (stocks/crypto)
64
64
 
65
65
  **Example**:
@@ -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 base currency)
6
+ - Currency normalization (all currencies -> base currency)
7
7
  - Asset allocation breakdown
8
8
  - Change detection (amount + percentage)
9
9
 
@@ -167,7 +167,7 @@ Be specific with numbers. Cite percentage changes and dollar amounts.
167
167
  Focus on actionable insights, not generic advice.
168
168
 
169
169
  Example 1:
170
- User: Net worth: $500k $575k over 6 months. Assets: +$65k (investments +$60k, savings +$5k). Liabilities: -$10k (new mortgage).
170
+ User: Net worth: $500k -> $575k over 6 months. Assets: +$65k (investments +$60k, savings +$5k). Liabilities: -$10k (new mortgage).
171
171
  Response: {
172
172
  "summary": "Net worth increased 15% ($75k) over 6 months, driven primarily by strong investment performance.",
173
173
  "period": "6 months",
@@ -191,7 +191,7 @@ Response: {
191
191
  }
192
192
 
193
193
  Example 2:
194
- User: Net worth: $100k $95k over 3 months. Assets: -$2k (market down). Liabilities: +$3k (credit card debt).
194
+ User: Net worth: $100k -> $95k over 3 months. Assets: -$2k (market down). Liabilities: +$3k (credit card debt).
195
195
  Response: {
196
196
  "summary": "Net worth decreased 5% ($5k) over 3 months due to market decline and rising credit card debt.",
197
197
  "period": "3 months",
@@ -215,7 +215,7 @@ Response: {
215
215
  "confidence": 0.89
216
216
  }
217
217
 
218
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
218
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
219
219
  Verify calculations independently. For personalized advice, consult a professional."""
220
220
 
221
221
  DEBT_REDUCTION_SYSTEM_PROMPT = """You are a debt counselor using the avalanche method (highest APR first).
@@ -269,7 +269,7 @@ Response: {
269
269
  "confidence": 0.98
270
270
  }
271
271
 
272
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
272
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
273
273
  Verify calculations independently. For personalized advice, consult a professional."""
274
274
 
275
275
  GOAL_RECOMMENDATION_SYSTEM_PROMPT = """You are a financial planner validating goals and suggesting paths.
@@ -322,7 +322,7 @@ Response: {
322
322
  "confidence": 0.89
323
323
  }
324
324
 
325
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
325
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
326
326
  Verify calculations independently. For personalized advice, consult a professional."""
327
327
 
328
328
  ASSET_ALLOCATION_SYSTEM_PROMPT = """You are a portfolio advisor recommending asset allocation.
@@ -333,7 +333,7 @@ Given current allocation, age, and risk tolerance:
333
333
  3. Provide specific rebalancing steps
334
334
 
335
335
  Rule of thumb:
336
- - Stock allocation = 100 - age (e.g., age 35 65% stocks)
336
+ - Stock allocation = 100 - age (e.g., age 35 -> 65% stocks)
337
337
  - Bonds for stability (increases with age)
338
338
  - Cash for emergency fund (3-6 months expenses)
339
339
 
@@ -365,7 +365,7 @@ Response: {
365
365
  "confidence": 0.91
366
366
  }
367
367
 
368
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
368
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
369
369
  Verify calculations independently. For personalized advice, consult a professional."""
370
370
 
371
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 AAPL
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 {"ticker": "AAPL", ...}
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:
@@ -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: Optional[str] = None):
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: Optional[DateType] = None,
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} {to_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: Optional[DateType] = None,
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} {to_currency}: {e}")
111
+ logger.error(f"Failed to get rate {from_currency} -> {to_currency}: {e}")
113
112
  raise CurrencyNotSupportedError(
114
- f"Rate not available: {from_currency} {to_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: Optional[DateType] = None,
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: Optional[str] = Field(None, description="Primary exchange (e.g., NASDAQ, NYSE)")
15
- cusip: Optional[str] = Field(None, description="CUSIP identifier")
16
- isin: Optional[str] = Field(None, description="ISIN identifier")
17
- sector: Optional[str] = Field(None, description="Business sector")
18
- industry: Optional[str] = Field(None, description="Industry classification")
19
- market_cap: Optional[float] = Field(None, description="Market capitalization in USD")
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: Optional[DateType] = Field(None, description="Rate date (None = current)")
30
- timestamp: Optional[int] = Field(None, description="Unix timestamp of rate")
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: Optional[DateType] = Field(None, description="Rate date")
40
+ date: DateType | None = Field(None, description="Rate date")
@@ -109,7 +109,7 @@ TICKER_TO_ISIN = {
109
109
  }
110
110
 
111
111
  # Provider-specific symbol normalization
112
- # Maps provider-specific format standard ticker
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: Optional[str] = None,
235
- isin: Optional[str] = None,
236
- metadata: Optional[dict] = None,
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).