@drico2008/fincli 0.1.3 → 0.2.2

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. package/LICENSE +21 -0
  2. package/README.md +909 -684
  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 +23 -16
  7. package/fincli/app/analysis/analyzer.py +107 -100
  8. package/fincli/app/analysis/assistant_context.py +187 -160
  9. package/fincli/app/analysis/backtest.py +179 -0
  10. package/fincli/app/analysis/gameplay_plan.py +79 -0
  11. package/fincli/app/analysis/indicators.py +1 -1
  12. package/fincli/app/analysis/market_structure.py +1 -1
  13. package/fincli/app/analysis/multi_timeframe.py +180 -0
  14. package/fincli/app/analysis/trading_methods.py +144 -0
  15. package/fincli/app/cli/commands.py +105 -77
  16. package/fincli/app/cli/router.py +2143 -1121
  17. package/fincli/app/connectors/__init__.py +5 -0
  18. package/fincli/app/connectors/catalog.py +148 -0
  19. package/fincli/app/connectors/news_connectors.py +412 -0
  20. package/fincli/app/modules/alerts.py +80 -0
  21. package/fincli/app/modules/economic_calendar.py +374 -1
  22. package/fincli/app/modules/reports.py +151 -0
  23. package/fincli/app/modules/scanner.py +111 -93
  24. package/fincli/app/modules/session_history.py +113 -0
  25. package/fincli/app/modules/transactions.py +84 -84
  26. package/fincli/app/modules/user_profile.py +84 -0
  27. package/fincli/app/plugins/loader.py +72 -0
  28. package/fincli/app/providers/ai/anthropic_provider.py +8 -7
  29. package/fincli/app/providers/ai/gemini_provider.py +8 -7
  30. package/fincli/app/providers/ai/groq_provider.py +8 -7
  31. package/fincli/app/providers/ai/huggingface_provider.py +8 -7
  32. package/fincli/app/providers/ai/manager.py +60 -60
  33. package/fincli/app/providers/ai/openai_provider.py +8 -7
  34. package/fincli/app/providers/ai/openrouter_provider.py +8 -7
  35. package/fincli/app/providers/ai/together_provider.py +8 -7
  36. package/fincli/app/providers/market/alphavantage_provider.py +194 -0
  37. package/fincli/app/providers/market/base.py +98 -77
  38. package/fincli/app/providers/market/custom_provider.py +186 -169
  39. package/fincli/app/providers/market/manager.py +85 -2
  40. package/fincli/app/providers/market/news_provider.py +4 -4
  41. package/fincli/app/providers/market/symbols.py +143 -0
  42. package/fincli/app/providers/market/twelvedata_provider.py +167 -167
  43. package/fincli/app/providers/market/yfinance_provider.py +1 -1
  44. package/fincli/app/research/__init__.py +7 -0
  45. package/fincli/app/research/engine.py +75 -0
  46. package/fincli/app/research/formatter.py +22 -0
  47. package/fincli/app/research/models.py +18 -0
  48. package/fincli/app/research/prompt_builder.py +47 -0
  49. package/fincli/app/services/macro_data.py +50 -0
  50. package/fincli/app/services/market_data.py +203 -203
  51. package/fincli/app/services/news_aggregator.py +90 -0
  52. package/fincli/app/services/web_research.py +267 -0
  53. package/fincli/app/storage/cache.py +2 -2
  54. package/fincli/app/storage/config.py +122 -88
  55. package/fincli/app/storage/database.py +201 -85
  56. package/fincli/app/storage/secrets.py +12 -3
  57. package/fincli/app/tui/components.py +68 -50
  58. package/fincli/app/tui/layout.py +270 -258
  59. package/fincli/app/tui/market_provider_selector.py +6 -1
  60. package/fincli/app/tui/model_selector.py +11 -3
  61. package/fincli/app/tui/theme.py +134 -74
  62. package/fincli/app/utils/formatting.py +125 -12
  63. package/npm/bin/fincli.js +9 -2
  64. package/package.json +23 -23
  65. package/pyproject.toml +35 -35
@@ -0,0 +1,180 @@
1
+ """Multi-timeframe technical alignment analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from dataclasses import dataclass
7
+ from typing import Protocol
8
+
9
+ from fincli.app.analysis.indicators import TechnicalSummary, summarize_technical_indicators
10
+ from fincli.app.analysis.market_structure import MarketStructureSummary, analyze_market_structure
11
+ from fincli.app.providers.market.base import Candle
12
+
13
+
14
+ class HistoryProvider(Protocol):
15
+ async def history(self, symbol: str, period: str = "6mo", interval: str = "1d") -> list[Candle]:
16
+ """Fetch candles for a symbol/timeframe."""
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class TimeframeAnalysis:
21
+ timeframe: str
22
+ status: str
23
+ candles: int
24
+ latest_close: float | None
25
+ trend_bias: str
26
+ structure_trend: str
27
+ rsi: float | None
28
+ macd: float | None
29
+ support: float | None
30
+ resistance: float | None
31
+ note: str = ""
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class MultiTimeframeAnalysis:
36
+ symbol: str
37
+ frames: list[TimeframeAnalysis]
38
+ alignment: str
39
+ score: int
40
+ bias: str
41
+ risk_note: str
42
+
43
+
44
+ async def analyze_multi_timeframe(
45
+ symbol: str,
46
+ provider: HistoryProvider,
47
+ timeframes: tuple[str, ...] = ("1d", "1h", "15m"),
48
+ ) -> MultiTimeframeAnalysis:
49
+ tasks = [_analyze_frame(symbol, provider, timeframe) for timeframe in timeframes]
50
+ frames = list(await asyncio.gather(*tasks))
51
+ score = sum(_frame_score(frame) for frame in frames if frame.status == "ready")
52
+ ready_count = sum(1 for frame in frames if frame.status == "ready")
53
+
54
+ if ready_count == 0:
55
+ return MultiTimeframeAnalysis(
56
+ symbol=symbol.upper(),
57
+ frames=frames,
58
+ alignment="unavailable",
59
+ score=0,
60
+ bias="caution",
61
+ risk_note="No timeframe returned enough data. Check provider, symbol format, and interval support.",
62
+ )
63
+
64
+ if score >= max(2, ready_count):
65
+ bias = "bullish"
66
+ elif score <= -max(2, ready_count):
67
+ bias = "bearish"
68
+ else:
69
+ bias = "mixed/caution"
70
+
71
+ alignment = _alignment_label(frames)
72
+ risk_note = _risk_note(frames, bias)
73
+ return MultiTimeframeAnalysis(symbol=symbol.upper(), frames=frames, alignment=alignment, score=score, bias=bias, risk_note=risk_note)
74
+
75
+
76
+ async def _analyze_frame(symbol: str, provider: HistoryProvider, timeframe: str) -> TimeframeAnalysis:
77
+ try:
78
+ candles = await provider.history(symbol, period=_period_for_timeframe(timeframe), interval=timeframe)
79
+ if len(candles) < 20:
80
+ return TimeframeAnalysis(
81
+ timeframe=timeframe,
82
+ status="insufficient",
83
+ candles=len(candles),
84
+ latest_close=candles[-1].close if candles else None,
85
+ trend_bias="neutral",
86
+ structure_trend="neutral",
87
+ rsi=None,
88
+ macd=None,
89
+ support=None,
90
+ resistance=None,
91
+ note="Need at least 20 candles for stable multi-timeframe summary.",
92
+ )
93
+ technical = summarize_technical_indicators(candles)
94
+ structure = analyze_market_structure(candles)
95
+ return _frame_from_summary(timeframe, candles, technical, structure)
96
+ except Exception as exc: # noqa: BLE001
97
+ return TimeframeAnalysis(
98
+ timeframe=timeframe,
99
+ status="unavailable",
100
+ candles=0,
101
+ latest_close=None,
102
+ trend_bias="neutral",
103
+ structure_trend="neutral",
104
+ rsi=None,
105
+ macd=None,
106
+ support=None,
107
+ resistance=None,
108
+ note=str(exc),
109
+ )
110
+
111
+
112
+ def _frame_from_summary(
113
+ timeframe: str,
114
+ candles: list[Candle],
115
+ technical: TechnicalSummary,
116
+ structure: MarketStructureSummary,
117
+ ) -> TimeframeAnalysis:
118
+ return TimeframeAnalysis(
119
+ timeframe=timeframe,
120
+ status="ready",
121
+ candles=len(candles),
122
+ latest_close=technical.latest_close,
123
+ trend_bias=technical.trend_bias,
124
+ structure_trend=structure.trend,
125
+ rsi=technical.rsi,
126
+ macd=technical.macd,
127
+ support=technical.support,
128
+ resistance=technical.resistance,
129
+ )
130
+
131
+
132
+ def _period_for_timeframe(timeframe: str) -> str:
133
+ normalized = timeframe.lower()
134
+ if normalized in {"1m", "5m", "15m", "30m", "1h", "4h"}:
135
+ return "60d"
136
+ if normalized in {"1w", "w"}:
137
+ return "2y"
138
+ return "1y"
139
+
140
+
141
+ def _frame_score(frame: TimeframeAnalysis) -> int:
142
+ score = 0
143
+ if frame.trend_bias == "bullish":
144
+ score += 1
145
+ elif frame.trend_bias == "bearish":
146
+ score -= 1
147
+ if frame.structure_trend == "bullish":
148
+ score += 1
149
+ elif frame.structure_trend == "bearish":
150
+ score -= 1
151
+ if frame.rsi is not None:
152
+ if frame.rsi > 70:
153
+ score -= 1
154
+ elif frame.rsi < 30:
155
+ score += 1
156
+ return score
157
+
158
+
159
+ def _alignment_label(frames: list[TimeframeAnalysis]) -> str:
160
+ ready = [frame for frame in frames if frame.status == "ready"]
161
+ if not ready:
162
+ return "unavailable"
163
+ trends = {frame.trend_bias for frame in ready}
164
+ structures = {frame.structure_trend for frame in ready}
165
+ if len(trends) == 1 and len(structures) == 1 and next(iter(trends)) == next(iter(structures)):
166
+ return f"aligned {next(iter(trends))}"
167
+ if all(frame.trend_bias in {"bullish", "neutral"} and frame.structure_trend in {"bullish", "neutral"} for frame in ready):
168
+ return "mostly bullish"
169
+ if all(frame.trend_bias in {"bearish", "neutral"} and frame.structure_trend in {"bearish", "neutral"} for frame in ready):
170
+ return "mostly bearish"
171
+ return "mixed"
172
+
173
+
174
+ def _risk_note(frames: list[TimeframeAnalysis], bias: str) -> str:
175
+ unavailable = [frame.timeframe for frame in frames if frame.status != "ready"]
176
+ if unavailable:
177
+ return f"{bias}; verify unavailable/insufficient timeframe(s): {', '.join(unavailable)}."
178
+ if bias == "mixed/caution":
179
+ return "Timeframes disagree. Prefer confirmation over directional conviction."
180
+ return f"{bias}; still validate support/resistance and news/fundamental context before acting."
@@ -0,0 +1,144 @@
1
+ """Trading-method context inspired by common SNR, volume, pivot, and gap workflows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from fincli.app.providers.market.base import Candle
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class TradingMethodContext:
12
+ nearest_support: float | None
13
+ nearest_resistance: float | None
14
+ support_break: bool
15
+ resistance_break: bool
16
+ bull_wick: bool
17
+ bear_wick: bool
18
+ volume_oscillator_percent: float | None
19
+ volume_confirmation: str
20
+ pivot_highs: list[float]
21
+ pivot_lows: list[float]
22
+ latest_gap: str
23
+ method_notes: list[str]
24
+
25
+
26
+ def analyze_trading_methods(candles: list[Candle], left: int = 3, right: int = 3) -> TradingMethodContext:
27
+ if not candles:
28
+ raise ValueError("Data candle kosong.")
29
+
30
+ pivot_highs, pivot_lows = _pivots(candles, left, right)
31
+ recent = candles[-20:]
32
+ latest = candles[-1]
33
+ previous = candles[-2] if len(candles) >= 2 else candles[-1]
34
+ support = _nearest_below(pivot_lows, latest.close) or min(float(candle.low) for candle in recent)
35
+ resistance = _nearest_above(pivot_highs, latest.close) or max(float(candle.high) for candle in recent)
36
+ volume_osc = _volume_oscillator(candles)
37
+ volume_ok = volume_osc is not None and volume_osc > 20
38
+
39
+ support_break = latest.close < support and volume_ok
40
+ resistance_break = latest.close > resistance and volume_ok
41
+ body = abs(latest.close - latest.open)
42
+ upper_wick = latest.high - max(latest.open, latest.close)
43
+ lower_wick = min(latest.open, latest.close) - latest.low
44
+ bull_wick = latest.close > resistance and lower_wick > body
45
+ bear_wick = latest.close < support and upper_wick > body
46
+ latest_gap = _latest_gap(previous, latest)
47
+
48
+ notes = [
49
+ "SNR uses pivot highs/lows and recent range when pivots are sparse.",
50
+ "Break confirmation requires close beyond level plus volume oscillator above 20%.",
51
+ "Wick labels flag rejection risk around levels.",
52
+ "Gap context is descriptive; confirm with liquidity and follow-through.",
53
+ ]
54
+ return TradingMethodContext(
55
+ nearest_support=support,
56
+ nearest_resistance=resistance,
57
+ support_break=support_break,
58
+ resistance_break=resistance_break,
59
+ bull_wick=bull_wick,
60
+ bear_wick=bear_wick,
61
+ volume_oscillator_percent=volume_osc,
62
+ volume_confirmation="confirmed" if volume_ok else "not confirmed",
63
+ pivot_highs=pivot_highs[-5:],
64
+ pivot_lows=pivot_lows[-5:],
65
+ latest_gap=latest_gap,
66
+ method_notes=notes,
67
+ )
68
+
69
+
70
+ def format_trading_methods_context(context: TradingMethodContext) -> str:
71
+ return (
72
+ "Trading Method Context:\n"
73
+ f"- SNR/Pivot: support={_fmt(context.nearest_support)}, resistance={_fmt(context.nearest_resistance)}, "
74
+ f"pivot_highs={_fmt_list(context.pivot_highs)}, pivot_lows={_fmt_list(context.pivot_lows)}\n"
75
+ f"- Break Logic: resistance_break={context.resistance_break}, support_break={context.support_break}, "
76
+ f"bull_wick={context.bull_wick}, bear_wick={context.bear_wick}\n"
77
+ f"- Volume: oscillator={_fmt(context.volume_oscillator_percent)}%, confirmation={context.volume_confirmation}\n"
78
+ f"- Gap: {context.latest_gap}\n"
79
+ f"- Method Notes: {'; '.join(context.method_notes)}"
80
+ )
81
+
82
+
83
+ def _pivots(candles: list[Candle], left: int, right: int) -> tuple[list[float], list[float]]:
84
+ highs: list[float] = []
85
+ lows: list[float] = []
86
+ if len(candles) < left + right + 1:
87
+ return highs, lows
88
+ for index in range(left, len(candles) - right):
89
+ window = candles[index - left : index + right + 1]
90
+ current = candles[index]
91
+ if current.high == max(candle.high for candle in window):
92
+ highs.append(float(current.high))
93
+ if current.low == min(candle.low for candle in window):
94
+ lows.append(float(current.low))
95
+ return highs, lows
96
+
97
+
98
+ def _nearest_below(levels: list[float], price: float) -> float | None:
99
+ candidates = [level for level in levels if level <= price]
100
+ return max(candidates) if candidates else None
101
+
102
+
103
+ def _nearest_above(levels: list[float], price: float) -> float | None:
104
+ candidates = [level for level in levels if level >= price]
105
+ return min(candidates) if candidates else None
106
+
107
+
108
+ def _volume_oscillator(candles: list[Candle]) -> float | None:
109
+ volumes = [float(candle.volume) for candle in candles]
110
+ if len(volumes) < 10:
111
+ return None
112
+ short = _ema(volumes, 5)[-1]
113
+ long = _ema(volumes, 10)[-1]
114
+ if long == 0:
115
+ return None
116
+ return 100 * (short - long) / long
117
+
118
+
119
+ def _ema(values: list[float], span: int) -> list[float]:
120
+ alpha = 2 / (span + 1)
121
+ result = [values[0]]
122
+ for value in values[1:]:
123
+ result.append((value * alpha) + result[-1] * (1 - alpha))
124
+ return result
125
+
126
+
127
+ def _latest_gap(previous: Candle, latest: Candle) -> str:
128
+ if latest.low > previous.high:
129
+ return f"gap up above previous high {_fmt(previous.high)}"
130
+ if latest.high < previous.low:
131
+ return f"gap down below previous low {_fmt(previous.low)}"
132
+ return "no open gap against previous candle"
133
+
134
+
135
+ def _fmt(value: float | None) -> str:
136
+ if value is None:
137
+ return "N/A"
138
+ return f"{value:.4f}"
139
+
140
+
141
+ def _fmt_list(values: list[float]) -> str:
142
+ if not values:
143
+ return "[]"
144
+ return "[" + ", ".join(_fmt(value) for value in values) + "]"
@@ -1,84 +1,112 @@
1
- """Slash command registry."""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import dataclass
6
-
7
-
8
- @dataclass(frozen=True, slots=True)
9
- class CommandSpec:
10
- name: str
11
- description: str
12
- example: str
13
- group: str = "General"
14
-
15
-
16
- COMMANDS: tuple[CommandSpec, ...] = (
17
- CommandSpec("/help", "Tampilkan bantuan, command list, dan contoh.", "/help"),
18
- CommandSpec("/dashboard", "Tampilkan dashboard compact FinCLI.", "/dashboard", "General"),
19
- CommandSpec("/ai_model", "Lihat atau ganti AI provider/model.", "/ai_model openrouter openai/gpt-4o-mini", "AI"),
20
- CommandSpec("/ai_model key", "Simpan API key AI lokal.", "/ai_model key groq <api_key>", "AI"),
1
+ """Slash command registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class CommandSpec:
10
+ name: str
11
+ description: str
12
+ example: str
13
+ group: str = "General"
14
+
15
+
16
+ COMMANDS: tuple[CommandSpec, ...] = (
17
+ CommandSpec("/help", "Tampilkan bantuan, command list, dan contoh.", "/help"),
18
+ CommandSpec("/dashboard", "Tampilkan dashboard compact FinCLI.", "/dashboard", "General"),
19
+ CommandSpec("/ai_model", "Lihat atau ganti AI provider/model.", "/ai_model openrouter openai/gpt-4o-mini", "AI"),
20
+ CommandSpec("/ai_model key", "Simpan API key AI lokal.", "/ai_model key groq <api_key>", "AI"),
21
21
  CommandSpec("/news_model", "Buka selector provider market/news dan fallback.", "/news_model", "Provider"),
22
- CommandSpec("/news_model key", "Simpan API key market/news lokal.", "/news_model key finnhub <api_key>", "Provider"),
22
+ CommandSpec("/news_model list", "Tampilkan 100+ news connector dan status akses.", "/news_model list", "Provider"),
23
+ CommandSpec("/news_model search", "Cari connector news.", "/news_model search rss", "Provider"),
24
+ CommandSpec("/news_model use", "Pilih primary news provider.", "/news_model use google_news_rss", "Provider"),
25
+ CommandSpec("/news_model priority", "Atur fallback news provider.", "/news_model priority google_news_rss,yfinance,marketaux", "Provider"),
26
+ CommandSpec("/news_model key", "Simpan API key news connector lokal.", "/news_model key marketaux <api_key>", "Provider"),
27
+ CommandSpec("/symbol", "Search symbol dan tampilkan normalisasi per provider.", "/symbol XAUUSD", "Market"),
28
+ CommandSpec("/research", "Pusat riset ringkas: market, technical, news, fundamental, dan AI deep mode.", "/research AAPL --quick", "Research"),
29
+ CommandSpec("/macro", "Dashboard macro fallback dan connector-ready context.", "/macro Indonesia", "Research"),
30
+ CommandSpec("/profile", "Tampilkan profil dan gameplay risk lokal.", "/profile", "Profile"),
31
+ CommandSpec("/profile set", "Simpan profil gameplay lokal.", '/profile set "Budi" 350 USD 1:100 1.5', "Profile"),
32
+ CommandSpec("/doctor", "Cek kesehatan konfigurasi, provider, database, dan command inti.", "/doctor", "System"),
33
+ CommandSpec("/setup", "Panduan setup lokal untuk API key, provider, dan profile.", "/setup", "System"),
34
+ CommandSpec("/agent", "Lihat agent framework FinCLI.", "/agent list", "AI"),
35
+ CommandSpec("/agent show", "Tampilkan detail agent framework.", "/agent show buffett", "AI"),
36
+ CommandSpec("/connector", "Lihat catalog data connector.", "/connector list macro", "Provider"),
37
+ CommandSpec("/connector search", "Cari connector data.", "/connector search yahoo", "Provider"),
38
+ CommandSpec("/plugin", "Tampilkan plugin lokal FinCLI.", "/plugin list", "System"),
39
+ CommandSpec("/plugin status", "Cek status manifest plugin lokal.", "/plugin status", "System"),
23
40
  CommandSpec("/market", "Ringkasan market profesional untuk instrumen.", "/market AAPL 1d", "Market"),
24
41
  CommandSpec("/news", "Tampilkan news/fundamental terbaru untuk instrumen.", "/news AAPL", "Market"),
25
42
  CommandSpec("/technical", "Analisis teknikal instrumen.", "/technical BTC-USD 1d", "Analysis"),
26
- CommandSpec("/structure", "Analisis struktur pasar instrumen.", "/structure BTC-USD 1d", "Analysis"),
27
- CommandSpec("/funda", "Fundamental ringkas instrumen.", "/funda MSFT", "Market"),
43
+ CommandSpec("/mtf", "Multi-timeframe technical alignment.", "/mtf AAPL 1d,1h,15m", "Analysis"),
44
+ CommandSpec("/backtest", "Lightweight rule-based strategy backtest.", "/backtest AAPL sma_cross 1d", "Analysis"),
28
45
  CommandSpec("/yahoo", "Tampilkan tabel Yahoo Finance untuk history/statistics/profile/financials/analysis/holders.", "/yahoo BBRI statistics", "Market"),
29
- CommandSpec("/ai", "Free chat dengan AI assistant.", "/ai ringkas risiko AAPL", "AI"),
30
- CommandSpec("/analyze", "AI menganalisis struktur pasar instrumen.", "/analyze ETH-USD 4h", "Analysis"),
31
- CommandSpec("/watchlist", "Tampilkan watchlist.", "/watchlist", "Watchlist"),
32
- CommandSpec("/watchlist add", "Tambahkan instrumen ke watchlist.", "/watchlist add AAPL", "Watchlist"),
33
- CommandSpec("/watchlist remove", "Hapus instrumen dari watchlist.", "/watchlist remove AAPL", "Watchlist"),
34
- CommandSpec("/portfolio", "Tampilkan portfolio lokal.", "/portfolio", "Portfolio"),
35
- CommandSpec("/portfolio add", "Tambahkan posisi/aset.", "/portfolio add BTC-USD 0.05 65000", "Portfolio"),
36
- CommandSpec("/portfolio remove", "Hapus posisi/aset.", "/portfolio remove BTC-USD", "Portfolio"),
37
- CommandSpec("/portfolio performance", "Tampilkan performa portfolio.", "/portfolio performance", "Portfolio"),
38
- CommandSpec("/tx", "Tampilkan transaction ledger.", "/tx list", "Portfolio"),
39
- CommandSpec("/tx add", "Tambahkan transaksi buy/sell.", "/tx add buy AAPL 10 185", "Portfolio"),
40
- CommandSpec("/journal", "Tampilkan journal trading/investasi.", "/journal", "Journal"),
41
- CommandSpec("/journal add", "Tambahkan catatan journal singkat.", '/journal add BTC-USD bullish "Breakout gagal, tunggu konfirmasi"', "Journal"),
42
- CommandSpec("/journal stats", "Tampilkan statistik journal.", "/journal stats", "Journal"),
43
- CommandSpec("/journal review", "AI review kebiasaan journal.", "/journal review", "Journal"),
44
- CommandSpec("/config", "Tampilkan konfigurasi aktif tanpa membocorkan API key.", "/config"),
45
- CommandSpec("/price", "Tampilkan harga instrumen.", "/price NVDA", "Market"),
46
- CommandSpec("/quote", "Alias harga/quote instrumen.", "/quote NVDA", "Market"),
46
+ CommandSpec("/ai", "Free chat dengan AI assistant.", "/ai ringkas risiko AAPL", "AI"),
47
+ CommandSpec("/analyze", "AI menganalisis struktur pasar instrumen.", "/analyze ETH-USD 4h", "Analysis"),
48
+ CommandSpec("/watchlist", "Tampilkan watchlist.", "/watchlist", "Watchlist"),
49
+ CommandSpec("/watchlist add", "Tambahkan instrumen ke watchlist.", "/watchlist add AAPL", "Watchlist"),
50
+ CommandSpec("/watchlist remove", "Hapus instrumen dari watchlist.", "/watchlist remove AAPL", "Watchlist"),
51
+ CommandSpec("/portfolio", "Tampilkan portfolio lokal.", "/portfolio", "Portfolio"),
52
+ CommandSpec("/portfolio add", "Tambahkan posisi/aset.", "/portfolio add BTC-USD 0.05 65000", "Portfolio"),
53
+ CommandSpec("/portfolio remove", "Hapus posisi/aset.", "/portfolio remove BTC-USD", "Portfolio"),
54
+ CommandSpec("/portfolio performance", "Tampilkan performa portfolio.", "/portfolio performance", "Portfolio"),
55
+ CommandSpec("/tx", "Tampilkan transaction ledger.", "/tx list", "Portfolio"),
56
+ CommandSpec("/tx add", "Tambahkan transaksi buy/sell.", "/tx add buy AAPL 10 185", "Portfolio"),
57
+ CommandSpec("/journal", "Tampilkan journal trading/investasi.", "/journal", "Journal"),
58
+ CommandSpec("/journal add", "Tambahkan catatan journal singkat.", '/journal add BTC-USD bullish "Breakout gagal, tunggu konfirmasi"', "Journal"),
59
+ CommandSpec("/journal stats", "Tampilkan statistik journal.", "/journal stats", "Journal"),
60
+ CommandSpec("/journal review", "AI review kebiasaan journal.", "/journal review", "Journal"),
61
+ CommandSpec("/alert", "Tampilkan alert harga lokal.", "/alert", "Alert"),
62
+ CommandSpec("/alert add", "Tambahkan alert harga.", "/alert add AAPL above 200", "Alert"),
63
+ CommandSpec("/alert check", "Cek alert aktif memakai quote provider.", "/alert check", "Alert"),
64
+ CommandSpec("/history", "Tampilkan command history current session.", "/history", "History"),
65
+ CommandSpec("/history sessions", "Tampilkan daftar session tersimpan.", "/history sessions", "History"),
66
+ CommandSpec("/history show", "Tampilkan detail session tertentu.", "/history show <session_id>", "History"),
67
+ CommandSpec("/history save", "Beri nama current session.", '/history save "Riset IHSG pagi"', "History"),
68
+ CommandSpec("/history delete", "Hapus session tertentu.", "/history delete <session_id>", "History"),
69
+ CommandSpec("/config", "Tampilkan konfigurasi aktif tanpa membocorkan API key.", "/config"),
47
70
  CommandSpec("/scan", "Scanner watchlist dengan filter indikator.", "/scan watchlist rsi<30", "Market"),
48
- CommandSpec("/calendar", "Economic calendar provider/fallback.", "/calendar week US high", "Market"),
71
+ CommandSpec("/scan export", "Export hasil scanner watchlist ke CSV/JSON.", "/scan export csv scan.csv rsi<30 1d", "Market"),
72
+ CommandSpec("/report market", "Export market report ke Markdown/JSON.", "/report market AAPL md report.md", "Export"),
73
+ CommandSpec("/calendar", "Economic calendar provider/fallback.", "/calendar week US high", "Market"),
74
+ CommandSpec("/calendar export", "Export economic calendar ke CSV/JSON.", "/calendar export csv calendar.csv week US high", "Market"),
49
75
  CommandSpec("/provider status", "Tampilkan status provider aktif.", "/provider status", "Provider"),
50
- CommandSpec("/provider list", "Tampilkan semua provider market yang tersedia.", "/provider list", "Provider"),
51
- CommandSpec("/provider test", "Test quote provider aktif untuk symbol.", "/provider test AAPL", "Provider"),
52
- CommandSpec("/provider key status", "Tampilkan status API key market provider.", "/provider key status", "Provider"),
53
- CommandSpec("/cache stats", "Tampilkan statistik cache market persistent.", "/cache stats", "System"),
54
- CommandSpec("/cache clear", "Bersihkan runtime dan persistent market cache.", "/cache clear", "System"),
55
- CommandSpec("/export journal", "Export journal. Phase 1 menyiapkan command.", "/export journal", "Export"),
56
- CommandSpec("/export portfolio", "Export portfolio. Phase 1 menyiapkan command.", "/export portfolio", "Export"),
57
- CommandSpec("/clear", "Bersihkan output terminal.", "/clear"),
58
- CommandSpec("/exit", "Keluar dari aplikasi.", "/exit"),
59
- )
60
-
61
-
62
- class CommandRegistry:
63
- """Lookup and autocomplete slash commands."""
64
-
65
- def __init__(self, commands: tuple[CommandSpec, ...] = COMMANDS) -> None:
66
- self.commands = commands
67
-
68
- def suggest(self, query: str, limit: int = 8) -> list[CommandSpec]:
69
- normalized = query.strip().lower()
70
- if not normalized:
71
- return list(self.commands[:limit])
72
- if not normalized.startswith("/"):
73
- normalized = f"/{normalized}"
74
-
75
- exact = [cmd for cmd in self.commands if cmd.name.lower().startswith(normalized)]
76
- fuzzy = [cmd for cmd in self.commands if normalized.replace("/", "") in cmd.name.lower().replace("/", "")]
77
- merged: list[CommandSpec] = []
78
- for cmd in [*exact, *fuzzy]:
79
- if cmd not in merged:
80
- merged.append(cmd)
81
- return merged[:limit]
82
-
83
- def all(self) -> tuple[CommandSpec, ...]:
84
- return self.commands
76
+ CommandSpec("/provider metrics", "Tampilkan metric runtime provider aktif.", "/provider metrics", "Provider"),
77
+ CommandSpec("/provider list", "Tampilkan semua provider market yang tersedia.", "/provider list", "Provider"),
78
+ CommandSpec("/provider entitlement", "Tampilkan capability dan realtime/delayed label provider.", "/provider entitlement", "Provider"),
79
+ CommandSpec("/provider test", "Test quote provider aktif untuk symbol.", "/provider test AAPL", "Provider"),
80
+ CommandSpec("/provider key status", "Tampilkan status API key market provider.", "/provider key status", "Provider"),
81
+ CommandSpec("/cache stats", "Tampilkan statistik cache market persistent.", "/cache stats", "System"),
82
+ CommandSpec("/cache clear", "Bersihkan runtime dan persistent market cache.", "/cache clear", "System"),
83
+ CommandSpec("/export journal", "Export journal ke CSV/JSON.", "/export journal csv journal.csv", "Export"),
84
+ CommandSpec("/export portfolio", "Export portfolio ke CSV/JSON.", "/export portfolio json portfolio.json", "Export"),
85
+ CommandSpec("/clear", "Bersihkan output terminal.", "/clear"),
86
+ CommandSpec("/exit", "Keluar dari aplikasi.", "/exit"),
87
+ )
88
+
89
+
90
+ class CommandRegistry:
91
+ """Lookup and autocomplete slash commands."""
92
+
93
+ def __init__(self, commands: tuple[CommandSpec, ...] = COMMANDS) -> None:
94
+ self.commands = commands
95
+
96
+ def suggest(self, query: str, limit: int = 8) -> list[CommandSpec]:
97
+ normalized = query.strip().lower()
98
+ if not normalized:
99
+ return list(self.commands[:limit])
100
+ if not normalized.startswith("/"):
101
+ normalized = f"/{normalized}"
102
+
103
+ exact = [cmd for cmd in self.commands if cmd.name.lower().startswith(normalized)]
104
+ fuzzy = [cmd for cmd in self.commands if normalized.replace("/", "") in cmd.name.lower().replace("/", "")]
105
+ merged: list[CommandSpec] = []
106
+ for cmd in [*exact, *fuzzy]:
107
+ if cmd not in merged:
108
+ merged.append(cmd)
109
+ return merged[:limit]
110
+
111
+ def all(self) -> tuple[CommandSpec, ...]:
112
+ return self.commands