nexusquant-cli 0.1.2__tar.gz → 0.2.1__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.
- nexusquant_cli-0.2.1/PKG-INFO +269 -0
- nexusquant_cli-0.2.1/README.md +257 -0
- nexusquant_cli-0.2.1/nexusquant_cli/__init__.py +1 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/commands/__init__.py +2 -0
- nexusquant_cli-0.2.1/nexusquant_cli/commands/policy/__init__.py +0 -0
- nexusquant_cli-0.2.1/nexusquant_cli/commands/policy/cmd.py +327 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/pyproject.toml +4 -1
- nexusquant_cli-0.1.2/PKG-INFO +0 -138
- nexusquant_cli-0.1.2/README.md +0 -127
- nexusquant_cli-0.1.2/nexusquant_cli/__init__.py +0 -1
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/.gitignore +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/CLAUDE.md +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/__main__.py +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/api_client.py +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/auth_pkce.py +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/commands/_util.py +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/commands/auth/__init__.py +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/commands/auth/cmd.py +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/commands/strategy/__init__.py +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/commands/strategy/cmd.py +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/config.py +0 -0
- {nexusquant_cli-0.1.2 → nexusquant_cli-0.2.1}/nexusquant_cli/main.py +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nexusquant-cli
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: NexusQuant strategy provider CLI
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: httpx>=0.27
|
|
7
|
+
Requires-Dist: nexusquant-sdk>=0.2
|
|
8
|
+
Requires-Dist: platformdirs>=4.2
|
|
9
|
+
Requires-Dist: rich>=13.7
|
|
10
|
+
Requires-Dist: typer>=0.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# nexusquant-cli
|
|
14
|
+
|
|
15
|
+
Command line tool for NexusQuant strategy providers.
|
|
16
|
+
|
|
17
|
+
It supports browser login through Cognito, stores tokens locally, refreshes tokens, and calls the Nexus strategy provider APIs. Provider commands require a Cognito user with `custom:userType=strategyProvider` or `custom:userType=admin`.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python3 -m pip install -e .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install nexusquant-cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Login
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
nexusquant auth
|
|
33
|
+
nexusquant auth --status
|
|
34
|
+
nexusquant auth --refresh
|
|
35
|
+
nexusquant auth --logout
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Tokens are stored in the current OS user's config directory, for example `~/Library/Application Support/nexusquant-cli/credentials.json` on macOS. The file is written with `0600` permissions when supported.
|
|
39
|
+
|
|
40
|
+
## Strategy Commands
|
|
41
|
+
|
|
42
|
+
Create or update a strategy:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
nexusquant strategy create \
|
|
46
|
+
--strategy-id my_alpha_001 \
|
|
47
|
+
--name "My Alpha" \
|
|
48
|
+
--schema-file schema.json \
|
|
49
|
+
--output-unit SHARE_COUNT
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
List strategies registered by the current provider. Admin users see all strategies:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
nexusquant strategy list
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
List signal history for one strategy:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
nexusquant strategy signal my_alpha_001 --history --limit 20
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Send a single signal:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
nexusquant strategy signal my_alpha_001 \
|
|
68
|
+
--strategy-name "My Alpha" \
|
|
69
|
+
--ticker AAPL \
|
|
70
|
+
--direction buy \
|
|
71
|
+
--price 150.25 \
|
|
72
|
+
--quantity 100 \
|
|
73
|
+
--order-type MARKET
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`--order-type` is a signal-level setting: `MARKET` or `LIMIT` (limit uses `--price`). Legacy `NORMAL` is accepted by the CLI as an alias for `LIMIT`. If omitted, the API defaults to `MARKET`.
|
|
77
|
+
|
|
78
|
+
Send a multi-route `signals` map:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
nexusquant strategy signal my_alpha_001 \
|
|
82
|
+
--strategy-name "My Alpha" \
|
|
83
|
+
--signals-file signals.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Get non-PII subscriber config for a strategy:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
nexusquant strategy sub config my_alpha_001
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Policy Commands
|
|
93
|
+
|
|
94
|
+
A SizingPolicy replaces the fixed `--quantity` on a signal with a **formula** that
|
|
95
|
+
decides the order quantity — and optionally a limit price — at order time. You
|
|
96
|
+
publish the formula once; each signal then carries only the parameter values it
|
|
97
|
+
needs. Values that depend on the individual subscriber's account are never sent by
|
|
98
|
+
you: they are bound inside that user's own container when the order is placed.
|
|
99
|
+
|
|
100
|
+
Requires `custom:userType=strategyProvider` or `admin`, the same as the strategy
|
|
101
|
+
commands.
|
|
102
|
+
|
|
103
|
+
### See which names a formula may use
|
|
104
|
+
|
|
105
|
+
The server registry is the authority — a name outside it cannot be published. Run
|
|
106
|
+
this before writing a formula:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
nexusquant policy features
|
|
110
|
+
nexusquant policy features --scope shared # names you send with each signal
|
|
111
|
+
nexusquant policy features --scope account # names bound in the user's container
|
|
112
|
+
nexusquant policy features --json # raw output for scripting
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Account-scoped names carry a "suppliable" flag: a name that exists but that the
|
|
116
|
+
container cannot currently supply will pass your editor and fail at publish.
|
|
117
|
+
|
|
118
|
+
### Publish a formula
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
nexusquant policy publish --strategy-id my_alpha --ticker TQQQ --file tqqq.json
|
|
122
|
+
nexusquant policy publish --strategy-id my_alpha --ticker TQQQ --file tqqq.json --profile normal
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`--file` holds the two things a policy is made of — the formulas keyed by output
|
|
126
|
+
slot, and the names you promise to supply with every signal:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"expr": {
|
|
131
|
+
"f_depth": "exp(-k_depth * depth / 10)",
|
|
132
|
+
"buy.quantity": "target_shares * f_depth * clamp(1 - pos_ratio, 0, 1)",
|
|
133
|
+
"buy.price": "ref_price * (1 - slip)",
|
|
134
|
+
"sell.quantity": "held_shares * exit_frac"
|
|
135
|
+
},
|
|
136
|
+
"params": ["target_shares", "k_depth", "depth", "ref_price", "slip", "exit_frac"]
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Output slots — the container picks the one matching the signal's direction:
|
|
141
|
+
|
|
142
|
+
| Slot | What the formula yields |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `buy.quantity` / `sell.quantity` | **Share count** (floored) |
|
|
145
|
+
| `buy.price` / `sell.price` | **Limit price** — supplied ⇒ limit order, omitted ⇒ market order |
|
|
146
|
+
|
|
147
|
+
Intermediate names (`f_depth`) are fine, but an `expr` must define at least one
|
|
148
|
+
output slot, and an intermediate name may not contain a dot. `--ticker` is separate
|
|
149
|
+
from the file because it is not part of the artifact: the same formula pointed at
|
|
150
|
+
another symbol is the same formula.
|
|
151
|
+
|
|
152
|
+
⚠️ **`publish` IS the go-live action.** There is no shadow or canary step, and the
|
|
153
|
+
previous version on the same slot is deactivated. `--profile` defaults to `normal`.
|
|
154
|
+
|
|
155
|
+
Before sending anything, the CLI fetches the registry and checks locally that every
|
|
156
|
+
free name in your formula has someone to supply it — forgetting to list a name in
|
|
157
|
+
`params` is the most common mistake. `--skip-name-check` disables that fetch, and
|
|
158
|
+
then local success does not imply the server will accept.
|
|
159
|
+
|
|
160
|
+
### List and stop
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
nexusquant policy list my_alpha
|
|
164
|
+
nexusquant policy list my_alpha --ticker TQQQ --mode active # --mode active / off
|
|
165
|
+
|
|
166
|
+
# Emergency stop for a live version — not a promotion step, since publish already went live
|
|
167
|
+
nexusquant policy set-mode --policy-id 'TQQQ/normal/…/abc123' --mode off
|
|
168
|
+
nexusquant policy set-mode --policy-id 'TQQQ/normal/…/abc123' --mode active
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Send a signal against a policy
|
|
172
|
+
|
|
173
|
+
Every declared parameter must be present, or the signal is rejected:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
nexusquant policy send-signal \
|
|
177
|
+
--strategy-id my_alpha --ticker TQQQ --price 51.2 --direction buy \
|
|
178
|
+
-p target_shares=10 -p k_depth=0.5 -p depth=3 \
|
|
179
|
+
-p ref_price=51.2 -p slip=0.002 -p exit_frac=0.25
|
|
180
|
+
|
|
181
|
+
# Or pass all parameter values as a JSON object
|
|
182
|
+
nexusquant policy send-signal --strategy-id my_alpha --ticker TQQQ \
|
|
183
|
+
--price 51.2 --direction buy --params-file params.json
|
|
184
|
+
|
|
185
|
+
# Validate and print the payload without sending it
|
|
186
|
+
nexusquant policy send-signal ... --dry-run
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
| Flag | Meaning |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `--param` / `-p` | One parameter value, `name=value`; repeat per declared name |
|
|
192
|
+
| `--params-file` | A JSON object with all parameter values instead |
|
|
193
|
+
| `--policy-id` | Defaults to the active policy on this `(strategy, ticker)` |
|
|
194
|
+
| `--quantity` | Used when the formula has no slot for this direction |
|
|
195
|
+
| `--order-type` | `MARKET` / `LIMIT` |
|
|
196
|
+
| `--strategy-name` | Defaults to the strategy id |
|
|
197
|
+
| `--no-fetch` | Skip fetching the policy — see below |
|
|
198
|
+
| `--dry-run` | Validate and print the payload, send nothing |
|
|
199
|
+
|
|
200
|
+
By default the CLI fetches the active policy first and uses it to validate and
|
|
201
|
+
coerce your values. Three checks, each mirroring the server:
|
|
202
|
+
|
|
203
|
+
| Situation | Result | Why not something else |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| A declared parameter is missing | Error | No defaults — a default turns "unknown" into "known" |
|
|
206
|
+
| A name you did not declare | Error | The formula cannot reference it; extra names mean the two sides disagree |
|
|
207
|
+
| An account-state name (`pos_ratio`, `held_shares`, …) | Error | Its value never leaves the execution plane |
|
|
208
|
+
|
|
209
|
+
`--no-fetch` skips the fetch, and then **only the account-state rule is checked** —
|
|
210
|
+
the rest is unknowable locally, so it is not pretended.
|
|
211
|
+
|
|
212
|
+
⚠️ **The server is always the authority.** Passing local validation does not mean
|
|
213
|
+
the server will accept — policy state and review status live there. This layer only
|
|
214
|
+
moves the obvious mistakes onto your machine, where you can see which field is
|
|
215
|
+
wrong instead of guessing from a rejection.
|
|
216
|
+
|
|
217
|
+
⚠️ **There is no publish-time bound on order size.** Parameters are names only,
|
|
218
|
+
with no declared domain, so nothing proves at publish time that a formula stays
|
|
219
|
+
under a limit. How large an order it can place is clamped at order time by the
|
|
220
|
+
subscriber's own `max_order_cash_usd`. Keep your formulas in a sane range yourself.
|
|
221
|
+
|
|
222
|
+
## JSON Inputs
|
|
223
|
+
|
|
224
|
+
`--schema-file` must contain the `strategy_schema` object accepted by `nexus-service`, for example:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"type": "object",
|
|
229
|
+
"properties": {
|
|
230
|
+
"window": {
|
|
231
|
+
"type": "integer",
|
|
232
|
+
"default": 14,
|
|
233
|
+
"title": "Window",
|
|
234
|
+
"source": "user"
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
"required": []
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`--signals-file` must contain a JSON object whose keys are `default` or user ids:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"default": {
|
|
246
|
+
"ticker": "AAPL",
|
|
247
|
+
"time": "2026-04-26T19:30:00Z",
|
|
248
|
+
"price": 150.25,
|
|
249
|
+
"direction": "buy",
|
|
250
|
+
"order_type": "LIMIT",
|
|
251
|
+
"quantity": 100,
|
|
252
|
+
"metadata": {}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Environment Overrides
|
|
258
|
+
|
|
259
|
+
Normal use does not require configuration. For staging or local development:
|
|
260
|
+
|
|
261
|
+
| Variable | Meaning |
|
|
262
|
+
| ------------------------------ | ------------------------------------------------------------------------------------- |
|
|
263
|
+
| `NEXUSQUANT_API_ENDPOINT` | API endpoint host, default `https://api.nexusquant.co`; CLI calls `/api/...` under it |
|
|
264
|
+
| `NEXUSQUANT_API_BASE_URL` | Optional full API base override, e.g. `https://api.nexusquant.co/api` |
|
|
265
|
+
| `NEXUSQUANT_COGNITO_DOMAIN` | Cognito Hosted UI host, default `auth.lookatwallstreet.com` |
|
|
266
|
+
| `NEXUSQUANT_COGNITO_CLIENT_ID` | Cognito app client id |
|
|
267
|
+
| `NEXUSQUANT_REDIRECT_URI` | OAuth callback, default `http://127.0.0.1:8251/callback` |
|
|
268
|
+
|
|
269
|
+
There is also a hidden typo-compatible alias: `nexusquant startegy ...` maps to `nexusquant strategy ...`.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# nexusquant-cli
|
|
2
|
+
|
|
3
|
+
Command line tool for NexusQuant strategy providers.
|
|
4
|
+
|
|
5
|
+
It supports browser login through Cognito, stores tokens locally, refreshes tokens, and calls the Nexus strategy provider APIs. Provider commands require a Cognito user with `custom:userType=strategyProvider` or `custom:userType=admin`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python3 -m pip install -e .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install nexusquant-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Login
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
nexusquant auth
|
|
21
|
+
nexusquant auth --status
|
|
22
|
+
nexusquant auth --refresh
|
|
23
|
+
nexusquant auth --logout
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Tokens are stored in the current OS user's config directory, for example `~/Library/Application Support/nexusquant-cli/credentials.json` on macOS. The file is written with `0600` permissions when supported.
|
|
27
|
+
|
|
28
|
+
## Strategy Commands
|
|
29
|
+
|
|
30
|
+
Create or update a strategy:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
nexusquant strategy create \
|
|
34
|
+
--strategy-id my_alpha_001 \
|
|
35
|
+
--name "My Alpha" \
|
|
36
|
+
--schema-file schema.json \
|
|
37
|
+
--output-unit SHARE_COUNT
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
List strategies registered by the current provider. Admin users see all strategies:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
nexusquant strategy list
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
List signal history for one strategy:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
nexusquant strategy signal my_alpha_001 --history --limit 20
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Send a single signal:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
nexusquant strategy signal my_alpha_001 \
|
|
56
|
+
--strategy-name "My Alpha" \
|
|
57
|
+
--ticker AAPL \
|
|
58
|
+
--direction buy \
|
|
59
|
+
--price 150.25 \
|
|
60
|
+
--quantity 100 \
|
|
61
|
+
--order-type MARKET
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`--order-type` is a signal-level setting: `MARKET` or `LIMIT` (limit uses `--price`). Legacy `NORMAL` is accepted by the CLI as an alias for `LIMIT`. If omitted, the API defaults to `MARKET`.
|
|
65
|
+
|
|
66
|
+
Send a multi-route `signals` map:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
nexusquant strategy signal my_alpha_001 \
|
|
70
|
+
--strategy-name "My Alpha" \
|
|
71
|
+
--signals-file signals.json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Get non-PII subscriber config for a strategy:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
nexusquant strategy sub config my_alpha_001
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Policy Commands
|
|
81
|
+
|
|
82
|
+
A SizingPolicy replaces the fixed `--quantity` on a signal with a **formula** that
|
|
83
|
+
decides the order quantity — and optionally a limit price — at order time. You
|
|
84
|
+
publish the formula once; each signal then carries only the parameter values it
|
|
85
|
+
needs. Values that depend on the individual subscriber's account are never sent by
|
|
86
|
+
you: they are bound inside that user's own container when the order is placed.
|
|
87
|
+
|
|
88
|
+
Requires `custom:userType=strategyProvider` or `admin`, the same as the strategy
|
|
89
|
+
commands.
|
|
90
|
+
|
|
91
|
+
### See which names a formula may use
|
|
92
|
+
|
|
93
|
+
The server registry is the authority — a name outside it cannot be published. Run
|
|
94
|
+
this before writing a formula:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
nexusquant policy features
|
|
98
|
+
nexusquant policy features --scope shared # names you send with each signal
|
|
99
|
+
nexusquant policy features --scope account # names bound in the user's container
|
|
100
|
+
nexusquant policy features --json # raw output for scripting
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Account-scoped names carry a "suppliable" flag: a name that exists but that the
|
|
104
|
+
container cannot currently supply will pass your editor and fail at publish.
|
|
105
|
+
|
|
106
|
+
### Publish a formula
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
nexusquant policy publish --strategy-id my_alpha --ticker TQQQ --file tqqq.json
|
|
110
|
+
nexusquant policy publish --strategy-id my_alpha --ticker TQQQ --file tqqq.json --profile normal
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`--file` holds the two things a policy is made of — the formulas keyed by output
|
|
114
|
+
slot, and the names you promise to supply with every signal:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"expr": {
|
|
119
|
+
"f_depth": "exp(-k_depth * depth / 10)",
|
|
120
|
+
"buy.quantity": "target_shares * f_depth * clamp(1 - pos_ratio, 0, 1)",
|
|
121
|
+
"buy.price": "ref_price * (1 - slip)",
|
|
122
|
+
"sell.quantity": "held_shares * exit_frac"
|
|
123
|
+
},
|
|
124
|
+
"params": ["target_shares", "k_depth", "depth", "ref_price", "slip", "exit_frac"]
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Output slots — the container picks the one matching the signal's direction:
|
|
129
|
+
|
|
130
|
+
| Slot | What the formula yields |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `buy.quantity` / `sell.quantity` | **Share count** (floored) |
|
|
133
|
+
| `buy.price` / `sell.price` | **Limit price** — supplied ⇒ limit order, omitted ⇒ market order |
|
|
134
|
+
|
|
135
|
+
Intermediate names (`f_depth`) are fine, but an `expr` must define at least one
|
|
136
|
+
output slot, and an intermediate name may not contain a dot. `--ticker` is separate
|
|
137
|
+
from the file because it is not part of the artifact: the same formula pointed at
|
|
138
|
+
another symbol is the same formula.
|
|
139
|
+
|
|
140
|
+
⚠️ **`publish` IS the go-live action.** There is no shadow or canary step, and the
|
|
141
|
+
previous version on the same slot is deactivated. `--profile` defaults to `normal`.
|
|
142
|
+
|
|
143
|
+
Before sending anything, the CLI fetches the registry and checks locally that every
|
|
144
|
+
free name in your formula has someone to supply it — forgetting to list a name in
|
|
145
|
+
`params` is the most common mistake. `--skip-name-check` disables that fetch, and
|
|
146
|
+
then local success does not imply the server will accept.
|
|
147
|
+
|
|
148
|
+
### List and stop
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
nexusquant policy list my_alpha
|
|
152
|
+
nexusquant policy list my_alpha --ticker TQQQ --mode active # --mode active / off
|
|
153
|
+
|
|
154
|
+
# Emergency stop for a live version — not a promotion step, since publish already went live
|
|
155
|
+
nexusquant policy set-mode --policy-id 'TQQQ/normal/…/abc123' --mode off
|
|
156
|
+
nexusquant policy set-mode --policy-id 'TQQQ/normal/…/abc123' --mode active
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Send a signal against a policy
|
|
160
|
+
|
|
161
|
+
Every declared parameter must be present, or the signal is rejected:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
nexusquant policy send-signal \
|
|
165
|
+
--strategy-id my_alpha --ticker TQQQ --price 51.2 --direction buy \
|
|
166
|
+
-p target_shares=10 -p k_depth=0.5 -p depth=3 \
|
|
167
|
+
-p ref_price=51.2 -p slip=0.002 -p exit_frac=0.25
|
|
168
|
+
|
|
169
|
+
# Or pass all parameter values as a JSON object
|
|
170
|
+
nexusquant policy send-signal --strategy-id my_alpha --ticker TQQQ \
|
|
171
|
+
--price 51.2 --direction buy --params-file params.json
|
|
172
|
+
|
|
173
|
+
# Validate and print the payload without sending it
|
|
174
|
+
nexusquant policy send-signal ... --dry-run
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| Flag | Meaning |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `--param` / `-p` | One parameter value, `name=value`; repeat per declared name |
|
|
180
|
+
| `--params-file` | A JSON object with all parameter values instead |
|
|
181
|
+
| `--policy-id` | Defaults to the active policy on this `(strategy, ticker)` |
|
|
182
|
+
| `--quantity` | Used when the formula has no slot for this direction |
|
|
183
|
+
| `--order-type` | `MARKET` / `LIMIT` |
|
|
184
|
+
| `--strategy-name` | Defaults to the strategy id |
|
|
185
|
+
| `--no-fetch` | Skip fetching the policy — see below |
|
|
186
|
+
| `--dry-run` | Validate and print the payload, send nothing |
|
|
187
|
+
|
|
188
|
+
By default the CLI fetches the active policy first and uses it to validate and
|
|
189
|
+
coerce your values. Three checks, each mirroring the server:
|
|
190
|
+
|
|
191
|
+
| Situation | Result | Why not something else |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| A declared parameter is missing | Error | No defaults — a default turns "unknown" into "known" |
|
|
194
|
+
| A name you did not declare | Error | The formula cannot reference it; extra names mean the two sides disagree |
|
|
195
|
+
| An account-state name (`pos_ratio`, `held_shares`, …) | Error | Its value never leaves the execution plane |
|
|
196
|
+
|
|
197
|
+
`--no-fetch` skips the fetch, and then **only the account-state rule is checked** —
|
|
198
|
+
the rest is unknowable locally, so it is not pretended.
|
|
199
|
+
|
|
200
|
+
⚠️ **The server is always the authority.** Passing local validation does not mean
|
|
201
|
+
the server will accept — policy state and review status live there. This layer only
|
|
202
|
+
moves the obvious mistakes onto your machine, where you can see which field is
|
|
203
|
+
wrong instead of guessing from a rejection.
|
|
204
|
+
|
|
205
|
+
⚠️ **There is no publish-time bound on order size.** Parameters are names only,
|
|
206
|
+
with no declared domain, so nothing proves at publish time that a formula stays
|
|
207
|
+
under a limit. How large an order it can place is clamped at order time by the
|
|
208
|
+
subscriber's own `max_order_cash_usd`. Keep your formulas in a sane range yourself.
|
|
209
|
+
|
|
210
|
+
## JSON Inputs
|
|
211
|
+
|
|
212
|
+
`--schema-file` must contain the `strategy_schema` object accepted by `nexus-service`, for example:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"type": "object",
|
|
217
|
+
"properties": {
|
|
218
|
+
"window": {
|
|
219
|
+
"type": "integer",
|
|
220
|
+
"default": 14,
|
|
221
|
+
"title": "Window",
|
|
222
|
+
"source": "user"
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"required": []
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`--signals-file` must contain a JSON object whose keys are `default` or user ids:
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"default": {
|
|
234
|
+
"ticker": "AAPL",
|
|
235
|
+
"time": "2026-04-26T19:30:00Z",
|
|
236
|
+
"price": 150.25,
|
|
237
|
+
"direction": "buy",
|
|
238
|
+
"order_type": "LIMIT",
|
|
239
|
+
"quantity": 100,
|
|
240
|
+
"metadata": {}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Environment Overrides
|
|
246
|
+
|
|
247
|
+
Normal use does not require configuration. For staging or local development:
|
|
248
|
+
|
|
249
|
+
| Variable | Meaning |
|
|
250
|
+
| ------------------------------ | ------------------------------------------------------------------------------------- |
|
|
251
|
+
| `NEXUSQUANT_API_ENDPOINT` | API endpoint host, default `https://api.nexusquant.co`; CLI calls `/api/...` under it |
|
|
252
|
+
| `NEXUSQUANT_API_BASE_URL` | Optional full API base override, e.g. `https://api.nexusquant.co/api` |
|
|
253
|
+
| `NEXUSQUANT_COGNITO_DOMAIN` | Cognito Hosted UI host, default `auth.lookatwallstreet.com` |
|
|
254
|
+
| `NEXUSQUANT_COGNITO_CLIENT_ID` | Cognito app client id |
|
|
255
|
+
| `NEXUSQUANT_REDIRECT_URI` | OAuth callback, default `http://127.0.0.1:8251/callback` |
|
|
256
|
+
|
|
257
|
+
There is also a hidden typo-compatible alias: `nexusquant startegy ...` maps to `nexusquant strategy ...`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.1"
|
|
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import typer
|
|
4
4
|
|
|
5
5
|
from nexusquant_cli.commands.auth.cmd import register as register_auth
|
|
6
|
+
from nexusquant_cli.commands.policy.cmd import register as register_policy
|
|
6
7
|
from nexusquant_cli.commands.strategy.cmd import register as register_strategy
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def register_all(app: typer.Typer) -> None:
|
|
10
11
|
register_auth(app)
|
|
12
|
+
register_policy(app)
|
|
11
13
|
register_strategy(app)
|
|
File without changes
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""`nexusquant policy …` —— SizingPolicy: publish a formula instead of a number.
|
|
2
|
+
|
|
3
|
+
All validation lives in ``nexusquant_sdk._policy`` and is called from here, never
|
|
4
|
+
re-implemented. A second copy of the rules would eventually drift from the server,
|
|
5
|
+
and a client check that disagrees with the server is worse than no check at all —
|
|
6
|
+
it hands you false confidence. (The backend is always the authority; these commands
|
|
7
|
+
only move the obvious mistakes onto your machine.)
|
|
8
|
+
|
|
9
|
+
What you submit is two things::
|
|
10
|
+
|
|
11
|
+
{"expr": {"buy.quantity": "...", "buy.price": "..."}, "params": ["a", "b"]}
|
|
12
|
+
|
|
13
|
+
``expr`` is the formula, ``params`` are the names it needs you to send with every
|
|
14
|
+
signal. Run ``nexusquant policy features`` first — that endpoint is the authority on
|
|
15
|
+
which names exist.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Annotated, Any
|
|
23
|
+
|
|
24
|
+
import typer
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
|
|
28
|
+
from nexusquant_cli.commands._util import load_json_file
|
|
29
|
+
from nexusquant_sdk import (
|
|
30
|
+
SignalValidationError,
|
|
31
|
+
parse_param_args,
|
|
32
|
+
policy_features,
|
|
33
|
+
policy_list,
|
|
34
|
+
policy_publish,
|
|
35
|
+
policy_send_signal,
|
|
36
|
+
policy_set_mode,
|
|
37
|
+
policy_spec_for,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
console = Console()
|
|
41
|
+
|
|
42
|
+
policy_app = typer.Typer(
|
|
43
|
+
help=(
|
|
44
|
+
"SizingPolicy: submit a formula that decides order quantity (and optionally "
|
|
45
|
+
"limit price) instead of sending a fixed number. Requires "
|
|
46
|
+
"custom:userType=strategyProvider or admin."
|
|
47
|
+
),
|
|
48
|
+
no_args_is_help=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _err(e: Exception) -> None:
|
|
53
|
+
console.print(f"[red]{e}[/red]")
|
|
54
|
+
raise typer.Exit(1) from e
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _print_gate(gate: dict[str, Any]) -> None:
|
|
58
|
+
"""Print every gate step. When a publish is rejected, which step and why is the
|
|
59
|
+
only useful information, so it is never collapsed into a single line."""
|
|
60
|
+
table = Table(show_header=True, header_style="dim")
|
|
61
|
+
table.add_column("gate", style="bold")
|
|
62
|
+
table.add_column("")
|
|
63
|
+
table.add_column("detail", overflow="fold")
|
|
64
|
+
for step in gate.get("steps") or []:
|
|
65
|
+
table.add_row(
|
|
66
|
+
str(step.get("name")),
|
|
67
|
+
"[green]pass[/green]" if step.get("ok") else "[red]fail[/red]",
|
|
68
|
+
str(step.get("detail") or ""),
|
|
69
|
+
)
|
|
70
|
+
console.print(table)
|
|
71
|
+
for warning in gate.get("warnings") or []:
|
|
72
|
+
console.print(f"[yellow]⚠ {warning}[/yellow]")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@policy_app.command(
|
|
76
|
+
"features",
|
|
77
|
+
help=(
|
|
78
|
+
"List the names a formula may use. Run this before writing one — the server "
|
|
79
|
+
"registry is the authority, and names outside it cannot be published."
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
def features_cmd(
|
|
83
|
+
scope: Annotated[
|
|
84
|
+
str | None,
|
|
85
|
+
typer.Option("--scope", help="Filter: 'shared' (you send them) or 'account' (bound in the user's container)"),
|
|
86
|
+
] = None,
|
|
87
|
+
as_json: Annotated[bool, typer.Option("--json", help="Raw output for scripting")] = False,
|
|
88
|
+
) -> None:
|
|
89
|
+
try:
|
|
90
|
+
resp = policy_features()
|
|
91
|
+
except Exception as e: # noqa: BLE001
|
|
92
|
+
_err(e)
|
|
93
|
+
data = (resp or {}).get("data") or {}
|
|
94
|
+
rows = data.get("features") or []
|
|
95
|
+
if not rows:
|
|
96
|
+
console.print("[dim]Server returned no features.[/dim]")
|
|
97
|
+
return
|
|
98
|
+
if as_json:
|
|
99
|
+
console.print_json(data=data)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
want = (scope or "").strip().lower()
|
|
103
|
+
if want in ("account", "per_user"):
|
|
104
|
+
rows = [r for r in rows if r.get("scope") == "per_user"]
|
|
105
|
+
elif want == "shared":
|
|
106
|
+
rows = [r for r in rows if r.get("scope") == "shared"]
|
|
107
|
+
elif want:
|
|
108
|
+
_err(RuntimeError("--scope must be 'shared' or 'account'"))
|
|
109
|
+
|
|
110
|
+
table = Table(show_header=True, header_style="dim")
|
|
111
|
+
table.add_column("name", style="bold", overflow="fold")
|
|
112
|
+
table.add_column("label", overflow="fold")
|
|
113
|
+
table.add_column("supplied by")
|
|
114
|
+
table.add_column("what it is", overflow="fold")
|
|
115
|
+
for r in sorted(rows, key=lambda x: (x.get("scope") != "shared", x.get("name") or "")):
|
|
116
|
+
per_user = r.get("scope") == "per_user"
|
|
117
|
+
# A registered-but-unsuppliable name is flagged in red here: it can be written
|
|
118
|
+
# into a formula, but publishing will reject it — and that rejection lands
|
|
119
|
+
# after you have already written the whole thing.
|
|
120
|
+
if per_user and not r.get("suppliable", True):
|
|
121
|
+
who = "[red]unavailable[/red]"
|
|
122
|
+
elif per_user:
|
|
123
|
+
who = "[cyan]user's container[/cyan]"
|
|
124
|
+
else:
|
|
125
|
+
who = "you, per signal"
|
|
126
|
+
table.add_row(
|
|
127
|
+
str(r.get("name")), str(r.get("label") or ""), who,
|
|
128
|
+
str(r.get("definition") or ""),
|
|
129
|
+
)
|
|
130
|
+
console.print(table)
|
|
131
|
+
if data.get("guidance"):
|
|
132
|
+
console.print(f"\n[yellow]{data['guidance']}[/yellow]")
|
|
133
|
+
console.print(
|
|
134
|
+
"\n[dim]Names marked 'you, per signal' go into your artifact's params. "
|
|
135
|
+
"Names bound in the user's container must NOT — reference them directly.[/dim]"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@policy_app.command(
|
|
140
|
+
"publish",
|
|
141
|
+
help=(
|
|
142
|
+
"Publish a policy. This call IS the go-live action — there is no shadow or "
|
|
143
|
+
"canary step, and the previous version on the same slot is deactivated."
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
def publish_cmd(
|
|
147
|
+
strategy_id: Annotated[str, typer.Option("--strategy-id", help="A strategy you own")],
|
|
148
|
+
ticker: Annotated[str, typer.Option("--ticker", help="Which symbol this formula is for")],
|
|
149
|
+
file: Annotated[Path, typer.Option("--file", help='JSON: {"expr": {...}, "params": [...]}')],
|
|
150
|
+
profile: Annotated[str | None, typer.Option("--profile", help="Tier; defaults to 'normal'")] = None,
|
|
151
|
+
skip_name_check: Annotated[
|
|
152
|
+
bool,
|
|
153
|
+
typer.Option("--skip-name-check", help="Do not fetch the registry before validating locally"),
|
|
154
|
+
] = False,
|
|
155
|
+
) -> None:
|
|
156
|
+
raw = load_json_file(file, label="--file")
|
|
157
|
+
if not isinstance(raw, dict):
|
|
158
|
+
_err(RuntimeError("--file must contain a JSON object"))
|
|
159
|
+
try:
|
|
160
|
+
resp = policy_publish(
|
|
161
|
+
strategy_id,
|
|
162
|
+
ticker=ticker,
|
|
163
|
+
expr=raw.get("expr") or {},
|
|
164
|
+
params=raw.get("params") or [],
|
|
165
|
+
profile=profile,
|
|
166
|
+
check_names=not skip_name_check,
|
|
167
|
+
)
|
|
168
|
+
except SignalValidationError as e:
|
|
169
|
+
_err(e)
|
|
170
|
+
except Exception as e: # noqa: BLE001
|
|
171
|
+
_err(e)
|
|
172
|
+
data = (resp or {}).get("data") or {}
|
|
173
|
+
if data.get("gate"):
|
|
174
|
+
_print_gate(data["gate"])
|
|
175
|
+
console.print(f"[green]Published and live[/green] {data.get('policy_id')}")
|
|
176
|
+
if data.get("slots"):
|
|
177
|
+
console.print(f"Output slots: {', '.join(data['slots'])}")
|
|
178
|
+
if data.get("params"):
|
|
179
|
+
console.print(
|
|
180
|
+
f"Send with every signal: {', '.join(data['params'])}"
|
|
181
|
+
" [dim]miss one and the signal is rejected[/dim]"
|
|
182
|
+
)
|
|
183
|
+
if data.get("account_features"):
|
|
184
|
+
console.print(
|
|
185
|
+
f"[cyan]Reads user holdings: {', '.join(data['account_features'])}[/cyan]"
|
|
186
|
+
" [dim]values are bound in the user's container; you never see them[/dim]"
|
|
187
|
+
)
|
|
188
|
+
if data.get("superseded"):
|
|
189
|
+
console.print(f"Deactivated on the same slot: {', '.join(data['superseded'])}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@policy_app.command("list", help="List published policies for one strategy.")
|
|
193
|
+
def list_cmd(
|
|
194
|
+
strategy_id: Annotated[str, typer.Argument(help="Strategy ID")],
|
|
195
|
+
ticker: Annotated[str | None, typer.Option("--ticker")] = None,
|
|
196
|
+
mode: Annotated[str | None, typer.Option("--mode", help="active / off")] = None,
|
|
197
|
+
) -> None:
|
|
198
|
+
try:
|
|
199
|
+
resp = policy_list(strategy_id, ticker=ticker, mode=mode)
|
|
200
|
+
except Exception as e: # noqa: BLE001
|
|
201
|
+
_err(e)
|
|
202
|
+
rows = ((resp or {}).get("data") or {}).get("policies") or []
|
|
203
|
+
if not rows:
|
|
204
|
+
console.print("[dim]No published policies.[/dim]")
|
|
205
|
+
return
|
|
206
|
+
table = Table(show_header=True, header_style="dim")
|
|
207
|
+
for col in ("policy_id", "ticker", "mode", "output slots", "params", "reads holdings"):
|
|
208
|
+
table.add_column(col, overflow="fold")
|
|
209
|
+
for r in rows:
|
|
210
|
+
art = r.get("artifact") or {}
|
|
211
|
+
slots = r.get("slots") or [k for k in (art.get("expr") or {}) if "." in k]
|
|
212
|
+
params = r.get("params") or art.get("params") or []
|
|
213
|
+
acct = r.get("account_features") or []
|
|
214
|
+
table.add_row(
|
|
215
|
+
str(r.get("policy_id")), str(r.get("ticker")),
|
|
216
|
+
("[green]active[/green]" if r.get("mode") == "active" else str(r.get("mode"))),
|
|
217
|
+
", ".join(sorted(slots)), ", ".join(sorted(params)),
|
|
218
|
+
("[cyan]" + ", ".join(sorted(acct)) + "[/cyan]") if acct else "",
|
|
219
|
+
)
|
|
220
|
+
console.print(table)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@policy_app.command(
|
|
224
|
+
"set-mode",
|
|
225
|
+
help="Deactivate or restore a policy (active ↔ off). Publishing already goes live, so this is the emergency stop, not a promotion step.",
|
|
226
|
+
)
|
|
227
|
+
def set_mode_cmd(
|
|
228
|
+
policy_id: Annotated[str, typer.Option("--policy-id")],
|
|
229
|
+
mode: Annotated[str, typer.Option("--mode", help="active or off")],
|
|
230
|
+
) -> None:
|
|
231
|
+
try:
|
|
232
|
+
resp = policy_set_mode(policy_id, mode)
|
|
233
|
+
except ValueError as e:
|
|
234
|
+
_err(e)
|
|
235
|
+
except Exception as e: # noqa: BLE001
|
|
236
|
+
_err(e)
|
|
237
|
+
data = (resp or {}).get("data") or {}
|
|
238
|
+
console.print(f"[green]{data.get('previous_mode')} → {data.get('mode')}[/green] {policy_id}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@policy_app.command(
|
|
242
|
+
"send-signal",
|
|
243
|
+
help=(
|
|
244
|
+
"Send a signal carrying the policy's parameter values (--param name=value, "
|
|
245
|
+
"repeatable). Every declared parameter must be present or the signal is rejected."
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
def send_signal_cmd(
|
|
249
|
+
strategy_id: Annotated[str, typer.Option("--strategy-id")],
|
|
250
|
+
ticker: Annotated[str, typer.Option("--ticker")],
|
|
251
|
+
price: Annotated[float, typer.Option("--price")],
|
|
252
|
+
direction: Annotated[str, typer.Option("--direction", help="buy or sell")],
|
|
253
|
+
strategy_name: Annotated[str | None, typer.Option("--strategy-name")] = None,
|
|
254
|
+
param: Annotated[
|
|
255
|
+
list[str] | None,
|
|
256
|
+
typer.Option("--param", "-p", help="Parameter value, e.g. depth=3; repeat per declared name"),
|
|
257
|
+
] = None,
|
|
258
|
+
params_file: Annotated[
|
|
259
|
+
Path | None, typer.Option("--params-file", help="Or a JSON object with all parameter values")
|
|
260
|
+
] = None,
|
|
261
|
+
quantity: Annotated[
|
|
262
|
+
int | None,
|
|
263
|
+
typer.Option("--quantity", help="Used when the formula has no slot for this direction"),
|
|
264
|
+
] = None,
|
|
265
|
+
order_type: Annotated[str | None, typer.Option("--order-type", help="MARKET / LIMIT")] = None,
|
|
266
|
+
policy_id: Annotated[
|
|
267
|
+
str | None, typer.Option("--policy-id", help="Defaults to the active policy on this (strategy, ticker)")
|
|
268
|
+
] = None,
|
|
269
|
+
no_fetch: Annotated[
|
|
270
|
+
bool,
|
|
271
|
+
typer.Option("--no-fetch", help="Skip fetching the policy; only the 'no account state' rule is then checked"),
|
|
272
|
+
] = False,
|
|
273
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", help="Validate and print the payload without sending")] = False,
|
|
274
|
+
) -> None:
|
|
275
|
+
supplied: dict[str, Any] = {}
|
|
276
|
+
if params_file:
|
|
277
|
+
loaded = load_json_file(params_file, label="--params-file")
|
|
278
|
+
if not isinstance(loaded, dict):
|
|
279
|
+
_err(RuntimeError("--params-file must contain a JSON object"))
|
|
280
|
+
supplied.update(loaded)
|
|
281
|
+
try:
|
|
282
|
+
supplied.update(parse_param_args(param or []))
|
|
283
|
+
except SignalValidationError as e:
|
|
284
|
+
_err(e)
|
|
285
|
+
|
|
286
|
+
spec = None
|
|
287
|
+
if not no_fetch:
|
|
288
|
+
try:
|
|
289
|
+
spec = policy_spec_for(strategy_id, ticker=ticker, policy_id=policy_id)
|
|
290
|
+
except SignalValidationError as e:
|
|
291
|
+
_err(e)
|
|
292
|
+
except Exception as e: # noqa: BLE001
|
|
293
|
+
_err(e)
|
|
294
|
+
|
|
295
|
+
if dry_run:
|
|
296
|
+
# Build without sending, so the same validation runs and you see the exact body.
|
|
297
|
+
from nexusquant_sdk import _policy
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
body = _policy.build_signal_payload(
|
|
301
|
+
strategy_id=strategy_id,
|
|
302
|
+
strategy_name=strategy_name or strategy_id,
|
|
303
|
+
ticker=ticker, price=price, direction=direction,
|
|
304
|
+
params=supplied, spec=spec, quantity=quantity, order_type=order_type,
|
|
305
|
+
)
|
|
306
|
+
except SignalValidationError as e:
|
|
307
|
+
_err(e)
|
|
308
|
+
console.print("[dim]--dry-run: validated, not sent[/dim]")
|
|
309
|
+
console.print_json(data=body)
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
resp = policy_send_signal(
|
|
314
|
+
strategy_id, strategy_name=strategy_name, ticker=ticker, price=price,
|
|
315
|
+
direction=direction, params=supplied, quantity=quantity,
|
|
316
|
+
order_type=order_type, spec=spec, fetch_spec=False,
|
|
317
|
+
)
|
|
318
|
+
except SignalValidationError as e:
|
|
319
|
+
_err(e)
|
|
320
|
+
except Exception as e: # noqa: BLE001
|
|
321
|
+
_err(e)
|
|
322
|
+
console.print("[green]Sent[/green]")
|
|
323
|
+
console.print_json(data=resp)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def register(app: typer.Typer) -> None:
|
|
327
|
+
app.add_typer(policy_app, name="policy")
|
|
@@ -4,12 +4,15 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nexusquant-cli"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "NexusQuant strategy provider CLI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"httpx>=0.27",
|
|
13
|
+
# policy 命令的全部校验都在 SDK 里,这里只调不抄 —— 抄一份就会与服务端分叉,
|
|
14
|
+
# 而分叉的客户端校验比没有更糟(它给的是假信心)。
|
|
15
|
+
"nexusquant-sdk>=0.2",
|
|
13
16
|
"platformdirs>=4.2",
|
|
14
17
|
"rich>=13.7",
|
|
15
18
|
"typer>=0.12",
|
nexusquant_cli-0.1.2/PKG-INFO
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: nexusquant-cli
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Summary: NexusQuant strategy provider CLI
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: httpx>=0.27
|
|
7
|
-
Requires-Dist: platformdirs>=4.2
|
|
8
|
-
Requires-Dist: rich>=13.7
|
|
9
|
-
Requires-Dist: typer>=0.12
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
|
|
12
|
-
# nexusquant-cli
|
|
13
|
-
|
|
14
|
-
Command line tool for NexusQuant strategy providers.
|
|
15
|
-
|
|
16
|
-
It supports browser login through Cognito, stores tokens locally, refreshes tokens, and calls the Nexus strategy provider APIs. Provider commands require a Cognito user with `custom:userType=strategyProvider` or `custom:userType=admin`.
|
|
17
|
-
|
|
18
|
-
## Install
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
python3 -m pip install -e .
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
pip install nexusquant-cli
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Login
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
nexusquant auth
|
|
32
|
-
nexusquant auth --status
|
|
33
|
-
nexusquant auth --refresh
|
|
34
|
-
nexusquant auth --logout
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Tokens are stored in the current OS user's config directory, for example `~/Library/Application Support/nexusquant-cli/credentials.json` on macOS. The file is written with `0600` permissions when supported.
|
|
38
|
-
|
|
39
|
-
## Strategy Commands
|
|
40
|
-
|
|
41
|
-
Create or update a strategy:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
nexusquant strategy create \
|
|
45
|
-
--strategy-id my_alpha_001 \
|
|
46
|
-
--name "My Alpha" \
|
|
47
|
-
--schema-file schema.json \
|
|
48
|
-
--output-unit SHARE_COUNT
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
List strategies registered by the current provider. Admin users see all strategies:
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
nexusquant strategy list
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
List signal history for one strategy:
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
nexusquant strategy signal my_alpha_001 --history --limit 20
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Send a single signal:
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
nexusquant strategy signal my_alpha_001 \
|
|
67
|
-
--strategy-name "My Alpha" \
|
|
68
|
-
--ticker AAPL \
|
|
69
|
-
--direction buy \
|
|
70
|
-
--price 150.25 \
|
|
71
|
-
--quantity 100 \
|
|
72
|
-
--order-type MARKET
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
`--order-type` is a signal-level setting: `MARKET` or `LIMIT` (limit uses `--price`). Legacy `NORMAL` is accepted by the CLI as an alias for `LIMIT`. If omitted, the API defaults to `MARKET`.
|
|
76
|
-
|
|
77
|
-
Send a multi-route `signals` map:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
nexusquant strategy signal my_alpha_001 \
|
|
81
|
-
--strategy-name "My Alpha" \
|
|
82
|
-
--signals-file signals.json
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Get non-PII subscriber config for a strategy:
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
nexusquant strategy sub config my_alpha_001
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## JSON Inputs
|
|
92
|
-
|
|
93
|
-
`--schema-file` must contain the `strategy_schema` object accepted by `nexus-service`, for example:
|
|
94
|
-
|
|
95
|
-
```json
|
|
96
|
-
{
|
|
97
|
-
"type": "object",
|
|
98
|
-
"properties": {
|
|
99
|
-
"window": {
|
|
100
|
-
"type": "integer",
|
|
101
|
-
"default": 14,
|
|
102
|
-
"title": "Window",
|
|
103
|
-
"source": "user"
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
"required": []
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
`--signals-file` must contain a JSON object whose keys are `default` or user ids:
|
|
111
|
-
|
|
112
|
-
```json
|
|
113
|
-
{
|
|
114
|
-
"default": {
|
|
115
|
-
"ticker": "AAPL",
|
|
116
|
-
"time": "2026-04-26T19:30:00Z",
|
|
117
|
-
"price": 150.25,
|
|
118
|
-
"direction": "buy",
|
|
119
|
-
"order_type": "LIMIT",
|
|
120
|
-
"quantity": 100,
|
|
121
|
-
"metadata": {}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## Environment Overrides
|
|
127
|
-
|
|
128
|
-
Normal use does not require configuration. For staging or local development:
|
|
129
|
-
|
|
130
|
-
| Variable | Meaning |
|
|
131
|
-
| ------------------------------ | ------------------------------------------------------------------------------------- |
|
|
132
|
-
| `NEXUSQUANT_API_ENDPOINT` | API endpoint host, default `https://api.nexusquant.co`; CLI calls `/api/...` under it |
|
|
133
|
-
| `NEXUSQUANT_API_BASE_URL` | Optional full API base override, e.g. `https://api.nexusquant.co/api` |
|
|
134
|
-
| `NEXUSQUANT_COGNITO_DOMAIN` | Cognito Hosted UI host, default `auth.lookatwallstreet.com` |
|
|
135
|
-
| `NEXUSQUANT_COGNITO_CLIENT_ID` | Cognito app client id |
|
|
136
|
-
| `NEXUSQUANT_REDIRECT_URI` | OAuth callback, default `http://127.0.0.1:8251/callback` |
|
|
137
|
-
|
|
138
|
-
There is also a hidden typo-compatible alias: `nexusquant startegy ...` maps to `nexusquant strategy ...`.
|
nexusquant_cli-0.1.2/README.md
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
# nexusquant-cli
|
|
2
|
-
|
|
3
|
-
Command line tool for NexusQuant strategy providers.
|
|
4
|
-
|
|
5
|
-
It supports browser login through Cognito, stores tokens locally, refreshes tokens, and calls the Nexus strategy provider APIs. Provider commands require a Cognito user with `custom:userType=strategyProvider` or `custom:userType=admin`.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
python3 -m pip install -e .
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
pip install nexusquant-cli
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Login
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
nexusquant auth
|
|
21
|
-
nexusquant auth --status
|
|
22
|
-
nexusquant auth --refresh
|
|
23
|
-
nexusquant auth --logout
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Tokens are stored in the current OS user's config directory, for example `~/Library/Application Support/nexusquant-cli/credentials.json` on macOS. The file is written with `0600` permissions when supported.
|
|
27
|
-
|
|
28
|
-
## Strategy Commands
|
|
29
|
-
|
|
30
|
-
Create or update a strategy:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
nexusquant strategy create \
|
|
34
|
-
--strategy-id my_alpha_001 \
|
|
35
|
-
--name "My Alpha" \
|
|
36
|
-
--schema-file schema.json \
|
|
37
|
-
--output-unit SHARE_COUNT
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
List strategies registered by the current provider. Admin users see all strategies:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
nexusquant strategy list
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
List signal history for one strategy:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
nexusquant strategy signal my_alpha_001 --history --limit 20
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Send a single signal:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
nexusquant strategy signal my_alpha_001 \
|
|
56
|
-
--strategy-name "My Alpha" \
|
|
57
|
-
--ticker AAPL \
|
|
58
|
-
--direction buy \
|
|
59
|
-
--price 150.25 \
|
|
60
|
-
--quantity 100 \
|
|
61
|
-
--order-type MARKET
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
`--order-type` is a signal-level setting: `MARKET` or `LIMIT` (limit uses `--price`). Legacy `NORMAL` is accepted by the CLI as an alias for `LIMIT`. If omitted, the API defaults to `MARKET`.
|
|
65
|
-
|
|
66
|
-
Send a multi-route `signals` map:
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
nexusquant strategy signal my_alpha_001 \
|
|
70
|
-
--strategy-name "My Alpha" \
|
|
71
|
-
--signals-file signals.json
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Get non-PII subscriber config for a strategy:
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
nexusquant strategy sub config my_alpha_001
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## JSON Inputs
|
|
81
|
-
|
|
82
|
-
`--schema-file` must contain the `strategy_schema` object accepted by `nexus-service`, for example:
|
|
83
|
-
|
|
84
|
-
```json
|
|
85
|
-
{
|
|
86
|
-
"type": "object",
|
|
87
|
-
"properties": {
|
|
88
|
-
"window": {
|
|
89
|
-
"type": "integer",
|
|
90
|
-
"default": 14,
|
|
91
|
-
"title": "Window",
|
|
92
|
-
"source": "user"
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
"required": []
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
`--signals-file` must contain a JSON object whose keys are `default` or user ids:
|
|
100
|
-
|
|
101
|
-
```json
|
|
102
|
-
{
|
|
103
|
-
"default": {
|
|
104
|
-
"ticker": "AAPL",
|
|
105
|
-
"time": "2026-04-26T19:30:00Z",
|
|
106
|
-
"price": 150.25,
|
|
107
|
-
"direction": "buy",
|
|
108
|
-
"order_type": "LIMIT",
|
|
109
|
-
"quantity": 100,
|
|
110
|
-
"metadata": {}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Environment Overrides
|
|
116
|
-
|
|
117
|
-
Normal use does not require configuration. For staging or local development:
|
|
118
|
-
|
|
119
|
-
| Variable | Meaning |
|
|
120
|
-
| ------------------------------ | ------------------------------------------------------------------------------------- |
|
|
121
|
-
| `NEXUSQUANT_API_ENDPOINT` | API endpoint host, default `https://api.nexusquant.co`; CLI calls `/api/...` under it |
|
|
122
|
-
| `NEXUSQUANT_API_BASE_URL` | Optional full API base override, e.g. `https://api.nexusquant.co/api` |
|
|
123
|
-
| `NEXUSQUANT_COGNITO_DOMAIN` | Cognito Hosted UI host, default `auth.lookatwallstreet.com` |
|
|
124
|
-
| `NEXUSQUANT_COGNITO_CLIENT_ID` | Cognito app client id |
|
|
125
|
-
| `NEXUSQUANT_REDIRECT_URI` | OAuth callback, default `http://127.0.0.1:8251/callback` |
|
|
126
|
-
|
|
127
|
-
There is also a hidden typo-compatible alias: `nexusquant startegy ...` maps to `nexusquant strategy ...`.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|