fin-infra 0.2.0__py3-none-any.whl → 0.2.2__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 (43) hide show
  1. fin_infra/analytics/__init__.py +2 -2
  2. fin_infra/analytics/add.py +3 -3
  3. fin_infra/analytics/portfolio.py +12 -12
  4. fin_infra/banking/history.py +4 -4
  5. fin_infra/brokerage/__init__.py +7 -7
  6. fin_infra/categorization/__init__.py +1 -1
  7. fin_infra/categorization/engine.py +1 -1
  8. fin_infra/categorization/llm_layer.py +4 -4
  9. fin_infra/chat/planning.py +1 -1
  10. fin_infra/cli/cmds/scaffold_cmds.py +6 -6
  11. fin_infra/credit/experian/parser.py +5 -5
  12. fin_infra/crypto/__init__.py +2 -2
  13. fin_infra/goals/management.py +3 -3
  14. fin_infra/goals/milestones.py +6 -6
  15. fin_infra/goals/models.py +2 -2
  16. fin_infra/investments/__init__.py +2 -2
  17. fin_infra/investments/providers/base.py +4 -4
  18. fin_infra/investments/scaffold_templates/README.md +17 -17
  19. fin_infra/markets/__init__.py +2 -2
  20. fin_infra/net_worth/__init__.py +6 -6
  21. fin_infra/net_worth/aggregator.py +1 -1
  22. fin_infra/net_worth/calculator.py +1 -1
  23. fin_infra/net_worth/insights.py +7 -7
  24. fin_infra/normalization/__init__.py +2 -2
  25. fin_infra/normalization/currency_converter.py +3 -3
  26. fin_infra/normalization/providers/static_mappings.py +1 -1
  27. fin_infra/obs/classifier.py +2 -2
  28. fin_infra/providers/brokerage/alpaca.py +1 -1
  29. fin_infra/recurring/add.py +1 -1
  30. fin_infra/recurring/detectors_llm.py +5 -5
  31. fin_infra/recurring/ease.py +4 -4
  32. fin_infra/recurring/insights.py +14 -14
  33. fin_infra/recurring/normalizer.py +6 -6
  34. fin_infra/recurring/normalizers.py +26 -26
  35. fin_infra/scaffold/goals.py +4 -4
  36. fin_infra/security/pii_filter.py +10 -10
  37. fin_infra/tax/add.py +2 -2
  38. fin_infra/tax/tlh.py +5 -5
  39. {fin_infra-0.2.0.dist-info → fin_infra-0.2.2.dist-info}/METADATA +15 -12
  40. {fin_infra-0.2.0.dist-info → fin_infra-0.2.2.dist-info}/RECORD +43 -43
  41. {fin_infra-0.2.0.dist-info → fin_infra-0.2.2.dist-info}/LICENSE +0 -0
  42. {fin_infra-0.2.0.dist-info → fin_infra-0.2.2.dist-info}/WHEEL +0 -0
  43. {fin_infra-0.2.0.dist-info → fin_infra-0.2.2.dist-info}/entry_points.txt +0 -0
@@ -8,8 +8,8 @@ This module provides comprehensive financial analytics capabilities including:
8
8
  - Growth projections (net worth forecasting with scenarios)
9
9
 
10
10
  Feature Status:
11
- STABLE: Core calculation functions (all analytics work with provided data)
12
- ⚠️ INTEGRATION: Auto-fetching from providers requires setup:
11
+ [OK] STABLE: Core calculation functions (all analytics work with provided data)
12
+ [!] INTEGRATION: Auto-fetching from providers requires setup:
13
13
  - Banking provider for transaction data
14
14
  - Brokerage provider for investment data
15
15
  - Categorization for expense categorization
@@ -239,9 +239,9 @@ def add_analytics(
239
239
 
240
240
  Note:
241
241
  Real holdings provide:
242
- - Accurate cost basis real profit/loss
243
- - Security types precise asset allocation
244
- - Current values live portfolio tracking
242
+ - Accurate cost basis -> real profit/loss
243
+ - Security types -> precise asset allocation
244
+ - Current values -> live portfolio tracking
245
245
  """
246
246
  # If with_holdings requested and investment provider available
247
247
  if with_holdings:
@@ -527,10 +527,10 @@ def portfolio_metrics_with_holdings(holdings: list) -> PortfolioMetrics:
527
527
  PortfolioMetrics with real portfolio analysis
528
528
 
529
529
  Real Data Advantages:
530
- - Actual cost basis accurate P/L calculations
531
- - Real security types precise asset allocation
532
- - Current market values live portfolio value
533
- - No mock data production-ready analytics
530
+ - Actual cost basis -> accurate P/L calculations
531
+ - Real security types -> precise asset allocation
532
+ - Current market values -> live portfolio value
533
+ - No mock data -> production-ready analytics
534
534
 
535
535
  Limitations:
536
536
  - Day/YTD/MTD returns require historical snapshots (not in holdings)
@@ -652,7 +652,7 @@ def calculate_day_change_with_snapshot(
652
652
  Function matches holdings by account_id + security_id for accurate tracking
653
653
  of individual position changes (accounts for buys/sells, not just price moves).
654
654
  """
655
- # Build lookup map for previous snapshot: (account_id, security_id) value
655
+ # Build lookup map for previous snapshot: (account_id, security_id) -> value
656
656
  previous_map = {}
657
657
  for holding in previous_snapshot:
658
658
  key = (holding.account_id, holding.security.security_id)
@@ -703,13 +703,13 @@ def _calculate_allocation_from_holdings(
703
703
  list[AssetAllocation] with asset_class, value, and percentage
704
704
 
705
705
  Asset Class Mapping:
706
- - equity Stocks
707
- - etf Stocks (equity ETFs grouped with stocks)
708
- - mutual_fund Bonds (conservative assumption)
709
- - bond Bonds
710
- - cash Cash
711
- - derivative Other
712
- - other Other
706
+ - equity -> Stocks
707
+ - etf -> Stocks (equity ETFs grouped with stocks)
708
+ - mutual_fund -> Bonds (conservative assumption)
709
+ - bond -> Bonds
710
+ - cash -> Cash
711
+ - derivative -> Other
712
+ - other -> Other
713
713
  """
714
714
  from collections import defaultdict
715
715
 
@@ -4,7 +4,7 @@ This module provides functionality to record and retrieve historical account bal
4
4
  snapshots over time. This enables balance trend analysis, sparklines, and time-series
5
5
  visualizations in fintech dashboards.
6
6
 
7
- ⚠️ WARNING: This module uses IN-MEMORY storage by default. All data is LOST on restart.
7
+ [!] WARNING: This module uses IN-MEMORY storage by default. All data is LOST on restart.
8
8
  For production use, integrate with svc-infra SQL database or set FIN_INFRA_STORAGE_BACKEND.
9
9
 
10
10
  Features:
@@ -57,7 +57,7 @@ __all__ = [
57
57
  _logger = logging.getLogger(__name__)
58
58
 
59
59
  # In-memory storage for testing (will be replaced with SQL database in production)
60
- # ⚠️ WARNING: All data is LOST on restart when using in-memory storage!
60
+ # [!] WARNING: All data is LOST on restart when using in-memory storage!
61
61
  _balance_snapshots: list[BalanceSnapshot] = []
62
62
  _production_warning_logged = False
63
63
 
@@ -73,7 +73,7 @@ def _check_in_memory_warning() -> None:
73
73
 
74
74
  if env in ("production", "staging") and storage_backend == "memory":
75
75
  _logger.warning(
76
- "⚠️ CRITICAL: Balance history using IN-MEMORY storage in %s environment! "
76
+ "[!] CRITICAL: Balance history using IN-MEMORY storage in %s environment! "
77
77
  "All balance snapshots will be LOST on restart. "
78
78
  "Set FIN_INFRA_STORAGE_BACKEND=sql for production persistence.",
79
79
  env,
@@ -114,7 +114,7 @@ def record_balance_snapshot(
114
114
  This function stores a point-in-time balance record for trend analysis.
115
115
  In production, this would write to a SQL database via svc-infra.
116
116
 
117
- ⚠️ WARNING: Uses in-memory storage by default. Data is LOST on restart!
117
+ [!] WARNING: Uses in-memory storage by default. Data is LOST on restart!
118
118
 
119
119
  Args:
120
120
  account_id: Account identifier
@@ -1,6 +1,6 @@
1
1
  """Brokerage module - easy setup for trading operations.
2
2
 
3
- ⚠️ **TRADING WARNING**: This module provides real trading capabilities.
3
+ [!] **TRADING WARNING**: This module provides real trading capabilities.
4
4
  Always use paper trading mode for development and testing.
5
5
  Live trading requires explicit opt-in and involves real financial risk.
6
6
 
@@ -51,11 +51,11 @@ def easy_brokerage(
51
51
  ) -> BrokerageProvider:
52
52
  """Create a brokerage provider with paper/live trading support.
53
53
 
54
- ⚠️ **SAFETY**: Defaults to paper trading mode. Live trading requires explicit mode="live".
54
+ [!] **SAFETY**: Defaults to paper trading mode. Live trading requires explicit mode="live".
55
55
 
56
56
  Auto-detects provider based on environment variables:
57
- 1. If ALPACA_API_KEY and ALPACA_API_SECRET are set Alpaca
58
- 2. Otherwise Raises error (credentials required)
57
+ 1. If ALPACA_API_KEY and ALPACA_API_SECRET are set -> Alpaca
58
+ 2. Otherwise -> Raises error (credentials required)
59
59
 
60
60
  Args:
61
61
  provider: Provider name ("alpaca"). If None, defaults to alpaca.
@@ -138,7 +138,7 @@ def add_brokerage(
138
138
  ) -> BrokerageProvider:
139
139
  """Wire brokerage provider to FastAPI app with routes and safety checks.
140
140
 
141
- ⚠️ **TRADING WARNING**: This mounts trading API endpoints.
141
+ [!] **TRADING WARNING**: This mounts trading API endpoints.
142
142
  Always use paper trading mode for development.
143
143
  Live trading requires explicit mode="live" and proper safeguards.
144
144
 
@@ -280,7 +280,7 @@ def add_brokerage(
280
280
  async def submit_order(order_request: OrderRequest):
281
281
  """Submit a new order.
282
282
 
283
- ⚠️ **TRADING WARNING**: This endpoint executes real trades in live mode.
283
+ [!] **TRADING WARNING**: This endpoint executes real trades in live mode.
284
284
  """
285
285
  try:
286
286
  order = brokerage_provider.submit_order(
@@ -447,7 +447,7 @@ def add_brokerage(
447
447
  add_prefixed_docs(
448
448
  app,
449
449
  prefix=prefix,
450
- title="Brokerage" + (" (Paper Trading)" if mode == "paper" else " ⚠️ LIVE"),
450
+ title="Brokerage" + (" (Paper Trading)" if mode == "paper" else " [!] LIVE"),
451
451
  auto_exclude_from_root=True,
452
452
  visible_envs=None, # Show in all environments
453
453
  )
@@ -2,7 +2,7 @@
2
2
  Transaction categorization module.
3
3
 
4
4
  Provides ML-based categorization of merchant transactions into 56 categories
5
- using a hybrid approach (exact match regex sklearn Naive Bayes LLM).
5
+ using a hybrid approach (exact match -> regex -> sklearn Naive Bayes -> LLM).
6
6
 
7
7
  Basic usage:
8
8
  from fin_infra.categorization import categorize
@@ -1,5 +1,5 @@
1
1
  """
2
- Hybrid categorization engine (exact regex ML LLM).
2
+ Hybrid categorization engine (exact -> regex -> ML -> LLM).
3
3
 
4
4
  4-layer approach:
5
5
  1. Layer 1 (Exact Match): O(1) dictionary lookup, 85-90% coverage
@@ -197,7 +197,7 @@ class LLMCategorizer:
197
197
  self._track_cost()
198
198
 
199
199
  logger.info(
200
- f"LLM categorized '{merchant_name}' {prediction.category} "
200
+ f"LLM categorized '{merchant_name}' -> {prediction.category} "
201
201
  f"(confidence={prediction.confidence:.2f})"
202
202
  )
203
203
 
@@ -252,7 +252,7 @@ class LLMCategorizer:
252
252
  # Format few-shot examples
253
253
  examples_text = "\n\n".join(
254
254
  [
255
- f'Merchant: "{merchant}"\n Category: "{category}"\n Reasoning: "{reasoning}"'
255
+ f'Merchant: "{merchant}"\n-> Category: "{category}"\n-> Reasoning: "{reasoning}"'
256
256
  for merchant, category, reasoning in FEW_SHOT_EXAMPLES
257
257
  ]
258
258
  )
@@ -338,10 +338,10 @@ Return JSON with category, confidence, and reasoning."""
338
338
 
339
339
  def reset_daily_cost(self):
340
340
  """Reset daily cost counter (called at midnight UTC)."""
341
- logger.info(f"Resetting daily cost: ${self.daily_cost:.5f} $0.00")
341
+ logger.info(f"Resetting daily cost: ${self.daily_cost:.5f} -> $0.00")
342
342
  self.daily_cost = 0.0
343
343
 
344
344
  def reset_monthly_cost(self):
345
345
  """Reset monthly cost counter (called on 1st of month)."""
346
- logger.info(f"Resetting monthly cost: ${self.monthly_cost:.5f} $0.00")
346
+ logger.info(f"Resetting monthly cost: ${self.monthly_cost:.5f} -> $0.00")
347
347
  self.monthly_cost = 0.0
@@ -173,7 +173,7 @@ Answer: "To assess your retirement progress, I need more information: (1) What's
173
173
  Follow-ups: ["I want to retire at 65 with $1.5M", "How much should I save monthly?", "What's a realistic retirement goal?"]
174
174
  Sources: []
175
175
 
176
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
176
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
177
177
  Verify calculations independently. For personalized advice, consult a professional."""
178
178
 
179
179
 
@@ -102,7 +102,7 @@ def cmd_scaffold(
102
102
  # Validate required parameters
103
103
  if dest_dir is None:
104
104
  typer.secho(
105
- " Error: --dest-dir is required",
105
+ "[X] Error: --dest-dir is required",
106
106
  fg=typer.colors.RED,
107
107
  err=True,
108
108
  )
@@ -137,7 +137,7 @@ def cmd_scaffold(
137
137
  )
138
138
  else:
139
139
  typer.secho(
140
- f" Unknown domain: {domain}. Must be one of: budgets, goals",
140
+ f"[X] Unknown domain: {domain}. Must be one of: budgets, goals",
141
141
  fg=typer.colors.RED,
142
142
  err=True,
143
143
  )
@@ -145,7 +145,7 @@ def cmd_scaffold(
145
145
 
146
146
  # Display results
147
147
  typer.echo("")
148
- typer.secho("📦 Scaffold Results:", bold=True)
148
+ typer.secho(" Scaffold Results:", bold=True)
149
149
  typer.echo("")
150
150
 
151
151
  files = result.get("files", [])
@@ -157,7 +157,7 @@ def cmd_scaffold(
157
157
  action = file_info.get("action", "unknown")
158
158
 
159
159
  if action == "wrote":
160
- typer.secho(f" Created: {path}", fg=typer.colors.GREEN)
160
+ typer.secho(f" [OK] Created: {path}", fg=typer.colors.GREEN)
161
161
  wrote_count += 1
162
162
  elif action == "skipped":
163
163
  reason = file_info.get("reason", "unknown")
@@ -168,7 +168,7 @@ def cmd_scaffold(
168
168
 
169
169
  # Summary
170
170
  typer.echo("")
171
- typer.secho(f" Done! Created {wrote_count} file(s), skipped {skipped_count}.", bold=True)
171
+ typer.secho(f" Done! Created {wrote_count} file(s), skipped {skipped_count}.", bold=True)
172
172
  typer.echo("")
173
173
 
174
174
  # Next steps
@@ -187,7 +187,7 @@ def cmd_scaffold(
187
187
  }
188
188
  route_prefix = prefix_map.get(domain, f"/{domain}")
189
189
 
190
- typer.secho("📝 Next Steps:", bold=True)
190
+ typer.secho(" Next Steps:", bold=True)
191
191
  typer.echo("")
192
192
  typer.echo(" 1. Review generated files and customize as needed")
193
193
  typer.echo(" 2. Run migrations:")
@@ -1,11 +1,11 @@
1
1
  """Response parsers for Experian API data to fin_infra models.
2
2
 
3
3
  Converts Experian API JSON responses to typed Pydantic models:
4
- - parse_credit_score(): dict CreditScore
5
- - parse_credit_report(): dict CreditReport
6
- - parse_account(): dict CreditAccount
7
- - parse_inquiry(): dict CreditInquiry
8
- - parse_public_record(): dict PublicRecord
4
+ - parse_credit_score(): dict -> CreditScore
5
+ - parse_credit_report(): dict -> CreditReport
6
+ - parse_account(): dict -> CreditAccount
7
+ - parse_inquiry(): dict -> CreditInquiry
8
+ - parse_public_record(): dict -> PublicRecord
9
9
 
10
10
  Example:
11
11
  >>> data = await client.get_credit_score("user123")
@@ -29,8 +29,8 @@ def easy_crypto(
29
29
  """Create a crypto data provider with zero or minimal configuration.
30
30
 
31
31
  Auto-detects provider based on environment variables:
32
- 1. If COINGECKO_API_KEY is set CoinGecko Pro
33
- 2. Otherwise CoinGecko Free (no key needed)
32
+ 1. If COINGECKO_API_KEY is set -> CoinGecko Pro
33
+ 2. Otherwise -> CoinGecko Free (no key needed)
34
34
 
35
35
  Args:
36
36
  provider: Provider name ("coingecko"). If None, defaults to coingecko.
@@ -182,7 +182,7 @@ Your response: {
182
182
  "confidence": 0.94
183
183
  }
184
184
 
185
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
185
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
186
186
  Verify calculations independently. For personalized advice, consult a professional."""
187
187
 
188
188
  GOAL_PROGRESS_SYSTEM_PROMPT = """You are a financial advisor reviewing goal progress.
@@ -237,7 +237,7 @@ Your response: {
237
237
  "projected_completion_date": "2029-06-01",
238
238
  "variance_from_target_days": -365,
239
239
  "course_corrections": [
240
- "⚠️ 12 months behind! Current $1,000/month payment needs to increase to $1,500/month",
240
+ "[!] 12 months behind! Current $1,000/month payment needs to increase to $1,500/month",
241
241
  "Emergency: reduce expenses by $500/month (cancel subscriptions, cut entertainment)",
242
242
  "Contact debt counselor for consolidation or negotiation options",
243
243
  "Consider side income: gig work, selling unused items ($500/month target)",
@@ -246,7 +246,7 @@ Your response: {
246
246
  "confidence": 0.95
247
247
  }
248
248
 
249
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
249
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
250
250
  Verify calculations independently. For personalized advice, consult a professional."""
251
251
 
252
252
 
@@ -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:
@@ -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").
@@ -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: