@kuandotdev/indicator 0.1.1 → 0.1.3
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/README.md +2 -7
- package/package.json +4 -1
- package/project/.cursor/rules/agent-docs.mdc +16 -0
- package/project/.cursor/rules/api-credential-storage.mdc +16 -0
- package/project/.cursor/rules/pinescript-v6.mdc +16 -0
- package/project/.cursor/rules/stock-model-forecast.mdc +16 -0
- package/project/.cursor/rules/system-prompt-injection.mdc +16 -0
- package/project/.cursor/rules/system-prompt-updater.mdc +16 -0
- package/project/.cursor/rules/tradingview-stock-data.mdc +16 -0
- package/project/.env.example +44 -0
- package/project/.npm-packaged-project +1 -0
- package/project/.pi/APPEND_SYSTEM.md +338 -0
- package/project/.pi/settings.json +8 -0
- package/project/AGENTS.md +538 -0
- package/project/CLAUDE.md +538 -0
- package/project/GEMINI.md +538 -0
- package/project/Makefile +488 -0
- package/project/README.md +419 -0
- package/project/conda-env-active.sh +98 -0
- package/project/conda-env-deactive.sh +42 -0
- package/project/docs/agent-install.md +446 -0
- package/project/docs/agent-skill-directory.md +222 -0
- package/project/docs/integration.html +271 -0
- package/project/packages/indicator/README.md +39 -0
- package/project/packages/indicator/package.json +40 -0
- package/project/packages/indicator/scripts/build-project-snapshot.js +57 -0
- package/project/packages/indicator/src/cli.js +368 -0
- package/project/packages/tradingview-stock-data-skill/README.md +112 -0
- package/project/packages/tradingview-stock-data-skill/extensions/stock-prompt-injector.ts +121 -0
- package/project/packages/tradingview-stock-data-skill/package.json +35 -0
- package/project/packages/tradingview-stock-data-skill/scripts/postinstall.sh +73 -0
- package/project/packages/tradingview-stock-data-skill/skills/tradingview-stock-data/SKILL.md +241 -0
- package/project/pyproject.toml +68 -0
- package/project/screenshots/.gitkeep +0 -0
- package/project/scripts/indicators/example_rsi_bands.pine +27 -0
- package/project/scripts/indicators/tsla_levels.pine +57 -0
- package/project/skills/agent-docs/SKILL.md +56 -0
- package/project/skills/api-credential-storage/SKILL.md +83 -0
- package/project/skills/api-credential-storage/scripts/upsert_env.py +151 -0
- package/project/skills/pinescript-v6/SKILL.md +129 -0
- package/project/skills/pinescript-v6/reference/built-ins.md +219 -0
- package/project/skills/pinescript-v6/reference/templates/alert-webhook.pine +76 -0
- package/project/skills/pinescript-v6/reference/templates/indicator.pine +48 -0
- package/project/skills/pinescript-v6/reference/templates/strategy.pine +50 -0
- package/project/skills/pinescript-v6/reference/v5-to-v6-migration.md +102 -0
- package/project/skills/pinescript-v6/reference/v6-language.md +202 -0
- package/project/skills/stock-model-forecast/SKILL.md +192 -0
- package/project/skills/system-prompt-injection/CUSTOM_SYSTEM_PROMPT.md +333 -0
- package/project/skills/system-prompt-injection/DEFAULT_SYSTEM_PROMPT.md +327 -0
- package/project/skills/system-prompt-injection/SKILL.md +90 -0
- package/project/skills/system-prompt-injection/SYSTEM_PROMPT.md +23 -0
- package/project/skills/system-prompt-updater/SKILL.md +82 -0
- package/project/skills/system-prompt-updater/scripts/system_prompt_update.sh +106 -0
- package/project/skills/tradingview-stock-data/SKILL.md +272 -0
- package/project/src/tv_indicator/__init__.py +0 -0
- package/project/src/tv_indicator/browser/__init__.py +0 -0
- package/project/src/tv_indicator/browser/automation.py +541 -0
- package/project/src/tv_indicator/browser/selectors.py +70 -0
- package/project/src/tv_indicator/cli/__init__.py +0 -0
- package/project/src/tv_indicator/cli/browser_cmds.py +92 -0
- package/project/src/tv_indicator/cli/data_cmds.py +178 -0
- package/project/src/tv_indicator/cli/main.py +56 -0
- package/project/src/tv_indicator/cli/model_cmds.py +255 -0
- package/project/src/tv_indicator/cli/pine_cmds.py +140 -0
- package/project/src/tv_indicator/config.py +98 -0
- package/project/src/tv_indicator/data/__init__.py +0 -0
- package/project/src/tv_indicator/data/client.py +187 -0
- package/project/src/tv_indicator/data/screener.py +268 -0
- package/project/src/tv_indicator/mcp/__init__.py +0 -0
- package/project/src/tv_indicator/mcp/agent_server.py +398 -0
- package/project/src/tv_indicator/mcp/browser_server.py +133 -0
- package/project/src/tv_indicator/mcp/data_server.py +239 -0
- package/project/src/tv_indicator/model/__init__.py +19 -0
- package/project/src/tv_indicator/model/forecast.py +693 -0
- package/project/tools/import_agent_tools.sh +503 -0
- package/project/tools/install_skills.sh +673 -0
- package/project/tools/interactive_install.sh +917 -0
- package/project/tools/progress.sh +114 -0
- package/project/tools/uninstall_agent_tools.sh +373 -0
- package/src/cli.js +22 -25
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""`training-view browser ...` subcommands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(no_args_is_help=True, help="Browser automation (Playwright) for TradingView.")
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("session-check")
|
|
16
|
+
def session_check() -> None:
|
|
17
|
+
"""Verify the saved browser profile / session cookie is still valid."""
|
|
18
|
+
from ..browser.automation import session_check as do_check
|
|
19
|
+
|
|
20
|
+
r = do_check()
|
|
21
|
+
ok = "✅" if r["signed_in"] else "❌"
|
|
22
|
+
|
|
23
|
+
method_label = {
|
|
24
|
+
"env": "env var (TV_SESSIONID in .env)",
|
|
25
|
+
"browser_profile": "browser profile (saved by `make login`)",
|
|
26
|
+
"both": "env var + browser profile (both)",
|
|
27
|
+
"none": "none",
|
|
28
|
+
}[r["auth_method"]]
|
|
29
|
+
|
|
30
|
+
console.print(f" {ok} signed_in: [bold]{r['signed_in']}[/bold]")
|
|
31
|
+
console.print(f" auth method: {method_label}")
|
|
32
|
+
console.print(
|
|
33
|
+
f" via .env cookie: {'✓' if r['via_env_cookie'] else '✗'}"
|
|
34
|
+
f" (TV_SESSIONID set in .env)"
|
|
35
|
+
)
|
|
36
|
+
console.print(
|
|
37
|
+
f" via browser profile: {'✓' if r['via_browser_profile'] else '✗'}"
|
|
38
|
+
f" (sessionid cookie persisted in Playwright profile)"
|
|
39
|
+
)
|
|
40
|
+
console.print(f" profile_dir: {r['profile_dir']}")
|
|
41
|
+
|
|
42
|
+
if not r["signed_in"]:
|
|
43
|
+
console.print(
|
|
44
|
+
"\n [yellow]Hint:[/yellow] run [bold]make login[/bold] for an interactive sign-in,"
|
|
45
|
+
" or paste a fresh sessionid into .env."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command()
|
|
50
|
+
def login(
|
|
51
|
+
timeout: int = typer.Option(300, "--timeout", help="Seconds to wait for manual login")
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Open a visible browser so you can log in manually. Profile is saved."""
|
|
54
|
+
from ..browser.automation import interactive_login
|
|
55
|
+
|
|
56
|
+
r = interactive_login(timeout_seconds=timeout)
|
|
57
|
+
if r["signed_in"]:
|
|
58
|
+
console.print(f"[green]✅ {r['message']}[/green]")
|
|
59
|
+
else:
|
|
60
|
+
console.print(f"[red]❌ {r['message']}[/red]")
|
|
61
|
+
raise typer.Exit(1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
def push(
|
|
66
|
+
script: Path = typer.Argument(..., exists=True, help="Path to .pine file"),
|
|
67
|
+
apply: Optional[str] = typer.Option(
|
|
68
|
+
None, "--apply", help="Symbol to apply chart to, e.g. NASDAQ:AAPL"
|
|
69
|
+
),
|
|
70
|
+
screenshot: bool = typer.Option(False, "--screenshot", "-s", help="Save a chart screenshot"),
|
|
71
|
+
name: Optional[str] = typer.Option(None, "--name", help="Script name in TradingView"),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Paste a .pine file into TV Pine Editor, save, optionally apply + screenshot."""
|
|
74
|
+
from ..browser.automation import push_script
|
|
75
|
+
|
|
76
|
+
r = push_script(script, apply_to=apply, screenshot=screenshot, script_name=name)
|
|
77
|
+
console.print_json(json.dumps(r, default=str))
|
|
78
|
+
if r["errors"]:
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command()
|
|
83
|
+
def screenshot(
|
|
84
|
+
symbol: Optional[str] = typer.Option(None, "--symbol", "-s"),
|
|
85
|
+
layout: Optional[str] = typer.Option(None, "--layout", help="Layout URL"),
|
|
86
|
+
out: Optional[Path] = typer.Option(None, "--out", "-o"),
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Open a chart and save a screenshot."""
|
|
89
|
+
from ..browser.automation import screenshot_chart
|
|
90
|
+
|
|
91
|
+
path = screenshot_chart(symbol=symbol, layout_url=layout, out_path=out)
|
|
92
|
+
console.print(f"[green]Saved:[/green] {path}")
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""`training-view data ...` subcommands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(no_args_is_help=True, help="Pull OHLCV, search symbols, run screener.")
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def ohlcv(
|
|
17
|
+
symbol: str = typer.Argument(..., help="Symbol, e.g. NASDAQ:AAPL or AAPL"),
|
|
18
|
+
interval: str = typer.Option("1D", "--interval", "-i", help="1, 5, 15, 60, 240, 1D, 1W, 1M"),
|
|
19
|
+
bars: int = typer.Option(500, "--bars", "-n", help="Number of bars to fetch"),
|
|
20
|
+
exchange: Optional[str] = typer.Option(None, "--exchange", "-e"),
|
|
21
|
+
csv: Optional[Path] = typer.Option(None, "--csv", help="Write to CSV instead of printing"),
|
|
22
|
+
tail: int = typer.Option(10, "--tail", help="Rows to print when not writing CSV"),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Fetch OHLCV candles for SYMBOL."""
|
|
25
|
+
from ..data.client import get_ohlcv
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
df = get_ohlcv(symbol, interval=interval, n_bars=bars, exchange=exchange)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
|
|
33
|
+
if df.empty:
|
|
34
|
+
console.print(f"[yellow]No data returned for {symbol}[/yellow]")
|
|
35
|
+
raise typer.Exit(2)
|
|
36
|
+
|
|
37
|
+
if csv:
|
|
38
|
+
csv.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
df.to_csv(csv)
|
|
40
|
+
console.print(f"[green]Wrote {len(df)} rows → {csv}[/green]")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
table = Table(title=f"{symbol} · {interval} · last {min(tail, len(df))} of {len(df)} bars")
|
|
44
|
+
table.add_column("time")
|
|
45
|
+
for c in df.columns:
|
|
46
|
+
table.add_column(c, justify="right")
|
|
47
|
+
for ts, row in df.tail(tail).iterrows():
|
|
48
|
+
table.add_row(str(ts), *[f"{v:.4f}" if isinstance(v, float) else str(v) for v in row])
|
|
49
|
+
console.print(table)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
def search(
|
|
54
|
+
query: str = typer.Argument(..., help="Ticker or name fragment"),
|
|
55
|
+
limit: int = typer.Option(20, "--limit", "-n"),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Search symbols by name/ticker."""
|
|
58
|
+
from ..data.screener import search_symbol
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
results = search_symbol(query, limit=limit)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
64
|
+
raise typer.Exit(1)
|
|
65
|
+
|
|
66
|
+
if not results:
|
|
67
|
+
console.print(f"[yellow]No matches for '{query}'[/yellow]")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
table = Table(title=f"Search: {query}")
|
|
71
|
+
for k in results[0].keys():
|
|
72
|
+
table.add_column(str(k))
|
|
73
|
+
for row in results:
|
|
74
|
+
table.add_row(*[str(row.get(k, "")) for k in results[0].keys()])
|
|
75
|
+
console.print(table)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command()
|
|
79
|
+
def screener(
|
|
80
|
+
market: str = typer.Option("america", "--market", "-m"),
|
|
81
|
+
filter_: list[str] = typer.Option(
|
|
82
|
+
[], "--filter", "-f",
|
|
83
|
+
help="Filter expression COL OP VAL (e.g. 'RSI < 30'). Repeatable.",
|
|
84
|
+
),
|
|
85
|
+
columns: list[str] = typer.Option(
|
|
86
|
+
[], "--col", "-c",
|
|
87
|
+
help="Columns to include. Repeatable. Default: name, close, RSI, volume, change",
|
|
88
|
+
),
|
|
89
|
+
order_by: Optional[str] = typer.Option(
|
|
90
|
+
None, "--order-by", help="Column to sort by (prefix '-' for descending)"
|
|
91
|
+
),
|
|
92
|
+
limit: int = typer.Option(50, "--limit", "-n"),
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Run a TradingView screener.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
|
|
98
|
+
training-view data screener -m america -f "RSI < 30" -f "volume > 1000000" -c name -c close -c RSI
|
|
99
|
+
"""
|
|
100
|
+
from ..data.screener import screener as run_screener
|
|
101
|
+
|
|
102
|
+
# Parse filters
|
|
103
|
+
parsed: list[tuple[str, str, object]] = []
|
|
104
|
+
for f in filter_:
|
|
105
|
+
parts = f.split(maxsplit=2)
|
|
106
|
+
if len(parts) != 3:
|
|
107
|
+
console.print(f"[red]Bad filter:[/red] '{f}' — expected 'COL OP VAL'")
|
|
108
|
+
raise typer.Exit(1)
|
|
109
|
+
col, op, val = parts
|
|
110
|
+
try:
|
|
111
|
+
v: object = float(val)
|
|
112
|
+
except ValueError:
|
|
113
|
+
v = val
|
|
114
|
+
parsed.append((col, op, v))
|
|
115
|
+
|
|
116
|
+
sort = None
|
|
117
|
+
if order_by:
|
|
118
|
+
ascending = not order_by.startswith("-")
|
|
119
|
+
sort = (order_by.lstrip("-"), ascending)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
df = run_screener(
|
|
123
|
+
market=market,
|
|
124
|
+
columns=columns or None,
|
|
125
|
+
filters=parsed or None,
|
|
126
|
+
order_by=sort,
|
|
127
|
+
limit=limit,
|
|
128
|
+
)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
131
|
+
raise typer.Exit(1)
|
|
132
|
+
|
|
133
|
+
if df.empty:
|
|
134
|
+
console.print("[yellow]No rows.[/yellow]")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
table = Table(title=f"Screener · {market} · {len(df)} rows")
|
|
138
|
+
for c in df.columns:
|
|
139
|
+
table.add_column(str(c))
|
|
140
|
+
for _, row in df.iterrows():
|
|
141
|
+
table.add_row(*[f"{v:.4f}" if isinstance(v, float) else str(v) for v in row])
|
|
142
|
+
console.print(table)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command()
|
|
146
|
+
def rating(
|
|
147
|
+
symbol: str = typer.Argument(..., help="Symbol, e.g. AAPL or NASDAQ:AAPL"),
|
|
148
|
+
interval: str = typer.Option("1D", "--interval", "-i"),
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Get TradingView's Strong Buy/Buy/Neutral/Sell/Strong Sell rating."""
|
|
151
|
+
from ..data.screener import technical_rating
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
r = technical_rating(symbol, interval=interval)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
157
|
+
raise typer.Exit(1)
|
|
158
|
+
|
|
159
|
+
if r.get("rating") is None:
|
|
160
|
+
console.print(f"[yellow]⚠ {r.get('error', 'no data')}[/yellow]")
|
|
161
|
+
if r.get("market_tried"):
|
|
162
|
+
console.print(f"[dim] market tried: {r['market_tried']}[/dim]")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
colors = {
|
|
166
|
+
"Strong Buy": "bold green",
|
|
167
|
+
"Buy": "green",
|
|
168
|
+
"Neutral": "yellow",
|
|
169
|
+
"Sell": "red",
|
|
170
|
+
"Strong Sell": "bold red",
|
|
171
|
+
}
|
|
172
|
+
c = colors.get(r["rating"], "white")
|
|
173
|
+
console.print(f"\n [bold]{r['symbol']}[/bold] → [{c}]{r['rating']}[/{c}]")
|
|
174
|
+
console.print(f" score: {r.get('score')}")
|
|
175
|
+
console.print(f" rsi: {r.get('rsi')}")
|
|
176
|
+
console.print(f" close: {r.get('close')}")
|
|
177
|
+
console.print(f" MA score: {r.get('ma_score')}")
|
|
178
|
+
console.print(f" Osc score: {r.get('oscillator_score')}\n")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""`training-view` — TradingView integration toolkit CLI.
|
|
2
|
+
|
|
3
|
+
Run `training-view --help` for all commands.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .data_cmds import app as data_app
|
|
11
|
+
from .browser_cmds import app as browser_app
|
|
12
|
+
from .pine_cmds import app as pine_app
|
|
13
|
+
from .model_cmds import app as model_app
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="training-view",
|
|
17
|
+
help="TradingView toolkit: data, browser automation, and PineScript scaffolding.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
add_completion=False,
|
|
20
|
+
)
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
app.add_typer(data_app, name="data", help="Pull OHLCV, search symbols, run screener.")
|
|
24
|
+
app.add_typer(browser_app, name="browser", help="Push Pine scripts, screenshot charts.")
|
|
25
|
+
app.add_typer(pine_app, name="pine", help="Scaffold Pine indicators / strategies.")
|
|
26
|
+
app.add_typer(model_app, name="model", help="Fetch server-side stock/ETF/index model forecasts.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def version() -> None:
|
|
31
|
+
"""Print version."""
|
|
32
|
+
from importlib.metadata import version as v
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
console.print(f"TradingView toolkit [bold]{v('tv-indicator')}[/bold]")
|
|
36
|
+
except Exception:
|
|
37
|
+
console.print("TradingView toolkit (dev)")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command()
|
|
41
|
+
def status() -> None:
|
|
42
|
+
"""Show config + auth status."""
|
|
43
|
+
from ..config import settings
|
|
44
|
+
|
|
45
|
+
console.print("[bold]TradingView integration toolkit[/bold]")
|
|
46
|
+
console.print(f" Session cookie set: {'✅' if settings.has_session else '❌'}")
|
|
47
|
+
console.print(f" Username: {settings.tv_username or '—'}")
|
|
48
|
+
console.print(f" Browser profile: {settings.browser_profile}")
|
|
49
|
+
console.print(f" Headless: {settings.browser_headless}")
|
|
50
|
+
console.print(f" Default chart URL: {settings.default_chart_url or '—'}")
|
|
51
|
+
console.print(f" Screenshot dir: {settings.screenshot_dir}")
|
|
52
|
+
console.print(f" Scripts dir: {settings.scripts_dir}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
app()
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""`training-view model ...` subcommands for server-side stock forecast integration."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ..model.forecast import dumps_json, forecast_for_symbol, model_status, setup_model_env
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(no_args_is_help=True, help="Server-side stock/ETF/index forecast API.")
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("status")
|
|
18
|
+
def status(
|
|
19
|
+
repo: Optional[Path] = typer.Option(
|
|
20
|
+
None,
|
|
21
|
+
"--repo",
|
|
22
|
+
help="Deprecated; ignored because the model now runs server-side.",
|
|
23
|
+
),
|
|
24
|
+
csv_path: Optional[Path] = typer.Option(
|
|
25
|
+
None,
|
|
26
|
+
"--csv",
|
|
27
|
+
help="Deprecated; ignored because forecasts now come from the API.",
|
|
28
|
+
),
|
|
29
|
+
conda_env: Optional[str] = typer.Option(
|
|
30
|
+
None,
|
|
31
|
+
"--conda-env",
|
|
32
|
+
help="Deprecated; ignored because no local ML runtime is used.",
|
|
33
|
+
),
|
|
34
|
+
api_url: Optional[str] = typer.Option(
|
|
35
|
+
None,
|
|
36
|
+
"--api-url",
|
|
37
|
+
help="Forecast endpoint. Defaults to TV_MODEL_API_URL or TV_MODEL_API_BASE_URL + /forecast.",
|
|
38
|
+
),
|
|
39
|
+
status_url: Optional[str] = typer.Option(
|
|
40
|
+
None,
|
|
41
|
+
"--status-url",
|
|
42
|
+
help="Optional API health endpoint. Defaults to TV_MODEL_API_STATUS_URL.",
|
|
43
|
+
),
|
|
44
|
+
json_output: bool = typer.Option(False, "--json", help="Print raw JSON payload."),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Show server-side model API configuration/status."""
|
|
47
|
+
payload = model_status(
|
|
48
|
+
repo=repo,
|
|
49
|
+
csv_path=csv_path,
|
|
50
|
+
conda_env=conda_env or None,
|
|
51
|
+
api_url=api_url,
|
|
52
|
+
status_url=status_url,
|
|
53
|
+
)
|
|
54
|
+
if json_output:
|
|
55
|
+
print(dumps_json(payload))
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
console.print("[bold]Server-side stock/ETF/index model forecast API[/bold]")
|
|
59
|
+
console.print(f" forecast API: {_mark(payload['model_api_configured'])} {payload.get('model_api_url') or 'not configured'}")
|
|
60
|
+
console.print(f" status API: {_status_mark(payload.get('model_api_reachable'))} {payload.get('model_api_status_url') or 'not configured'}")
|
|
61
|
+
if payload.get("model_api_error"):
|
|
62
|
+
console.print(f" API note: [yellow]{payload['model_api_error']}[/yellow]")
|
|
63
|
+
console.print(f" temp dir: {_mark(payload['temp_dir_exists'])} {payload['temp_dir']}")
|
|
64
|
+
console.print(" auth: " + ("Bearer token set" if _api_token_set() else "no token"))
|
|
65
|
+
console.print(" commands:")
|
|
66
|
+
for value in payload["commands"].values():
|
|
67
|
+
console.print(f" {value}")
|
|
68
|
+
if payload.get("next_actions"):
|
|
69
|
+
console.print(" next actions:")
|
|
70
|
+
for action in payload["next_actions"]:
|
|
71
|
+
console.print(f" - {action}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command("setup")
|
|
75
|
+
def setup(
|
|
76
|
+
repo: Optional[Path] = typer.Option(
|
|
77
|
+
None,
|
|
78
|
+
"--repo",
|
|
79
|
+
help="Deprecated; ignored because the model now runs server-side.",
|
|
80
|
+
),
|
|
81
|
+
conda_env: Optional[str] = typer.Option(
|
|
82
|
+
None,
|
|
83
|
+
"--conda-env",
|
|
84
|
+
help="Deprecated; ignored because no local ML runtime is used.",
|
|
85
|
+
),
|
|
86
|
+
no_extra_deps: bool = typer.Option(
|
|
87
|
+
False,
|
|
88
|
+
"--no-extra-deps",
|
|
89
|
+
help="Deprecated; no local model dependencies are installed.",
|
|
90
|
+
),
|
|
91
|
+
api_url: Optional[str] = typer.Option(
|
|
92
|
+
None,
|
|
93
|
+
"--api-url",
|
|
94
|
+
help="Forecast endpoint. Defaults to TV_MODEL_API_URL or TV_MODEL_API_BASE_URL + /forecast.",
|
|
95
|
+
),
|
|
96
|
+
status_url: Optional[str] = typer.Option(
|
|
97
|
+
None,
|
|
98
|
+
"--status-url",
|
|
99
|
+
help="Optional API health endpoint. Defaults to TV_MODEL_API_STATUS_URL.",
|
|
100
|
+
),
|
|
101
|
+
json_output: bool = typer.Option(False, "--json", help="Print raw JSON payload."),
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Validate server-side model API configuration."""
|
|
104
|
+
payload = setup_model_env(
|
|
105
|
+
repo=repo,
|
|
106
|
+
conda_env=conda_env,
|
|
107
|
+
install_extra_deps=not no_extra_deps,
|
|
108
|
+
api_url=api_url,
|
|
109
|
+
status_url=status_url,
|
|
110
|
+
)
|
|
111
|
+
if json_output:
|
|
112
|
+
print(dumps_json(payload))
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
ok = "✓" if payload["ok"] else "✗"
|
|
116
|
+
console.print(f"[bold]Model API setup[/bold] {ok}")
|
|
117
|
+
if payload["stdout"]:
|
|
118
|
+
console.print(payload["stdout"])
|
|
119
|
+
if payload["stderr"]:
|
|
120
|
+
console.print(f"[yellow]{payload['stderr']}[/yellow]")
|
|
121
|
+
if not payload["ok"]:
|
|
122
|
+
raise typer.Exit(1)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("forecast")
|
|
126
|
+
def forecast(
|
|
127
|
+
symbol: str = typer.Argument(..., help="Ticker or TradingView symbol, e.g. TSLA or NASDAQ:TSLA"),
|
|
128
|
+
repo: Optional[Path] = typer.Option(
|
|
129
|
+
None,
|
|
130
|
+
"--repo",
|
|
131
|
+
help="Deprecated; ignored because forecasts now come from the API.",
|
|
132
|
+
),
|
|
133
|
+
csv_path: Optional[Path] = typer.Option(
|
|
134
|
+
None,
|
|
135
|
+
"--csv",
|
|
136
|
+
help="Deprecated; ignored because forecasts now come from the API.",
|
|
137
|
+
),
|
|
138
|
+
refresh: bool = typer.Option(
|
|
139
|
+
False,
|
|
140
|
+
"--refresh",
|
|
141
|
+
help="Ask the server-side model API to refresh/recompute before returning a forecast.",
|
|
142
|
+
),
|
|
143
|
+
conda_env: Optional[str] = typer.Option(
|
|
144
|
+
None,
|
|
145
|
+
"--conda-env",
|
|
146
|
+
help="Deprecated; ignored because no local ML runtime is used.",
|
|
147
|
+
),
|
|
148
|
+
period: str = typer.Option("10y", "--period", help="Historical period hint sent to the API."),
|
|
149
|
+
api_url: Optional[str] = typer.Option(
|
|
150
|
+
None,
|
|
151
|
+
"--api-url",
|
|
152
|
+
help="Forecast endpoint. Defaults to TV_MODEL_API_URL or TV_MODEL_API_BASE_URL + /forecast.",
|
|
153
|
+
),
|
|
154
|
+
json_output: bool = typer.Option(False, "--json", help="Print raw JSON payload."),
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Fetch the xs_range 20-day model forecast for a ticker from the API.
|
|
157
|
+
|
|
158
|
+
Crypto, forex, and futures symbols are reported as not applicable without an
|
|
159
|
+
API request.
|
|
160
|
+
"""
|
|
161
|
+
payload = forecast_for_symbol(
|
|
162
|
+
symbol,
|
|
163
|
+
repo=repo,
|
|
164
|
+
csv_path=csv_path,
|
|
165
|
+
refresh=refresh,
|
|
166
|
+
conda_env=conda_env or None,
|
|
167
|
+
period=period,
|
|
168
|
+
api_url=api_url,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if json_output:
|
|
172
|
+
# Plain stdout so agents can parse JSON without Rich line wrapping.
|
|
173
|
+
print(dumps_json(payload))
|
|
174
|
+
else:
|
|
175
|
+
_print_human(payload)
|
|
176
|
+
|
|
177
|
+
# Forecast unavailability is a handled analysis state. Keep exit code 0 so
|
|
178
|
+
# agents can continue with TradingView/news/fundamentals and inspect
|
|
179
|
+
# payload["ok"] / payload["error"] instead of aborting the full report.
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _mark(ok: bool) -> str:
|
|
183
|
+
return "✓" if ok else "✗"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _status_mark(value: object) -> str:
|
|
187
|
+
if value is True:
|
|
188
|
+
return "✓"
|
|
189
|
+
if value is False:
|
|
190
|
+
return "✗"
|
|
191
|
+
return "-"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _api_token_set() -> bool:
|
|
195
|
+
import os
|
|
196
|
+
|
|
197
|
+
return bool(os.getenv("TV_MODEL_API_KEY") or os.getenv("TV_MODEL_API_TOKEN"))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _print_human(payload: dict) -> None:
|
|
201
|
+
row = payload.get("row")
|
|
202
|
+
console.print(f"[bold]Model ticker:[/bold] {payload['model_ticker']} covered={payload['covered']}")
|
|
203
|
+
if not payload.get("model_applicable", True):
|
|
204
|
+
console.print(f"[bold]Asset class:[/bold] {payload.get('asset_class')}")
|
|
205
|
+
console.print(f"[yellow]Model forecast not applicable:[/yellow] {payload.get('unsupported_reason')}")
|
|
206
|
+
console.print("[yellow]No API request was made.[/yellow]")
|
|
207
|
+
return
|
|
208
|
+
console.print(f"[bold]Forecast API:[/bold] {payload.get('model_api_url') or 'not configured'}")
|
|
209
|
+
if payload.get("refresh_requested"):
|
|
210
|
+
console.print("[bold]Refresh requested:[/bold] yes")
|
|
211
|
+
|
|
212
|
+
if not row:
|
|
213
|
+
console.print(f"[yellow]Model forecast unavailable:[/yellow] {payload.get('error')}")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
table = Table(title="20-day model forecast")
|
|
217
|
+
table.add_column("Field")
|
|
218
|
+
table.add_column("Value", justify="right")
|
|
219
|
+
fields = [
|
|
220
|
+
"ticker",
|
|
221
|
+
"class",
|
|
222
|
+
"position",
|
|
223
|
+
"rank",
|
|
224
|
+
"close",
|
|
225
|
+
"low_20d",
|
|
226
|
+
"high_20d",
|
|
227
|
+
"fair_value",
|
|
228
|
+
"value_gap",
|
|
229
|
+
"signal",
|
|
230
|
+
"mean_iou",
|
|
231
|
+
"iou60",
|
|
232
|
+
"dir_win",
|
|
233
|
+
"pred_low_pct",
|
|
234
|
+
"pred_high_pct",
|
|
235
|
+
"pred_mid_pct",
|
|
236
|
+
"range_width_pct",
|
|
237
|
+
"direction",
|
|
238
|
+
"directional_win_rate",
|
|
239
|
+
"range_overlap_rate",
|
|
240
|
+
"close_in_range_rate",
|
|
241
|
+
"range_coverage_rate",
|
|
242
|
+
]
|
|
243
|
+
for field in fields:
|
|
244
|
+
if field in row and row[field] is not None:
|
|
245
|
+
table.add_row(field, _fmt(row[field]))
|
|
246
|
+
console.print(table)
|
|
247
|
+
if payload.get("model_output"):
|
|
248
|
+
console.print("[bold]model_output:[/bold]")
|
|
249
|
+
console.print(dumps_json(payload["model_output"]))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _fmt(value: object) -> str:
|
|
253
|
+
if isinstance(value, float):
|
|
254
|
+
return f"{value:.4f}"
|
|
255
|
+
return str(value)
|