@drico2008/fincli 0.1.9 → 0.3.0

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -625
  3. package/fincli/__init__.py +3 -3
  4. package/fincli/app/agents/__init__.py +5 -0
  5. package/fincli/app/agents/registry.py +76 -0
  6. package/fincli/app/analysis/ai_prompts.py +26 -14
  7. package/fincli/app/analysis/analyzer.py +107 -96
  8. package/fincli/app/analysis/assistant_context.py +187 -186
  9. package/fincli/app/analysis/backtest.py +179 -0
  10. package/fincli/app/analysis/gameplay_plan.py +79 -0
  11. package/fincli/app/analysis/multi_timeframe.py +180 -0
  12. package/fincli/app/analysis/trading_methods.py +144 -0
  13. package/fincli/app/cli/commands.py +108 -81
  14. package/fincli/app/cli/router.py +2327 -1237
  15. package/fincli/app/connectors/__init__.py +5 -0
  16. package/fincli/app/connectors/catalog.py +148 -0
  17. package/fincli/app/connectors/news_connectors.py +412 -0
  18. package/fincli/app/modules/alerts.py +80 -0
  19. package/fincli/app/modules/economic_calendar.py +374 -1
  20. package/fincli/app/modules/portfolio_risk.py +305 -0
  21. package/fincli/app/modules/reports.py +151 -0
  22. package/fincli/app/modules/scanner.py +111 -93
  23. package/fincli/app/modules/transactions.py +84 -84
  24. package/fincli/app/modules/user_profile.py +84 -0
  25. package/fincli/app/plugins/loader.py +72 -0
  26. package/fincli/app/providers/ai/manager.py +60 -60
  27. package/fincli/app/providers/market/alphavantage_provider.py +194 -0
  28. package/fincli/app/providers/market/base.py +98 -77
  29. package/fincli/app/providers/market/custom_provider.py +186 -169
  30. package/fincli/app/providers/market/manager.py +84 -1
  31. package/fincli/app/providers/market/symbols.py +143 -0
  32. package/fincli/app/providers/market/twelvedata_provider.py +167 -167
  33. package/fincli/app/providers/reliability.py +86 -0
  34. package/fincli/app/research/__init__.py +8 -0
  35. package/fincli/app/research/engine.py +137 -0
  36. package/fincli/app/research/exporter.py +91 -0
  37. package/fincli/app/research/formatter.py +27 -0
  38. package/fincli/app/research/models.py +24 -0
  39. package/fincli/app/research/prompt_builder.py +54 -0
  40. package/fincli/app/services/macro_data.py +50 -0
  41. package/fincli/app/services/market_data.py +274 -169
  42. package/fincli/app/services/market_overview.py +42 -1
  43. package/fincli/app/services/news_aggregator.py +95 -0
  44. package/fincli/app/services/web_research.py +267 -267
  45. package/fincli/app/storage/config.py +122 -88
  46. package/fincli/app/storage/database.py +209 -99
  47. package/fincli/app/storage/provider_metrics.py +61 -0
  48. package/fincli/app/storage/secrets.py +26 -2
  49. package/fincli/app/tui/components.py +68 -50
  50. package/fincli/app/tui/layout.py +269 -258
  51. package/fincli/app/tui/market_provider_selector.py +3 -1
  52. package/fincli/app/tui/theme.py +134 -74
  53. package/fincli/app/utils/formatting.py +123 -60
  54. package/package.json +22 -20
  55. package/pyproject.toml +35 -35
@@ -1,77 +1,98 @@
1
- """Base market provider contract for future provider implementations."""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import dataclass
6
- from datetime import datetime
7
- from typing import Protocol
8
-
9
-
10
- @dataclass(frozen=True, slots=True)
11
- class Quote:
12
- symbol: str
13
- price: float | None
14
- currency: str
15
- provider: str
16
- timestamp: datetime
17
- status: str
18
-
19
-
20
- @dataclass(frozen=True, slots=True)
21
- class Candle:
22
- timestamp: datetime
23
- open: float
24
- high: float
25
- low: float
26
- close: float
27
- volume: float
28
-
29
-
30
- @dataclass(frozen=True, slots=True)
31
- class NewsItem:
32
- title: str
33
- source: str
34
- url: str | None
35
- published_at: datetime | None
36
- summary: str = ""
37
-
38
-
39
- @dataclass(frozen=True, slots=True)
40
- class FundamentalSnapshot:
41
- symbol: str
42
- provider: str
43
- currency: str
44
- market_cap: float | None = None
45
- pe_ratio: float | None = None
46
- eps: float | None = None
47
- revenue: float | None = None
48
- beta: float | None = None
49
- sector: str | None = None
50
- industry: str | None = None
51
-
52
-
53
- @dataclass(frozen=True, slots=True)
54
- class ProviderStatus:
55
- name: str
56
- realtime: bool
57
- status: str
58
- message: str
59
-
60
-
61
- class BaseMarketProvider(Protocol):
62
- name: str
63
-
64
- async def quote(self, symbol: str) -> Quote:
65
- """Fetch a single quote."""
66
-
67
- async def history(self, symbol: str, period: str = "6mo", interval: str = "1d") -> list[Candle]:
68
- """Fetch historical candles."""
69
-
70
- async def status(self) -> ProviderStatus:
71
- """Return provider health and realtime/delayed status."""
72
-
73
- async def news(self, symbol: str, limit: int = 5) -> list[NewsItem]:
74
- """Fetch latest news items."""
75
-
76
- async def fundamentals(self, symbol: str) -> FundamentalSnapshot:
77
- """Fetch a compact fundamental snapshot."""
1
+ """Base market provider contract for future provider implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from typing import Protocol
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Quote:
12
+ symbol: str
13
+ price: float | None
14
+ currency: str
15
+ provider: str
16
+ timestamp: datetime
17
+ status: str
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class Candle:
22
+ timestamp: datetime
23
+ open: float
24
+ high: float
25
+ low: float
26
+ close: float
27
+ volume: float
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class NewsItem:
32
+ title: str
33
+ source: str
34
+ url: str | None
35
+ published_at: datetime | None
36
+ summary: str = ""
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class FundamentalSnapshot:
41
+ symbol: str
42
+ provider: str
43
+ currency: str
44
+ market_cap: float | None = None
45
+ pe_ratio: float | None = None
46
+ eps: float | None = None
47
+ revenue: float | None = None
48
+ beta: float | None = None
49
+ sector: str | None = None
50
+ industry: str | None = None
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class ProviderStatus:
55
+ name: str
56
+ realtime: bool
57
+ status: str
58
+ message: str
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class ProviderEntitlement:
63
+ provider: str
64
+ status: str
65
+ realtime_label: str
66
+ asset_classes: tuple[str, ...]
67
+ capabilities: tuple[str, ...]
68
+ limitations: tuple[str, ...] = ()
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class SymbolSearchResult:
73
+ symbol: str
74
+ name: str
75
+ asset_class: str
76
+ exchange: str = ""
77
+ currency: str = ""
78
+ provider_symbols: dict[str, str] | None = None
79
+ notes: str = ""
80
+
81
+
82
+ class BaseMarketProvider(Protocol):
83
+ name: str
84
+
85
+ async def quote(self, symbol: str) -> Quote:
86
+ """Fetch a single quote."""
87
+
88
+ async def history(self, symbol: str, period: str = "6mo", interval: str = "1d") -> list[Candle]:
89
+ """Fetch historical candles."""
90
+
91
+ async def status(self) -> ProviderStatus:
92
+ """Return provider health and realtime/delayed status."""
93
+
94
+ async def news(self, symbol: str, limit: int = 5) -> list[NewsItem]:
95
+ """Fetch latest news items."""
96
+
97
+ async def fundamentals(self, symbol: str) -> FundamentalSnapshot:
98
+ """Fetch a compact fundamental snapshot."""
@@ -1,169 +1,186 @@
1
- """Custom HTTP market API provider.
2
-
3
- Expected endpoint contract:
4
- - GET /quote/{symbol}
5
- - GET /history/{symbol}?period=6mo&interval=1d
6
- - GET /news/{symbol}?limit=5
7
- - GET /fundamentals/{symbol}
8
-
9
- The provider accepts common JSON key variants so users can adapt simple APIs
10
- without changing FinCLI core.
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- from datetime import datetime
16
- from typing import Any
17
-
18
- import httpx
19
-
20
- from fincli.app.providers.market.base import (
21
- BaseMarketProvider,
22
- Candle,
23
- FundamentalSnapshot,
24
- NewsItem,
25
- ProviderStatus,
26
- Quote,
27
- )
28
- from fincli.app.utils.errors import ProviderError, RateLimitError
29
-
30
-
31
- class CustomMarketProvider(BaseMarketProvider):
32
- name = "custom"
33
-
34
- def __init__(
35
- self,
36
- api_key: str | None,
37
- base_url: str,
38
- client: httpx.AsyncClient | None = None,
39
- ) -> None:
40
- self.api_key = api_key or ""
41
- self.base_url = base_url.rstrip("/")
42
- self._client = client
43
-
44
- async def quote(self, symbol: str) -> Quote:
45
- data = await self._get(f"/quote/{symbol.upper()}")
46
- return Quote(
47
- symbol=str(data.get("symbol") or symbol).upper(),
48
- price=_safe_float(data.get("price") or data.get("last") or data.get("last_price")),
49
- currency=str(data.get("currency") or "USD"),
50
- provider=self.name,
51
- timestamp=_parse_datetime(data.get("timestamp")) or datetime.now(),
52
- status=str(data.get("status") or "custom"),
53
- )
54
-
55
- async def history(self, symbol: str, period: str = "6mo", interval: str = "1d") -> list[Candle]:
56
- data = await self._get(f"/history/{symbol.upper()}", params={"period": period, "interval": interval})
57
- raw_items = data.get("candles") if isinstance(data, dict) else data
58
- if not isinstance(raw_items, list):
59
- raise ProviderError("Response history custom provider tidak valid.")
60
- candles: list[Candle] = []
61
- for item in raw_items:
62
- if not isinstance(item, dict):
63
- continue
64
- candles.append(
65
- Candle(
66
- timestamp=_parse_datetime(item.get("timestamp") or item.get("date")) or datetime.now(),
67
- open=float(item.get("open") or item.get("o")),
68
- high=float(item.get("high") or item.get("h")),
69
- low=float(item.get("low") or item.get("l")),
70
- close=float(item.get("close") or item.get("c")),
71
- volume=float(item.get("volume") or item.get("v") or 0),
72
- )
73
- )
74
- if not candles:
75
- raise ProviderError(f"Data OHLCV kosong untuk {symbol}.")
76
- return candles
77
-
78
- async def news(self, symbol: str, limit: int = 5) -> list[NewsItem]:
79
- data = await self._get(f"/news/{symbol.upper()}", params={"limit": limit})
80
- raw_items = data.get("news") if isinstance(data, dict) else data
81
- if not isinstance(raw_items, list):
82
- raise ProviderError("Response news custom provider tidak valid.")
83
- items: list[NewsItem] = []
84
- for item in raw_items[:limit]:
85
- if not isinstance(item, dict):
86
- continue
87
- items.append(
88
- NewsItem(
89
- title=str(item.get("title") or "Untitled"),
90
- source=str(item.get("source") or self.name),
91
- url=item.get("url"),
92
- published_at=_parse_datetime(item.get("published_at") or item.get("timestamp")),
93
- summary=str(item.get("summary") or ""),
94
- )
95
- )
96
- return items
97
-
98
- async def fundamentals(self, symbol: str) -> FundamentalSnapshot:
99
- data = await self._get(f"/fundamentals/{symbol.upper()}")
100
- return FundamentalSnapshot(
101
- symbol=str(data.get("symbol") or symbol).upper(),
102
- provider=self.name,
103
- currency=str(data.get("currency") or "USD"),
104
- market_cap=_safe_float(data.get("market_cap") or data.get("marketCap")),
105
- pe_ratio=_safe_float(data.get("pe_ratio") or data.get("trailingPE")),
106
- eps=_safe_float(data.get("eps") or data.get("trailingEps")),
107
- revenue=_safe_float(data.get("revenue") or data.get("totalRevenue")),
108
- beta=_safe_float(data.get("beta")),
109
- sector=data.get("sector"),
110
- industry=data.get("industry"),
111
- )
112
-
113
- async def status(self) -> ProviderStatus:
114
- status = "configured" if self.api_key else "unavailable"
115
- message = "Custom provider configured." if self.api_key else "Requires MARKET_DATA_API_KEY."
116
- return ProviderStatus(name=self.name, realtime=True, status=status, message=message)
117
-
118
- async def _get(self, path: str, params: dict[str, object] | None = None) -> Any:
119
- if not self.api_key:
120
- raise ProviderError(
121
- "API key custom market provider belum diatur.",
122
- "Gunakan /news_model key custom <api_key> <base_url>.",
123
- )
124
- if not self.base_url:
125
- raise ProviderError(
126
- "Base URL custom market provider belum diatur.",
127
- "Gunakan /news_model key custom <api_key> <base_url>.",
128
- )
129
-
130
- close_client = self._client is None
131
- client = self._client or httpx.AsyncClient(timeout=30)
132
- headers = {"X-API-Key": self.api_key, "Authorization": f"Bearer {self.api_key}"}
133
- try:
134
- response = await client.get(f"{self.base_url}{path}", params=params, headers=headers)
135
- if response.status_code == 429:
136
- raise RateLimitError("Custom market provider terkena rate limit.")
137
- response.raise_for_status()
138
- return response.json()
139
- except httpx.TimeoutException as exc:
140
- raise ProviderError("Custom market provider timeout.") from exc
141
- except httpx.HTTPStatusError as exc:
142
- raise ProviderError(f"Custom market provider gagal: HTTP {exc.response.status_code}.") from exc
143
- except ValueError as exc:
144
- raise ProviderError("Response custom market provider bukan JSON valid.") from exc
145
- finally:
146
- if close_client:
147
- await client.aclose()
148
-
149
-
150
- def _safe_float(value: Any) -> float | None:
151
- try:
152
- if value is None:
153
- return None
154
- return float(value)
155
- except (TypeError, ValueError):
156
- return None
157
-
158
-
159
- def _parse_datetime(value: Any) -> datetime | None:
160
- if value is None:
161
- return None
162
- if isinstance(value, (int, float)):
163
- return datetime.fromtimestamp(value)
164
- if isinstance(value, str):
165
- try:
166
- return datetime.fromisoformat(value.replace("Z", "+00:00"))
167
- except ValueError:
168
- return None
169
- return None
1
+ """Custom HTTP market API provider.
2
+
3
+ Expected endpoint contract:
4
+ - GET /quote/{symbol}
5
+ - GET /history/{symbol}?period=6mo&interval=1d
6
+ - GET /news/{symbol}?limit=5
7
+ - GET /fundamentals/{symbol}
8
+
9
+ The provider accepts common JSON key variants so users can adapt simple APIs
10
+ without changing FinCLI core.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from datetime import datetime
16
+ from typing import Any
17
+
18
+ import httpx
19
+
20
+ from fincli.app.providers.market.base import (
21
+ BaseMarketProvider,
22
+ Candle,
23
+ FundamentalSnapshot,
24
+ NewsItem,
25
+ ProviderStatus,
26
+ Quote,
27
+ )
28
+ from fincli.app.utils.errors import ProviderError, RateLimitError
29
+
30
+
31
+ class CustomMarketProvider(BaseMarketProvider):
32
+ name = "custom"
33
+
34
+ def __init__(
35
+ self,
36
+ api_key: str | None,
37
+ base_url: str,
38
+ client: httpx.AsyncClient | None = None,
39
+ ) -> None:
40
+ self.api_key = api_key or ""
41
+ self.base_url = base_url.rstrip("/")
42
+ self._client = client
43
+
44
+ async def quote(self, symbol: str) -> Quote:
45
+ data = await self._get(f"/quote/{symbol.upper()}")
46
+ data = _require_mapping(data, "quote")
47
+ price = _required_float(data.get("price") or data.get("last") or data.get("last_price"), "quote.price")
48
+ return Quote(
49
+ symbol=str(data.get("symbol") or symbol).upper(),
50
+ price=price,
51
+ currency=str(data.get("currency") or "USD"),
52
+ provider=self.name,
53
+ timestamp=_parse_datetime(data.get("timestamp")) or datetime.now(),
54
+ status=str(data.get("status") or "custom"),
55
+ )
56
+
57
+ async def history(self, symbol: str, period: str = "6mo", interval: str = "1d") -> list[Candle]:
58
+ data = await self._get(f"/history/{symbol.upper()}", params={"period": period, "interval": interval})
59
+ raw_items = data.get("candles") if isinstance(data, dict) else data
60
+ if not isinstance(raw_items, list):
61
+ raise ProviderError("Response history custom provider tidak valid.")
62
+ candles: list[Candle] = []
63
+ for item in raw_items:
64
+ if not isinstance(item, dict):
65
+ continue
66
+ candles.append(
67
+ Candle(
68
+ timestamp=_parse_datetime(item.get("timestamp") or item.get("date")) or datetime.now(),
69
+ open=_required_float(item.get("open") or item.get("o"), "history.open"),
70
+ high=_required_float(item.get("high") or item.get("h"), "history.high"),
71
+ low=_required_float(item.get("low") or item.get("l"), "history.low"),
72
+ close=_required_float(item.get("close") or item.get("c"), "history.close"),
73
+ volume=float(item.get("volume") or item.get("v") or 0),
74
+ )
75
+ )
76
+ if not candles:
77
+ raise ProviderError(f"Data OHLCV kosong untuk {symbol}.")
78
+ return candles
79
+
80
+ async def news(self, symbol: str, limit: int = 5) -> list[NewsItem]:
81
+ data = await self._get(f"/news/{symbol.upper()}", params={"limit": limit})
82
+ raw_items = data.get("news") if isinstance(data, dict) else data
83
+ if not isinstance(raw_items, list):
84
+ raise ProviderError("Response news custom provider tidak valid.")
85
+ items: list[NewsItem] = []
86
+ for item in raw_items[:limit]:
87
+ if not isinstance(item, dict):
88
+ continue
89
+ items.append(
90
+ NewsItem(
91
+ title=str(item.get("title") or "Untitled"),
92
+ source=str(item.get("source") or self.name),
93
+ url=item.get("url"),
94
+ published_at=_parse_datetime(item.get("published_at") or item.get("timestamp")),
95
+ summary=str(item.get("summary") or ""),
96
+ )
97
+ )
98
+ return items
99
+
100
+ async def fundamentals(self, symbol: str) -> FundamentalSnapshot:
101
+ data = await self._get(f"/fundamentals/{symbol.upper()}")
102
+ data = _require_mapping(data, "fundamentals")
103
+ return FundamentalSnapshot(
104
+ symbol=str(data.get("symbol") or symbol).upper(),
105
+ provider=self.name,
106
+ currency=str(data.get("currency") or "USD"),
107
+ market_cap=_safe_float(data.get("market_cap") or data.get("marketCap")),
108
+ pe_ratio=_safe_float(data.get("pe_ratio") or data.get("trailingPE")),
109
+ eps=_safe_float(data.get("eps") or data.get("trailingEps")),
110
+ revenue=_safe_float(data.get("revenue") or data.get("totalRevenue")),
111
+ beta=_safe_float(data.get("beta")),
112
+ sector=data.get("sector"),
113
+ industry=data.get("industry"),
114
+ )
115
+
116
+ async def status(self) -> ProviderStatus:
117
+ status = "configured" if self.api_key else "unavailable"
118
+ message = "Custom provider configured." if self.api_key else "Requires MARKET_DATA_API_KEY."
119
+ return ProviderStatus(name=self.name, realtime=True, status=status, message=message)
120
+
121
+ async def _get(self, path: str, params: dict[str, object] | None = None) -> Any:
122
+ if not self.api_key:
123
+ raise ProviderError(
124
+ "API key custom market provider belum diatur.",
125
+ "Gunakan /news_model key custom <api_key> <base_url>.",
126
+ )
127
+ if not self.base_url:
128
+ raise ProviderError(
129
+ "Base URL custom market provider belum diatur.",
130
+ "Gunakan /news_model key custom <api_key> <base_url>.",
131
+ )
132
+
133
+ close_client = self._client is None
134
+ client = self._client or httpx.AsyncClient(timeout=30)
135
+ headers = {"X-API-Key": self.api_key, "Authorization": f"Bearer {self.api_key}"}
136
+ try:
137
+ response = await client.get(f"{self.base_url}{path}", params=params, headers=headers)
138
+ if response.status_code == 429:
139
+ raise RateLimitError("Custom market provider terkena rate limit.")
140
+ response.raise_for_status()
141
+ data = response.json()
142
+ return data
143
+ except httpx.TimeoutException as exc:
144
+ raise ProviderError("Custom market provider timeout.") from exc
145
+ except httpx.HTTPStatusError as exc:
146
+ raise ProviderError(f"Custom market provider gagal: HTTP {exc.response.status_code}.") from exc
147
+ except ValueError as exc:
148
+ raise ProviderError("Response custom market provider bukan JSON valid.") from exc
149
+ finally:
150
+ if close_client:
151
+ await client.aclose()
152
+
153
+
154
+ def _safe_float(value: Any) -> float | None:
155
+ try:
156
+ if value is None:
157
+ return None
158
+ return float(value)
159
+ except (TypeError, ValueError):
160
+ return None
161
+
162
+
163
+ def _required_float(value: Any, field_name: str) -> float:
164
+ number = _safe_float(value)
165
+ if number is None:
166
+ raise ProviderError(f"Response custom provider tidak valid: {field_name} wajib berupa angka.")
167
+ return number
168
+
169
+
170
+ def _require_mapping(value: Any, section: str) -> dict[str, Any]:
171
+ if not isinstance(value, dict):
172
+ raise ProviderError(f"Response {section} custom provider tidak valid: root JSON harus object.")
173
+ return value
174
+
175
+
176
+ def _parse_datetime(value: Any) -> datetime | None:
177
+ if value is None:
178
+ return None
179
+ if isinstance(value, (int, float)):
180
+ return datetime.fromtimestamp(value)
181
+ if isinstance(value, str):
182
+ try:
183
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
184
+ except ValueError:
185
+ return None
186
+ return None
@@ -5,7 +5,8 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass
6
6
  import os
7
7
 
8
- from fincli.app.providers.market.base import BaseMarketProvider
8
+ from fincli.app.providers.market.base import BaseMarketProvider, ProviderEntitlement
9
+ from fincli.app.providers.market.alphavantage_provider import AlphaVantageProvider
9
10
  from fincli.app.providers.market.custom_provider import CustomMarketProvider
10
11
  from fincli.app.providers.market.finnhub_provider import FinnhubProvider
11
12
  from fincli.app.providers.market.twelvedata_provider import TwelveDataProvider
@@ -46,6 +47,12 @@ MARKET_PROVIDERS: dict[str, MarketProviderInfo] = {
46
47
  status="configured",
47
48
  notes="Multi-asset provider for stocks, forex, ETFs, indices, commodities, and crypto. Requires TWELVE_DATA_API_KEY.",
48
49
  ),
50
+ "alphavantage": MarketProviderInfo(
51
+ name="alphavantage",
52
+ realtime=False,
53
+ status="configured",
54
+ notes="Alpha Vantage adapter for stocks, FX, news sentiment, and company overview. Requires ALPHA_VANTAGE_API_KEY.",
55
+ ),
49
56
  }
50
57
 
51
58
 
@@ -71,6 +78,8 @@ class MarketProviderManager:
71
78
  return FinnhubProvider(api_key=os.getenv("FINNHUB_API_KEY"))
72
79
  if provider_name == "twelvedata":
73
80
  return TwelveDataProvider(api_key=os.getenv("TWELVE_DATA_API_KEY"))
81
+ if provider_name == "alphavantage":
82
+ return AlphaVantageProvider(api_key=os.getenv("ALPHA_VANTAGE_API_KEY"))
74
83
  raise ValueError(f"Market provider tidak dikenal: {name}")
75
84
 
76
85
  def create_many(self, names: list[str]) -> list[BaseMarketProvider]:
@@ -113,6 +122,80 @@ class MarketProviderManager:
113
122
  "status": _mask_status(os.getenv("TWELVE_DATA_API_KEY")),
114
123
  "source": secret_source("TWELVE_DATA_API_KEY"),
115
124
  },
125
+ {
126
+ "provider": "alphavantage",
127
+ "key": "ALPHA_VANTAGE_API_KEY",
128
+ "status": _mask_status(os.getenv("ALPHA_VANTAGE_API_KEY")),
129
+ "source": secret_source("ALPHA_VANTAGE_API_KEY"),
130
+ },
131
+ ]
132
+
133
+ def entitlements(self) -> list[ProviderEntitlement]:
134
+ return [
135
+ ProviderEntitlement(
136
+ provider="yfinance",
137
+ status="available",
138
+ realtime_label="delayed/fallback",
139
+ asset_classes=("stocks", "ETFs", "indices", "forex", "crypto", "commodities", "funds"),
140
+ capabilities=("quote", "history", "news", "fundamentals", "yahoo tables"),
141
+ limitations=(
142
+ "No API key required.",
143
+ "Data may be delayed and Yahoo coverage varies by exchange.",
144
+ "Not suitable for guaranteed realtime execution workflows.",
145
+ ),
146
+ ),
147
+ ProviderEntitlement(
148
+ provider="twelvedata",
149
+ status="configured" if os.getenv("TWELVE_DATA_API_KEY") else "missing key",
150
+ realtime_label="plan-dependent",
151
+ asset_classes=("stocks", "ETFs", "indices", "forex", "crypto", "commodities"),
152
+ capabilities=("quote", "history"),
153
+ limitations=(
154
+ "Requires TWELVE_DATA_API_KEY.",
155
+ "Realtime access depends on plan and exchange entitlement.",
156
+ "News/fundamentals are not implemented in this adapter yet.",
157
+ ),
158
+ ),
159
+ ProviderEntitlement(
160
+ provider="finnhub",
161
+ status="configured" if os.getenv("FINNHUB_API_KEY") else "missing key",
162
+ realtime_label="plan-dependent",
163
+ asset_classes=("stocks", "forex", "crypto"),
164
+ capabilities=("quote", "history", "news", "fundamentals", "economic calendar"),
165
+ limitations=(
166
+ "Requires FINNHUB_API_KEY.",
167
+ "Forex/crypto support is strongest for candles.",
168
+ "News/fundamentals coverage is strongest for equities.",
169
+ ),
170
+ ),
171
+ ProviderEntitlement(
172
+ provider="custom",
173
+ status=(
174
+ "configured"
175
+ if os.getenv("MARKET_DATA_API_KEY") and os.getenv("MARKET_DATA_BASE_URL")
176
+ else "missing key/base url"
177
+ ),
178
+ realtime_label="custom",
179
+ asset_classes=("provider-defined",),
180
+ capabilities=("quote", "history", "news", "fundamentals"),
181
+ limitations=(
182
+ "Requires MARKET_DATA_API_KEY and MARKET_DATA_BASE_URL.",
183
+ "Realtime/delayed status must be supplied by the custom API payload.",
184
+ "Payloads are validated by FinCLI before being accepted.",
185
+ ),
186
+ ),
187
+ ProviderEntitlement(
188
+ provider="alphavantage",
189
+ status="configured" if os.getenv("ALPHA_VANTAGE_API_KEY") else "missing key",
190
+ realtime_label="delayed/plan-dependent",
191
+ asset_classes=("stocks", "forex", "selected crypto/commodities via provider functions"),
192
+ capabilities=("quote", "history", "news", "fundamentals"),
193
+ limitations=(
194
+ "Requires ALPHA_VANTAGE_API_KEY.",
195
+ "Free plans are heavily rate-limited.",
196
+ "Realtime availability and exchange coverage depend on Alpha Vantage plan.",
197
+ ),
198
+ ),
116
199
  ]
117
200
 
118
201