@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.
- package/LICENSE +21 -0
- package/README.md +909 -684
- package/fincli/__init__.py +3 -3
- package/fincli/app/agents/__init__.py +5 -0
- package/fincli/app/agents/registry.py +76 -0
- package/fincli/app/analysis/ai_prompts.py +23 -16
- package/fincli/app/analysis/analyzer.py +107 -100
- package/fincli/app/analysis/assistant_context.py +187 -160
- package/fincli/app/analysis/backtest.py +179 -0
- package/fincli/app/analysis/gameplay_plan.py +79 -0
- package/fincli/app/analysis/indicators.py +1 -1
- package/fincli/app/analysis/market_structure.py +1 -1
- package/fincli/app/analysis/multi_timeframe.py +180 -0
- package/fincli/app/analysis/trading_methods.py +144 -0
- package/fincli/app/cli/commands.py +105 -77
- package/fincli/app/cli/router.py +2143 -1121
- package/fincli/app/connectors/__init__.py +5 -0
- package/fincli/app/connectors/catalog.py +148 -0
- package/fincli/app/connectors/news_connectors.py +412 -0
- package/fincli/app/modules/alerts.py +80 -0
- package/fincli/app/modules/economic_calendar.py +374 -1
- package/fincli/app/modules/reports.py +151 -0
- package/fincli/app/modules/scanner.py +111 -93
- package/fincli/app/modules/session_history.py +113 -0
- package/fincli/app/modules/transactions.py +84 -84
- package/fincli/app/modules/user_profile.py +84 -0
- package/fincli/app/plugins/loader.py +72 -0
- package/fincli/app/providers/ai/anthropic_provider.py +8 -7
- package/fincli/app/providers/ai/gemini_provider.py +8 -7
- package/fincli/app/providers/ai/groq_provider.py +8 -7
- package/fincli/app/providers/ai/huggingface_provider.py +8 -7
- package/fincli/app/providers/ai/manager.py +60 -60
- package/fincli/app/providers/ai/openai_provider.py +8 -7
- package/fincli/app/providers/ai/openrouter_provider.py +8 -7
- package/fincli/app/providers/ai/together_provider.py +8 -7
- package/fincli/app/providers/market/alphavantage_provider.py +194 -0
- package/fincli/app/providers/market/base.py +98 -77
- package/fincli/app/providers/market/custom_provider.py +186 -169
- package/fincli/app/providers/market/manager.py +85 -2
- package/fincli/app/providers/market/news_provider.py +4 -4
- package/fincli/app/providers/market/symbols.py +143 -0
- package/fincli/app/providers/market/twelvedata_provider.py +167 -167
- package/fincli/app/providers/market/yfinance_provider.py +1 -1
- package/fincli/app/research/__init__.py +7 -0
- package/fincli/app/research/engine.py +75 -0
- package/fincli/app/research/formatter.py +22 -0
- package/fincli/app/research/models.py +18 -0
- package/fincli/app/research/prompt_builder.py +47 -0
- package/fincli/app/services/macro_data.py +50 -0
- package/fincli/app/services/market_data.py +203 -203
- package/fincli/app/services/news_aggregator.py +90 -0
- package/fincli/app/services/web_research.py +267 -0
- package/fincli/app/storage/cache.py +2 -2
- package/fincli/app/storage/config.py +122 -88
- package/fincli/app/storage/database.py +201 -85
- package/fincli/app/storage/secrets.py +12 -3
- package/fincli/app/tui/components.py +68 -50
- package/fincli/app/tui/layout.py +270 -258
- package/fincli/app/tui/market_provider_selector.py +6 -1
- package/fincli/app/tui/model_selector.py +11 -3
- package/fincli/app/tui/theme.py +134 -74
- package/fincli/app/utils/formatting.py +125 -12
- package/npm/bin/fincli.js +9 -2
- package/package.json +23 -23
- 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
|
|
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("/
|
|
27
|
-
CommandSpec("/
|
|
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("/
|
|
45
|
-
CommandSpec("/
|
|
46
|
-
CommandSpec("/
|
|
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("/
|
|
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
|
|
51
|
-
CommandSpec("/provider
|
|
52
|
-
CommandSpec("/provider
|
|
53
|
-
CommandSpec("/
|
|
54
|
-
CommandSpec("/
|
|
55
|
-
CommandSpec("/
|
|
56
|
-
CommandSpec("/
|
|
57
|
-
CommandSpec("/
|
|
58
|
-
CommandSpec("/
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if not normalized
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
for cmd in
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|