nexus-agent-cli 0.2.0__tar.gz → 0.6.0__tar.gz
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.
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/PKG-INFO +22 -2
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/README.md +21 -1
- nexus_agent_cli-0.6.0/nexus_agent_cli/__init__.py +1 -0
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/nexus_agent_cli/api_client.py +72 -0
- nexus_agent_cli-0.6.0/nexus_agent_cli/commands/run/cmd.py +300 -0
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/nexus_agent_cli/config.py +5 -2
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/pyproject.toml +1 -1
- nexus_agent_cli-0.2.0/nexus_agent_cli/__init__.py +0 -1
- nexus_agent_cli-0.2.0/nexus_agent_cli/commands/run/cmd.py +0 -148
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/.gitignore +0 -0
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/nexus_agent_cli/__main__.py +0 -0
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/nexus_agent_cli/auth_pkce.py +0 -0
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/nexus_agent_cli/commands/__init__.py +0 -0
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/nexus_agent_cli/commands/auth/__init__.py +0 -0
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/nexus_agent_cli/commands/auth/cmd.py +0 -0
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/nexus_agent_cli/commands/run/__init__.py +0 -0
- {nexus_agent_cli-0.2.0 → nexus_agent_cli-0.6.0}/nexus_agent_cli/main.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nexus-agent-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Nexus internal agent ops CLI — pull run params, push analysis results, send notifications. Admin only.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: httpx>=0.27
|
|
@@ -61,6 +61,26 @@ nexus-agent push-backtest --ticker tqqq \
|
|
|
61
61
|
--conservative conservative_aggregated.json \
|
|
62
62
|
--normal normal_aggregated.json
|
|
63
63
|
|
|
64
|
+
# 回读 S3(均为只读,登录即可):最新回测 / 市场分析报告 / 组合推荐
|
|
65
|
+
nexus-agent get-backtest --ticker tqqq --out tqqq_backtest.json
|
|
66
|
+
nexus-agent get-market-analysis --ticker tqqq --out tqqq_market.json
|
|
67
|
+
nexus-agent get-portfolio --out portfolio.json
|
|
68
|
+
|
|
69
|
+
# 拉取 S3 上最新的 signal_position 基线 config(Position-Research 阶段用)
|
|
70
|
+
nexus-agent pull-config --ticker tqqq --kind normal --out tqqq-normal.json
|
|
71
|
+
|
|
72
|
+
# 历史版本(by-date):列出 → 取指定版本
|
|
73
|
+
nexus-agent pull-config --ticker tqqq --kind normal --list --since 2026-06-01
|
|
74
|
+
nexus-agent pull-config --ticker tqqq --kind normal --date 2026-06-01_to_2026-06-14 --out old.json
|
|
75
|
+
nexus-agent get-market-analysis --ticker tqqq --list
|
|
76
|
+
nexus-agent get-portfolio --list --since 2026-06-01
|
|
77
|
+
|
|
78
|
+
# 回传 Position-Research 产出的 3 套仓位 config(≠ push-backtest 的回测聚合)
|
|
79
|
+
nexus-agent push-config --ticker tqqq --time-range 2026-06-01_2026-06-14 \
|
|
80
|
+
--aggressive tqqq-aggressive.json \
|
|
81
|
+
--conservative tqqq-conservative.json \
|
|
82
|
+
--normal tqqq-normal.json
|
|
83
|
+
|
|
64
84
|
# 服务端 SES 发通知邮件
|
|
65
85
|
nexus-agent notify --to a@x.com --to b@y.com --subject "Weekly report" --body-file summary.txt
|
|
66
86
|
```
|
|
@@ -73,7 +93,7 @@ nexus-agent notify --to a@x.com --to b@y.com --subject "Weekly report" --body-fi
|
|
|
73
93
|
| `NEXUS_AGENT_API_BASE_URL` | — | 直接覆盖完整 API base(含 `/api`) |
|
|
74
94
|
| `NEXUS_AGENT_COGNITO_DOMAIN` | `auth.lookatwallstreet.com` | Cognito Hosted UI 域名 |
|
|
75
95
|
| `NEXUS_AGENT_COGNITO_CLIENT_ID` | (内置) | Cognito app client id |
|
|
76
|
-
| `NEXUS_AGENT_REDIRECT_URI` | `http://127.0.0.1:
|
|
96
|
+
| `NEXUS_AGENT_REDIRECT_URI` | `http://127.0.0.1:8251/callback` | 本地回调(须 localhost+端口;端口必须已加入 Cognito app client 的 Allowed callback URLs) |
|
|
77
97
|
|
|
78
98
|
例如指向本地后端调试:
|
|
79
99
|
|
|
@@ -50,6 +50,26 @@ nexus-agent push-backtest --ticker tqqq \
|
|
|
50
50
|
--conservative conservative_aggregated.json \
|
|
51
51
|
--normal normal_aggregated.json
|
|
52
52
|
|
|
53
|
+
# 回读 S3(均为只读,登录即可):最新回测 / 市场分析报告 / 组合推荐
|
|
54
|
+
nexus-agent get-backtest --ticker tqqq --out tqqq_backtest.json
|
|
55
|
+
nexus-agent get-market-analysis --ticker tqqq --out tqqq_market.json
|
|
56
|
+
nexus-agent get-portfolio --out portfolio.json
|
|
57
|
+
|
|
58
|
+
# 拉取 S3 上最新的 signal_position 基线 config(Position-Research 阶段用)
|
|
59
|
+
nexus-agent pull-config --ticker tqqq --kind normal --out tqqq-normal.json
|
|
60
|
+
|
|
61
|
+
# 历史版本(by-date):列出 → 取指定版本
|
|
62
|
+
nexus-agent pull-config --ticker tqqq --kind normal --list --since 2026-06-01
|
|
63
|
+
nexus-agent pull-config --ticker tqqq --kind normal --date 2026-06-01_to_2026-06-14 --out old.json
|
|
64
|
+
nexus-agent get-market-analysis --ticker tqqq --list
|
|
65
|
+
nexus-agent get-portfolio --list --since 2026-06-01
|
|
66
|
+
|
|
67
|
+
# 回传 Position-Research 产出的 3 套仓位 config(≠ push-backtest 的回测聚合)
|
|
68
|
+
nexus-agent push-config --ticker tqqq --time-range 2026-06-01_2026-06-14 \
|
|
69
|
+
--aggressive tqqq-aggressive.json \
|
|
70
|
+
--conservative tqqq-conservative.json \
|
|
71
|
+
--normal tqqq-normal.json
|
|
72
|
+
|
|
53
73
|
# 服务端 SES 发通知邮件
|
|
54
74
|
nexus-agent notify --to a@x.com --to b@y.com --subject "Weekly report" --body-file summary.txt
|
|
55
75
|
```
|
|
@@ -62,7 +82,7 @@ nexus-agent notify --to a@x.com --to b@y.com --subject "Weekly report" --body-fi
|
|
|
62
82
|
| `NEXUS_AGENT_API_BASE_URL` | — | 直接覆盖完整 API base(含 `/api`) |
|
|
63
83
|
| `NEXUS_AGENT_COGNITO_DOMAIN` | `auth.lookatwallstreet.com` | Cognito Hosted UI 域名 |
|
|
64
84
|
| `NEXUS_AGENT_COGNITO_CLIENT_ID` | (内置) | Cognito app client id |
|
|
65
|
-
| `NEXUS_AGENT_REDIRECT_URI` | `http://127.0.0.1:
|
|
85
|
+
| `NEXUS_AGENT_REDIRECT_URI` | `http://127.0.0.1:8251/callback` | 本地回调(须 localhost+端口;端口必须已加入 Cognito app client 的 Allowed callback URLs) |
|
|
66
86
|
|
|
67
87
|
例如指向本地后端调试:
|
|
68
88
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.0"
|
|
@@ -60,6 +60,21 @@ def get_strategy_data(
|
|
|
60
60
|
return get_json("/nexus-agent/strategy-data/", params=params)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def get_backtest(ticker: str) -> Any:
|
|
64
|
+
"""GET /nexus-agent/backtest/ — latest aggregated backtest data for one ticker."""
|
|
65
|
+
return get_json("/nexus-agent/backtest/", params={"ticker": ticker})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_market_analysis(ticker: str) -> Any:
|
|
69
|
+
"""GET /nexus-agent/market-analysis/ — latest market analysis report for one ticker."""
|
|
70
|
+
return get_json("/nexus-agent/market-analysis/", params={"ticker": ticker})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_portfolio_recommendation() -> Any:
|
|
74
|
+
"""GET /nexus-agent/portfolio-recommendation/ — latest portfolio recommendation."""
|
|
75
|
+
return get_json("/nexus-agent/portfolio-recommendation/")
|
|
76
|
+
|
|
77
|
+
|
|
63
78
|
def upload_market_analysis(ticker: str, content: str, language: str = "zh") -> Any:
|
|
64
79
|
"""POST /nexus-agent/upload/market-analysis/"""
|
|
65
80
|
return post_json(
|
|
@@ -85,6 +100,63 @@ def upload_backtest(ticker: str, levels: dict[str, Any]) -> Any:
|
|
|
85
100
|
return post_json("/nexus-agent/upload/backtest/", {"ticker": ticker, **levels})
|
|
86
101
|
|
|
87
102
|
|
|
103
|
+
def get_position_config(
|
|
104
|
+
ticker: str, kind: str = "normal", date_range: str | None = None
|
|
105
|
+
) -> Any:
|
|
106
|
+
"""GET /nexus-agent/position-config/ — signal_position config from S3.
|
|
107
|
+
|
|
108
|
+
Mirrors the worker's download_latest_normal_config_from_s3: the server reads
|
|
109
|
+
configs/signal_position/{ticker}/<date_range>/{ticker}-{kind}.json and returns it
|
|
110
|
+
as JSON. With date_range, fetch that specific historical version; otherwise the
|
|
111
|
+
latest. This is the Position-Research baseline the local skill cannot otherwise read.
|
|
112
|
+
"""
|
|
113
|
+
params: dict[str, Any] = {"ticker": ticker, "kind": kind}
|
|
114
|
+
if date_range:
|
|
115
|
+
params["date_range"] = date_range
|
|
116
|
+
return get_json("/nexus-agent/position-config/", params=params)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def list_history(
|
|
120
|
+
artifact_type: str, ticker: str | None = None, kind: str | None = None, since: str | None = None
|
|
121
|
+
) -> Any:
|
|
122
|
+
"""GET /nexus-agent/history/ — list historical versions of an artifact.
|
|
123
|
+
|
|
124
|
+
artifact_type: config / market-analysis / portfolio.
|
|
125
|
+
Returns {type, count, items: [{key, created_at, date_range?}, ...]} newest-first.
|
|
126
|
+
"""
|
|
127
|
+
params: dict[str, Any] = {"type": artifact_type}
|
|
128
|
+
if ticker:
|
|
129
|
+
params["ticker"] = ticker
|
|
130
|
+
if kind:
|
|
131
|
+
params["kind"] = kind
|
|
132
|
+
if since:
|
|
133
|
+
params["since"] = since
|
|
134
|
+
return get_json("/nexus-agent/history/", params=params)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def upload_position_config(
|
|
138
|
+
ticker: str, configs: dict[str, Any], time_range: str | None = None
|
|
139
|
+
) -> Any:
|
|
140
|
+
"""POST /nexus-agent/upload/position-config/ — write the 3 position configs to S3.
|
|
141
|
+
|
|
142
|
+
configs: {aggressive/conservative/normal: {<level>: {...}}}. Distinct from
|
|
143
|
+
upload_backtest (which uploads backtest aggregation, not position configs).
|
|
144
|
+
Server writes configs/signal_position/{ticker}/{time_range}/{ticker}-{kind}.json
|
|
145
|
+
and invalidates the dashboard cache.
|
|
146
|
+
"""
|
|
147
|
+
body: dict[str, Any] = {"ticker": ticker, **configs}
|
|
148
|
+
if time_range is not None:
|
|
149
|
+
body["time_range"] = time_range
|
|
150
|
+
return post_json("/nexus-agent/upload/position-config/", body)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def upload_position_report(ticker: str, content: str) -> Any:
|
|
154
|
+
"""POST /nexus-agent/upload/position-report/ — archive a position-update narrative (md)."""
|
|
155
|
+
return post_json(
|
|
156
|
+
"/nexus-agent/upload/position-report/", {"ticker": ticker, "content": content}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
88
160
|
def notify(
|
|
89
161
|
to: list[str], subject: str, body_text: str | None = None, body_html: str | None = None
|
|
90
162
|
) -> Any:
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.json import JSON
|
|
9
|
+
|
|
10
|
+
from nexus_agent_cli import api_client
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _run_err(e: Exception) -> None:
|
|
16
|
+
console.print(f"[red]{e}[/red]")
|
|
17
|
+
raise typer.Exit(1) from e
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def register(app: typer.Typer) -> None:
|
|
21
|
+
@app.command("run-config", help="Pull this week's run params (tickers + date range).")
|
|
22
|
+
def run_config(
|
|
23
|
+
tickers: str = typer.Option(
|
|
24
|
+
None, "--tickers", help="Comma-separated override, e.g. TQQQ,SOXL,IBIT."
|
|
25
|
+
),
|
|
26
|
+
) -> None:
|
|
27
|
+
try:
|
|
28
|
+
data = api_client.get_run_config(tickers=tickers)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
_run_err(e)
|
|
31
|
+
console.print(JSON(json.dumps(data)))
|
|
32
|
+
|
|
33
|
+
@app.command(
|
|
34
|
+
"strategy-data",
|
|
35
|
+
help="Pull strategy trigger rows for one ticker (Sheets creds stay server-side).",
|
|
36
|
+
)
|
|
37
|
+
def strategy_data(
|
|
38
|
+
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
39
|
+
start: str = typer.Option(None, "--start", help="Start date YYYY-MM-DD (optional)."),
|
|
40
|
+
end: str = typer.Option(None, "--end", help="End date YYYY-MM-DD (optional)."),
|
|
41
|
+
out: Path = typer.Option(
|
|
42
|
+
None, "--out", help="Write the JSON response to this file instead of stdout."
|
|
43
|
+
),
|
|
44
|
+
) -> None:
|
|
45
|
+
try:
|
|
46
|
+
data = api_client.get_strategy_data(ticker, start_date=start, end_date=end)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
_run_err(e)
|
|
49
|
+
if out is not None:
|
|
50
|
+
out.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
51
|
+
console.print(f"[green]已保存 {data.get('count')} 行 →[/green] {out}")
|
|
52
|
+
else:
|
|
53
|
+
console.print(JSON(json.dumps(data)))
|
|
54
|
+
|
|
55
|
+
@app.command(
|
|
56
|
+
"push-market-analysis", help="Upload a market analysis markdown report for one ticker."
|
|
57
|
+
)
|
|
58
|
+
def push_market_analysis(
|
|
59
|
+
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
60
|
+
file: Path = typer.Option(
|
|
61
|
+
..., "--file", exists=True, readable=True, help="Path to the markdown report."
|
|
62
|
+
),
|
|
63
|
+
language: str = typer.Option("zh", "--language", help="zh (default) or en."),
|
|
64
|
+
) -> None:
|
|
65
|
+
try:
|
|
66
|
+
content = file.read_text(encoding="utf-8")
|
|
67
|
+
data = api_client.upload_market_analysis(ticker, content, language=language)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
_run_err(e)
|
|
70
|
+
console.print(f"[green]已上传:[/green] {data.get('key')}")
|
|
71
|
+
|
|
72
|
+
@app.command(
|
|
73
|
+
"push-portfolio", help="Upload a portfolio recommendation report (English + optional CN)."
|
|
74
|
+
)
|
|
75
|
+
def push_portfolio(
|
|
76
|
+
file: Path = typer.Option(
|
|
77
|
+
..., "--file", exists=True, readable=True, help="English markdown report."
|
|
78
|
+
),
|
|
79
|
+
file_cn: Path = typer.Option(
|
|
80
|
+
None, "--file-cn", exists=True, readable=True, help="Chinese markdown report (optional)."
|
|
81
|
+
),
|
|
82
|
+
time_range: str = typer.Option(
|
|
83
|
+
None, "--time-range", help="e.g. 2026-06-01_2026-06-14 (optional, for the S3 key)."
|
|
84
|
+
),
|
|
85
|
+
) -> None:
|
|
86
|
+
try:
|
|
87
|
+
content = file.read_text(encoding="utf-8")
|
|
88
|
+
content_cn = file_cn.read_text(encoding="utf-8") if file_cn else None
|
|
89
|
+
data = api_client.upload_portfolio_recommendation(
|
|
90
|
+
content, content_cn=content_cn, time_range=time_range
|
|
91
|
+
)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
_run_err(e)
|
|
94
|
+
console.print(JSON(json.dumps(data)))
|
|
95
|
+
|
|
96
|
+
@app.command("push-backtest", help="Upload aggregated backtest JSON for one ticker.")
|
|
97
|
+
def push_backtest(
|
|
98
|
+
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
99
|
+
aggressive: Path = typer.Option(
|
|
100
|
+
None, "--aggressive", exists=True, readable=True, help="aggressive_aggregated.json"
|
|
101
|
+
),
|
|
102
|
+
conservative: Path = typer.Option(
|
|
103
|
+
None, "--conservative", exists=True, readable=True, help="conservative_aggregated.json"
|
|
104
|
+
),
|
|
105
|
+
normal: Path = typer.Option(
|
|
106
|
+
None, "--normal", exists=True, readable=True, help="normal_aggregated.json"
|
|
107
|
+
),
|
|
108
|
+
) -> None:
|
|
109
|
+
levels: dict[str, object] = {}
|
|
110
|
+
for name, path in (
|
|
111
|
+
("aggressive", aggressive),
|
|
112
|
+
("conservative", conservative),
|
|
113
|
+
("normal", normal),
|
|
114
|
+
):
|
|
115
|
+
if path is not None:
|
|
116
|
+
try:
|
|
117
|
+
levels[name] = json.loads(path.read_text(encoding="utf-8"))
|
|
118
|
+
except Exception as e:
|
|
119
|
+
_run_err(e)
|
|
120
|
+
if not levels:
|
|
121
|
+
console.print("[red]至少提供一个档位文件: --aggressive / --conservative / --normal[/red]")
|
|
122
|
+
raise typer.Exit(1)
|
|
123
|
+
try:
|
|
124
|
+
data = api_client.upload_backtest(ticker, levels)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
_run_err(e)
|
|
127
|
+
console.print(JSON(json.dumps(data)))
|
|
128
|
+
|
|
129
|
+
@app.command(
|
|
130
|
+
"pull-config",
|
|
131
|
+
help="Pull the latest signal_position config for one ticker from S3 (the "
|
|
132
|
+
"Position-Research baseline; S3 creds stay server-side).",
|
|
133
|
+
)
|
|
134
|
+
def pull_config(
|
|
135
|
+
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
136
|
+
kind: str = typer.Option(
|
|
137
|
+
"normal", "--kind", help="normal (default) / aggressive / conservative."
|
|
138
|
+
),
|
|
139
|
+
list_versions: bool = typer.Option(
|
|
140
|
+
False, "--list", help="List historical config versions instead of fetching one."
|
|
141
|
+
),
|
|
142
|
+
date_range: str = typer.Option(
|
|
143
|
+
None, "--date", help="Fetch a specific version by its date_range (from --list)."
|
|
144
|
+
),
|
|
145
|
+
since: str = typer.Option(
|
|
146
|
+
None, "--since", help="With --list: only versions created on/after this ISO date."
|
|
147
|
+
),
|
|
148
|
+
out: Path = typer.Option(
|
|
149
|
+
None, "--out", help="Write JSON to this file instead of stdout."
|
|
150
|
+
),
|
|
151
|
+
) -> None:
|
|
152
|
+
try:
|
|
153
|
+
if list_versions:
|
|
154
|
+
data = api_client.list_history("config", ticker=ticker, kind=kind, since=since)
|
|
155
|
+
else:
|
|
156
|
+
data = api_client.get_position_config(ticker, kind=kind, date_range=date_range)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
_run_err(e)
|
|
159
|
+
_emit(data, out)
|
|
160
|
+
|
|
161
|
+
def _emit(data: object, out) -> None:
|
|
162
|
+
if out is not None:
|
|
163
|
+
out.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
164
|
+
console.print(f"[green]已保存 →[/green] {out}")
|
|
165
|
+
else:
|
|
166
|
+
console.print(JSON(json.dumps(data)))
|
|
167
|
+
|
|
168
|
+
@app.command(
|
|
169
|
+
"get-backtest", help="Fetch the latest aggregated backtest data for one ticker from S3."
|
|
170
|
+
)
|
|
171
|
+
def get_backtest(
|
|
172
|
+
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
173
|
+
out: Path = typer.Option(None, "--out", help="Write JSON to this file instead of stdout."),
|
|
174
|
+
) -> None:
|
|
175
|
+
try:
|
|
176
|
+
data = api_client.get_backtest(ticker)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
_run_err(e)
|
|
179
|
+
_emit(data, out)
|
|
180
|
+
|
|
181
|
+
@app.command(
|
|
182
|
+
"get-market-analysis",
|
|
183
|
+
help="Fetch the latest market analysis report for one ticker from S3.",
|
|
184
|
+
)
|
|
185
|
+
def get_market_analysis(
|
|
186
|
+
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
187
|
+
list_versions: bool = typer.Option(
|
|
188
|
+
False, "--list", help="List historical report versions instead of fetching the latest."
|
|
189
|
+
),
|
|
190
|
+
since: str = typer.Option(
|
|
191
|
+
None, "--since", help="With --list: only versions created on/after this ISO date."
|
|
192
|
+
),
|
|
193
|
+
out: Path = typer.Option(None, "--out", help="Write JSON to this file instead of stdout."),
|
|
194
|
+
) -> None:
|
|
195
|
+
try:
|
|
196
|
+
if list_versions:
|
|
197
|
+
data = api_client.list_history("market-analysis", ticker=ticker, since=since)
|
|
198
|
+
else:
|
|
199
|
+
data = api_client.get_market_analysis(ticker)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
_run_err(e)
|
|
202
|
+
_emit(data, out)
|
|
203
|
+
|
|
204
|
+
@app.command(
|
|
205
|
+
"get-portfolio", help="Fetch the latest portfolio recommendation from S3."
|
|
206
|
+
)
|
|
207
|
+
def get_portfolio(
|
|
208
|
+
list_versions: bool = typer.Option(
|
|
209
|
+
False, "--list", help="List historical portfolio versions instead of the latest."
|
|
210
|
+
),
|
|
211
|
+
since: str = typer.Option(
|
|
212
|
+
None, "--since", help="With --list: only versions created on/after this ISO date."
|
|
213
|
+
),
|
|
214
|
+
out: Path = typer.Option(None, "--out", help="Write JSON to this file instead of stdout."),
|
|
215
|
+
) -> None:
|
|
216
|
+
try:
|
|
217
|
+
if list_versions:
|
|
218
|
+
data = api_client.list_history("portfolio", since=since)
|
|
219
|
+
else:
|
|
220
|
+
data = api_client.get_portfolio_recommendation()
|
|
221
|
+
except Exception as e:
|
|
222
|
+
_run_err(e)
|
|
223
|
+
_emit(data, out)
|
|
224
|
+
|
|
225
|
+
@app.command(
|
|
226
|
+
"push-config",
|
|
227
|
+
help="Upload the 3 Position-Research configs for one ticker "
|
|
228
|
+
"(distinct from push-backtest, which uploads backtest aggregation).",
|
|
229
|
+
)
|
|
230
|
+
def push_config(
|
|
231
|
+
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
232
|
+
aggressive: Path = typer.Option(
|
|
233
|
+
None, "--aggressive", exists=True, readable=True, help="{ticker}-aggressive.json"
|
|
234
|
+
),
|
|
235
|
+
conservative: Path = typer.Option(
|
|
236
|
+
None, "--conservative", exists=True, readable=True, help="{ticker}-conservative.json"
|
|
237
|
+
),
|
|
238
|
+
normal: Path = typer.Option(
|
|
239
|
+
None, "--normal", exists=True, readable=True, help="{ticker}-normal.json"
|
|
240
|
+
),
|
|
241
|
+
time_range: str = typer.Option(
|
|
242
|
+
None, "--time-range", help="e.g. 2026-06-01_2026-06-14 (for the S3 key)."
|
|
243
|
+
),
|
|
244
|
+
) -> None:
|
|
245
|
+
configs: dict[str, object] = {}
|
|
246
|
+
for name, path in (
|
|
247
|
+
("aggressive", aggressive),
|
|
248
|
+
("conservative", conservative),
|
|
249
|
+
("normal", normal),
|
|
250
|
+
):
|
|
251
|
+
if path is not None:
|
|
252
|
+
try:
|
|
253
|
+
configs[name] = json.loads(path.read_text(encoding="utf-8"))
|
|
254
|
+
except Exception as e:
|
|
255
|
+
_run_err(e)
|
|
256
|
+
if not configs:
|
|
257
|
+
console.print("[red]至少提供一个档位文件: --aggressive / --conservative / --normal[/red]")
|
|
258
|
+
raise typer.Exit(1)
|
|
259
|
+
try:
|
|
260
|
+
data = api_client.upload_position_config(ticker, configs, time_range=time_range)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
_run_err(e)
|
|
263
|
+
console.print(JSON(json.dumps(data)))
|
|
264
|
+
|
|
265
|
+
@app.command(
|
|
266
|
+
"push-position-report",
|
|
267
|
+
help="Archive a position-update narrative markdown (reports/position_updates/).",
|
|
268
|
+
)
|
|
269
|
+
def push_position_report(
|
|
270
|
+
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
271
|
+
file: Path = typer.Option(
|
|
272
|
+
..., "--file", exists=True, readable=True, help="Path to the markdown report."
|
|
273
|
+
),
|
|
274
|
+
) -> None:
|
|
275
|
+
try:
|
|
276
|
+
data = api_client.upload_position_report(ticker, file.read_text(encoding="utf-8"))
|
|
277
|
+
except Exception as e:
|
|
278
|
+
_run_err(e)
|
|
279
|
+
console.print(f"[green]已上传:[/green] {data.get('key')}")
|
|
280
|
+
|
|
281
|
+
@app.command("notify", help="Send a notification email via server-side SES.")
|
|
282
|
+
def notify(
|
|
283
|
+
to: list[str] = typer.Option(..., "--to", help="Recipient (repeatable)."),
|
|
284
|
+
subject: str = typer.Option(..., "--subject", help="Email subject."),
|
|
285
|
+
body_file: Path = typer.Option(
|
|
286
|
+
None, "--body-file", exists=True, readable=True, help="Plain-text body file."
|
|
287
|
+
),
|
|
288
|
+
body: str = typer.Option(None, "--body", help="Inline plain-text body."),
|
|
289
|
+
) -> None:
|
|
290
|
+
body_text = body
|
|
291
|
+
if body_file is not None:
|
|
292
|
+
body_text = body_file.read_text(encoding="utf-8")
|
|
293
|
+
if not body_text:
|
|
294
|
+
console.print("[red]需要 --body 或 --body-file 之一。[/red]")
|
|
295
|
+
raise typer.Exit(1)
|
|
296
|
+
try:
|
|
297
|
+
data = api_client.notify(list(to), subject, body_text=body_text)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
_run_err(e)
|
|
300
|
+
console.print(f"[green]已发送:[/green] {data}")
|
|
@@ -15,8 +15,11 @@ CREDENTIALS_PATH = CONFIG_DIR / "credentials.json"
|
|
|
15
15
|
DEFAULT_API_ENDPOINT = "https://api.nexusquant.co"
|
|
16
16
|
DEFAULT_COGNITO_DOMAIN = "auth.lookatwallstreet.com"
|
|
17
17
|
DEFAULT_COGNITO_CLIENT_ID = "5o7hhd4hn4birr8c29vndehiat"
|
|
18
|
-
# 与 nexusquant-cli
|
|
19
|
-
|
|
18
|
+
# 与 nexusquant-cli 共用同一个 Cognito app client(5o7hhd4hn4birr8c29vndehiat),
|
|
19
|
+
# 该 client 只登记了 8251/callback。沿用 8251 以避免 redirect_mismatch;
|
|
20
|
+
# 两套 CLI 不会同时跑 OAuth 回调,端口冲突可忽略。需要错开时用 NEXUS_AGENT_REDIRECT_URI 覆盖
|
|
21
|
+
# (但所选端口必须已加入 app client 的 Allowed callback URLs)。
|
|
22
|
+
DEFAULT_REDIRECT_URI = "http://127.0.0.1:8251/callback"
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def _env(name: str, default: str | None = None) -> str | None:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nexus-agent-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Nexus internal agent ops CLI — pull run params, push analysis results, send notifications. Admin only."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import typer
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
from rich.json import JSON
|
|
9
|
-
|
|
10
|
-
from nexus_agent_cli import api_client
|
|
11
|
-
|
|
12
|
-
console = Console()
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _run_err(e: Exception) -> None:
|
|
16
|
-
console.print(f"[red]{e}[/red]")
|
|
17
|
-
raise typer.Exit(1) from e
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def register(app: typer.Typer) -> None:
|
|
21
|
-
@app.command("run-config", help="Pull this week's run params (tickers + date range).")
|
|
22
|
-
def run_config(
|
|
23
|
-
tickers: str = typer.Option(
|
|
24
|
-
None, "--tickers", help="Comma-separated override, e.g. TQQQ,SOXL,IBIT."
|
|
25
|
-
),
|
|
26
|
-
) -> None:
|
|
27
|
-
try:
|
|
28
|
-
data = api_client.get_run_config(tickers=tickers)
|
|
29
|
-
except Exception as e:
|
|
30
|
-
_run_err(e)
|
|
31
|
-
console.print(JSON(json.dumps(data)))
|
|
32
|
-
|
|
33
|
-
@app.command(
|
|
34
|
-
"strategy-data",
|
|
35
|
-
help="Pull strategy trigger rows for one ticker (Sheets creds stay server-side).",
|
|
36
|
-
)
|
|
37
|
-
def strategy_data(
|
|
38
|
-
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
39
|
-
start: str = typer.Option(None, "--start", help="Start date YYYY-MM-DD (optional)."),
|
|
40
|
-
end: str = typer.Option(None, "--end", help="End date YYYY-MM-DD (optional)."),
|
|
41
|
-
out: Path = typer.Option(
|
|
42
|
-
None, "--out", help="Write the JSON response to this file instead of stdout."
|
|
43
|
-
),
|
|
44
|
-
) -> None:
|
|
45
|
-
try:
|
|
46
|
-
data = api_client.get_strategy_data(ticker, start_date=start, end_date=end)
|
|
47
|
-
except Exception as e:
|
|
48
|
-
_run_err(e)
|
|
49
|
-
if out is not None:
|
|
50
|
-
out.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
51
|
-
console.print(f"[green]已保存 {data.get('count')} 行 →[/green] {out}")
|
|
52
|
-
else:
|
|
53
|
-
console.print(JSON(json.dumps(data)))
|
|
54
|
-
|
|
55
|
-
@app.command(
|
|
56
|
-
"push-market-analysis", help="Upload a market analysis markdown report for one ticker."
|
|
57
|
-
)
|
|
58
|
-
def push_market_analysis(
|
|
59
|
-
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
60
|
-
file: Path = typer.Option(
|
|
61
|
-
..., "--file", exists=True, readable=True, help="Path to the markdown report."
|
|
62
|
-
),
|
|
63
|
-
language: str = typer.Option("zh", "--language", help="zh (default) or en."),
|
|
64
|
-
) -> None:
|
|
65
|
-
try:
|
|
66
|
-
content = file.read_text(encoding="utf-8")
|
|
67
|
-
data = api_client.upload_market_analysis(ticker, content, language=language)
|
|
68
|
-
except Exception as e:
|
|
69
|
-
_run_err(e)
|
|
70
|
-
console.print(f"[green]已上传:[/green] {data.get('key')}")
|
|
71
|
-
|
|
72
|
-
@app.command(
|
|
73
|
-
"push-portfolio", help="Upload a portfolio recommendation report (English + optional CN)."
|
|
74
|
-
)
|
|
75
|
-
def push_portfolio(
|
|
76
|
-
file: Path = typer.Option(
|
|
77
|
-
..., "--file", exists=True, readable=True, help="English markdown report."
|
|
78
|
-
),
|
|
79
|
-
file_cn: Path = typer.Option(
|
|
80
|
-
None, "--file-cn", exists=True, readable=True, help="Chinese markdown report (optional)."
|
|
81
|
-
),
|
|
82
|
-
time_range: str = typer.Option(
|
|
83
|
-
None, "--time-range", help="e.g. 2026-06-01_2026-06-14 (optional, for the S3 key)."
|
|
84
|
-
),
|
|
85
|
-
) -> None:
|
|
86
|
-
try:
|
|
87
|
-
content = file.read_text(encoding="utf-8")
|
|
88
|
-
content_cn = file_cn.read_text(encoding="utf-8") if file_cn else None
|
|
89
|
-
data = api_client.upload_portfolio_recommendation(
|
|
90
|
-
content, content_cn=content_cn, time_range=time_range
|
|
91
|
-
)
|
|
92
|
-
except Exception as e:
|
|
93
|
-
_run_err(e)
|
|
94
|
-
console.print(JSON(json.dumps(data)))
|
|
95
|
-
|
|
96
|
-
@app.command("push-backtest", help="Upload aggregated backtest JSON for one ticker.")
|
|
97
|
-
def push_backtest(
|
|
98
|
-
ticker: str = typer.Option(..., "--ticker", help="tqqq / soxl / ibit."),
|
|
99
|
-
aggressive: Path = typer.Option(
|
|
100
|
-
None, "--aggressive", exists=True, readable=True, help="aggressive_aggregated.json"
|
|
101
|
-
),
|
|
102
|
-
conservative: Path = typer.Option(
|
|
103
|
-
None, "--conservative", exists=True, readable=True, help="conservative_aggregated.json"
|
|
104
|
-
),
|
|
105
|
-
normal: Path = typer.Option(
|
|
106
|
-
None, "--normal", exists=True, readable=True, help="normal_aggregated.json"
|
|
107
|
-
),
|
|
108
|
-
) -> None:
|
|
109
|
-
levels: dict[str, object] = {}
|
|
110
|
-
for name, path in (
|
|
111
|
-
("aggressive", aggressive),
|
|
112
|
-
("conservative", conservative),
|
|
113
|
-
("normal", normal),
|
|
114
|
-
):
|
|
115
|
-
if path is not None:
|
|
116
|
-
try:
|
|
117
|
-
levels[name] = json.loads(path.read_text(encoding="utf-8"))
|
|
118
|
-
except Exception as e:
|
|
119
|
-
_run_err(e)
|
|
120
|
-
if not levels:
|
|
121
|
-
console.print("[red]至少提供一个档位文件: --aggressive / --conservative / --normal[/red]")
|
|
122
|
-
raise typer.Exit(1)
|
|
123
|
-
try:
|
|
124
|
-
data = api_client.upload_backtest(ticker, levels)
|
|
125
|
-
except Exception as e:
|
|
126
|
-
_run_err(e)
|
|
127
|
-
console.print(JSON(json.dumps(data)))
|
|
128
|
-
|
|
129
|
-
@app.command("notify", help="Send a notification email via server-side SES.")
|
|
130
|
-
def notify(
|
|
131
|
-
to: list[str] = typer.Option(..., "--to", help="Recipient (repeatable)."),
|
|
132
|
-
subject: str = typer.Option(..., "--subject", help="Email subject."),
|
|
133
|
-
body_file: Path = typer.Option(
|
|
134
|
-
None, "--body-file", exists=True, readable=True, help="Plain-text body file."
|
|
135
|
-
),
|
|
136
|
-
body: str = typer.Option(None, "--body", help="Inline plain-text body."),
|
|
137
|
-
) -> None:
|
|
138
|
-
body_text = body
|
|
139
|
-
if body_file is not None:
|
|
140
|
-
body_text = body_file.read_text(encoding="utf-8")
|
|
141
|
-
if not body_text:
|
|
142
|
-
console.print("[red]需要 --body 或 --body-file 之一。[/red]")
|
|
143
|
-
raise typer.Exit(1)
|
|
144
|
-
try:
|
|
145
|
-
data = api_client.notify(list(to), subject, body_text=body_text)
|
|
146
|
-
except Exception as e:
|
|
147
|
-
_run_err(e)
|
|
148
|
-
console.print(f"[green]已发送:[/green] {data}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|