@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.
- package/LICENSE +21 -0
- package/README.md +124 -625
- 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 +26 -14
- package/fincli/app/analysis/analyzer.py +107 -96
- package/fincli/app/analysis/assistant_context.py +187 -186
- package/fincli/app/analysis/backtest.py +179 -0
- package/fincli/app/analysis/gameplay_plan.py +79 -0
- package/fincli/app/analysis/multi_timeframe.py +180 -0
- package/fincli/app/analysis/trading_methods.py +144 -0
- package/fincli/app/cli/commands.py +108 -81
- package/fincli/app/cli/router.py +2327 -1237
- 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/portfolio_risk.py +305 -0
- package/fincli/app/modules/reports.py +151 -0
- package/fincli/app/modules/scanner.py +111 -93
- 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/manager.py +60 -60
- 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 +84 -1
- package/fincli/app/providers/market/symbols.py +143 -0
- package/fincli/app/providers/market/twelvedata_provider.py +167 -167
- package/fincli/app/providers/reliability.py +86 -0
- package/fincli/app/research/__init__.py +8 -0
- package/fincli/app/research/engine.py +137 -0
- package/fincli/app/research/exporter.py +91 -0
- package/fincli/app/research/formatter.py +27 -0
- package/fincli/app/research/models.py +24 -0
- package/fincli/app/research/prompt_builder.py +54 -0
- package/fincli/app/services/macro_data.py +50 -0
- package/fincli/app/services/market_data.py +274 -169
- package/fincli/app/services/market_overview.py +42 -1
- package/fincli/app/services/news_aggregator.py +95 -0
- package/fincli/app/services/web_research.py +267 -267
- package/fincli/app/storage/config.py +122 -88
- package/fincli/app/storage/database.py +209 -99
- package/fincli/app/storage/provider_metrics.py +61 -0
- package/fincli/app/storage/secrets.py +26 -2
- package/fincli/app/tui/components.py +68 -50
- package/fincli/app/tui/layout.py +269 -258
- package/fincli/app/tui/market_provider_selector.py +3 -1
- package/fincli/app/tui/theme.py +134 -74
- package/fincli/app/utils/formatting.py +123 -60
- package/package.json +22 -20
- package/pyproject.toml +35 -35
|
@@ -12,9 +12,15 @@ from fincli.app.utils.errors import ConfigError
|
|
|
12
12
|
SECRETS_FILE = APP_DIR / "secrets.env"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def load_local_secrets(
|
|
15
|
+
def load_local_secrets(
|
|
16
|
+
path: Path | None = None,
|
|
17
|
+
*,
|
|
18
|
+
override: bool = False,
|
|
19
|
+
override_keys: set[str] | None = None,
|
|
20
|
+
) -> None:
|
|
16
21
|
"""Load persisted secrets into process environment."""
|
|
17
22
|
path = path or SECRETS_FILE
|
|
23
|
+
override_keys = override_keys or set()
|
|
18
24
|
if not path.exists():
|
|
19
25
|
return
|
|
20
26
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
@@ -24,7 +30,7 @@ def load_local_secrets(path: Path | None = None, *, override: bool = False) -> N
|
|
|
24
30
|
key, value = stripped.split("=", 1)
|
|
25
31
|
key = key.strip()
|
|
26
32
|
value = _unquote(value.strip())
|
|
27
|
-
if key and (override or key not in os.environ or os.environ.get(key, "") == ""):
|
|
33
|
+
if key and (override or key in override_keys or key not in os.environ or os.environ.get(key, "") == ""):
|
|
28
34
|
os.environ[key] = value
|
|
29
35
|
|
|
30
36
|
|
|
@@ -54,6 +60,24 @@ def save_secret(env_key: str, value: str, path: Path | None = None) -> None:
|
|
|
54
60
|
os.environ[key] = secret
|
|
55
61
|
|
|
56
62
|
|
|
63
|
+
def clear_secrets(path: Path | None = None) -> int:
|
|
64
|
+
"""Clear persisted local secrets and remove them from the current process."""
|
|
65
|
+
path = path or SECRETS_FILE
|
|
66
|
+
secrets = read_secrets(path)
|
|
67
|
+
for key in secrets:
|
|
68
|
+
os.environ.pop(key, None)
|
|
69
|
+
try:
|
|
70
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
path.write_text("# FinCLI local secrets. Do not commit or share this file.\n", encoding="utf-8")
|
|
72
|
+
try:
|
|
73
|
+
os.chmod(path, 0o600)
|
|
74
|
+
except OSError:
|
|
75
|
+
pass
|
|
76
|
+
except OSError as exc:
|
|
77
|
+
raise ConfigError("Secret lokal gagal dibersihkan.", f"Path: {path}") from exc
|
|
78
|
+
return len(secrets)
|
|
79
|
+
|
|
80
|
+
|
|
57
81
|
def read_secrets(path: Path | None = None) -> dict[str, str]:
|
|
58
82
|
"""Read local secrets without printing or masking them."""
|
|
59
83
|
path = path or SECRETS_FILE
|
|
@@ -1,55 +1,73 @@
|
|
|
1
|
-
"""Reusable TUI components."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from rich.markdown import Markdown
|
|
6
|
-
from rich.panel import Panel
|
|
7
|
-
from rich.table import Table
|
|
8
|
-
from rich.text import Text
|
|
1
|
+
"""Reusable TUI components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.markdown import Markdown
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.text import Text
|
|
9
9
|
from textual.widgets import Static
|
|
10
|
-
|
|
11
|
-
from fincli.app.cli.commands import CommandSpec
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class CommandPalette(Static):
|
|
15
|
-
"""Slash command palette shown near the command input."""
|
|
16
|
-
|
|
17
|
-
def render_commands(self, commands: list[CommandSpec], query: str = "") -> None:
|
|
18
|
-
table = Table.grid(expand=True)
|
|
19
|
-
table.add_column("Command", style="white", no_wrap=True, ratio=1)
|
|
20
|
-
table.add_column("Description", style="bright_black", justify="right", ratio=3)
|
|
21
|
-
|
|
22
|
-
for index, command in enumerate(commands):
|
|
23
|
-
command_text = command.name
|
|
24
|
-
description = command.description
|
|
25
|
-
if index == 0:
|
|
26
|
-
command_text = f"[black on cyan]> {command.name}[/]"
|
|
27
|
-
description = f"[black on cyan]{command.description}[/]"
|
|
28
|
-
table.add_row(command_text, description)
|
|
29
|
-
|
|
30
|
-
if len(commands) > 6:
|
|
31
|
-
table.add_row("[bright_black]v more[/]", "[bright_black]Ketik command lebih spesifik[/]")
|
|
32
|
-
|
|
33
|
-
title = f"[cyan]>[/] {query or '/'}"
|
|
34
|
-
self.update(Panel(table, title=title, border_style="bright_black", padding=(0, 1)))
|
|
35
|
-
|
|
36
|
-
def clear_palette(self) -> None:
|
|
37
|
-
self.update("")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def format_user_message(message: str) -> Panel:
|
|
41
|
-
text = Text()
|
|
42
|
-
text.append("> ", style="bold cyan")
|
|
43
|
-
text.append(message, style="bold white")
|
|
44
|
-
return Panel(text, border_style="#2f332f", style="on #2b2f2b", padding=(0, 1))
|
|
10
|
+
|
|
11
|
+
from fincli.app.cli.commands import CommandSpec
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommandPalette(Static):
|
|
15
|
+
"""Slash command palette shown near the command input."""
|
|
16
|
+
|
|
17
|
+
def render_commands(self, commands: list[CommandSpec], query: str = "") -> None:
|
|
18
|
+
table = Table.grid(expand=True)
|
|
19
|
+
table.add_column("Command", style="white", no_wrap=True, ratio=1)
|
|
20
|
+
table.add_column("Description", style="bright_black", justify="right", ratio=3)
|
|
21
|
+
|
|
22
|
+
for index, command in enumerate(commands):
|
|
23
|
+
command_text = command.name
|
|
24
|
+
description = command.description
|
|
25
|
+
if index == 0:
|
|
26
|
+
command_text = f"[black on cyan]> {command.name}[/]"
|
|
27
|
+
description = f"[black on cyan]{command.description}[/]"
|
|
28
|
+
table.add_row(command_text, description)
|
|
29
|
+
|
|
30
|
+
if len(commands) > 6:
|
|
31
|
+
table.add_row("[bright_black]v more[/]", "[bright_black]Ketik command lebih spesifik[/]")
|
|
32
|
+
|
|
33
|
+
title = f"[cyan]>[/] {query or '/'}"
|
|
34
|
+
self.update(Panel(table, title=title, border_style="bright_black", padding=(0, 1)))
|
|
35
|
+
|
|
36
|
+
def clear_palette(self) -> None:
|
|
37
|
+
self.update("")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_user_message(message: str) -> Panel:
|
|
41
|
+
text = Text()
|
|
42
|
+
text.append("> ", style="bold cyan")
|
|
43
|
+
text.append(message, style="bold white")
|
|
44
|
+
return Panel(text, border_style="#2f332f", style="on #2b2f2b", padding=(0, 1))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def format_thinking_message(message: str) -> Text:
|
|
48
|
+
text = Text()
|
|
49
|
+
text.append("> Thinking: ", style="dim")
|
|
50
|
+
text.append(message, style="italic dim")
|
|
51
|
+
return text
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def format_ai_message(message: str) -> Markdown:
|
|
55
|
+
return Markdown(message)
|
|
45
56
|
|
|
46
57
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
text.append("> Thinking: ", style="dim")
|
|
50
|
-
text.append(message, style="italic dim")
|
|
51
|
-
return text
|
|
58
|
+
def write_output_entry(log: object, renderable: object) -> None:
|
|
59
|
+
"""Write one output entry with a single blank line separator.
|
|
52
60
|
|
|
61
|
+
No visual barrier characters are emitted here; Rich/Textual renderables keep
|
|
62
|
+
their own borders if they need one.
|
|
63
|
+
"""
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
items = getattr(log, "items", None)
|
|
66
|
+
if isinstance(items, list) and items:
|
|
67
|
+
log.write("")
|
|
68
|
+
log.write(renderable)
|
|
69
|
+
return
|
|
70
|
+
line_count = getattr(log, "line_count", 0)
|
|
71
|
+
if isinstance(line_count, int) and line_count > 0:
|
|
72
|
+
log.write("")
|
|
73
|
+
log.write(renderable)
|