alphaengine 0.9.0__tar.gz → 0.9.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.
- {alphaengine-0.9.0 → alphaengine-0.9.1}/PKG-INFO +1 -1
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/_version.py +5 -1
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/risk.py +6 -1
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/model.py +6 -7
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/tui/app.py +181 -17
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/tui/dispatch.py +49 -10
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_tui.py +92 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/.github/workflows/ci.yml +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/.github/workflows/publish.yml +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/.gitignore +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/AGENTS.md +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/LICENSE +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/README.md +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/SECURITY.md +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/docs/assets/banner.png +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/docs/assets/data_boundary.png +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/docs/assets/session.png +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/docs/figure_contract.md +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/docs/workflows/evaluate_process.json +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/docs/workflows/research_week.json +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/docs/workflows/stress_study.json +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/pyproject.toml +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/scripts/gen_docs.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/__init__.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/__main__.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/agent/__init__.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/agent/answer.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/agent/driver.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/auth.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/book.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/charts.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/cli.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/client/__init__.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/client/agent.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/client/executor.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/client/session.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/commands.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/complete.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/connectors/__init__.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/__init__.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/active.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/allocate.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/backtest.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/covariance.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/cross_section.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/factors.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/pairs.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/panel.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/performance.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/process.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/profile.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/screen.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/series_shapes.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/signals.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/stress.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/technical.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/trace.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/validation.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/core/walkforward.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/demo.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/demo_book.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/demo_returns.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/demo_signal.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/demo_universe.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/events.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/loaders.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/py.typed +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/repl.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/study/__init__.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/study/report.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/study/schema.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/sweep/__init__.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/sweep/runner.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/src/alphaengine/tui/__init__.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_agent.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_answer.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_book.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_cli.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_client.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_commands.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_events.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_executor_ops.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_goldens.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_loaders.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_math_audit.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_model.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_process.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_quant_maths.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_screen.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_signals.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_smoke.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_sweep.py +0 -0
- {alphaengine-0.9.0 → alphaengine-0.9.1}/tests/test_trace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: alphaengine
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Validated research tooling for investment strategies: deflation, overfitting detection, and honest trial counts.
|
|
5
5
|
Project-URL: Homepage, https://github.com/quantOSC/alphaengine
|
|
6
6
|
Project-URL: Documentation, https://github.com/quantOSC/alphaengine#readme
|
|
@@ -76,4 +76,8 @@ is what a changed figure costs. The API may still move underneath it.
|
|
|
76
76
|
# Close fills are unchanged. No golden in tests/test_goldens.py moves. The
|
|
77
77
|
# same release makes the full-screen terminal the only session, and posts each
|
|
78
78
|
# math line's series on `/traces` without putting those series inside figures.
|
|
79
|
-
|
|
79
|
+
#
|
|
80
|
+
# 0.9.1 IS THE SESSION, NOT THE MATHS. No computed value moved. A signed-in
|
|
81
|
+
# account loads its one stored universe from the portal. `enter model key`
|
|
82
|
+
# takes whichever provider key is pasted. The left pane lists the commands.
|
|
83
|
+
__version__ = "0.9.1"
|
|
@@ -129,7 +129,12 @@ def compute_var_cvar(
|
|
|
129
129
|
record(
|
|
130
130
|
"parametric_var",
|
|
131
131
|
"z * std(r, ddof=1) * sqrt(horizon), z = normsinv(confidence), as a positive percent",
|
|
132
|
-
inputs={
|
|
132
|
+
inputs={
|
|
133
|
+
"n_obs": int(n_obs),
|
|
134
|
+
"confidence": confidence,
|
|
135
|
+
"horizon_days": int(horizon_days),
|
|
136
|
+
"z": round(z, 6),
|
|
137
|
+
},
|
|
133
138
|
series={"returns": arr},
|
|
134
139
|
result=result["parametric"]["var_pct"],
|
|
135
140
|
)
|
|
@@ -328,13 +328,12 @@ def build_think(
|
|
|
328
328
|
)
|
|
329
329
|
|
|
330
330
|
raise NoModelConfigured(
|
|
331
|
-
"No model key found.
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
"
|
|
335
|
-
"
|
|
336
|
-
"
|
|
337
|
-
"no model at all."
|
|
331
|
+
"No model key found. In the session, type `enter model key` and paste it.\n"
|
|
332
|
+
"Or set the key in this shell: ANTHROPIC_API_KEY, OPENAI_API_KEY, "
|
|
333
|
+
"GEMINI_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY, "
|
|
334
|
+
"AZURE_OPENAI_API_KEY, or ALPHAENGINE_API_KEY with ALPHAENGINE_BASE_URL.\n"
|
|
335
|
+
"Nothing here sends that key to QuantOS, and the scripted path "
|
|
336
|
+
"(`run <workflow>`) needs no model at all."
|
|
338
337
|
)
|
|
339
338
|
|
|
340
339
|
|
|
@@ -16,6 +16,28 @@ from .dispatch import Desk, submit
|
|
|
16
16
|
|
|
17
17
|
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|
18
18
|
|
|
19
|
+
_COMMANDS = (
|
|
20
|
+
"login",
|
|
21
|
+
"enter model key",
|
|
22
|
+
"load <name>",
|
|
23
|
+
"screen",
|
|
24
|
+
"diagnose",
|
|
25
|
+
"signal",
|
|
26
|
+
"validate",
|
|
27
|
+
"stress",
|
|
28
|
+
"overlap",
|
|
29
|
+
"size",
|
|
30
|
+
"monitor",
|
|
31
|
+
"run <name>",
|
|
32
|
+
"workflows",
|
|
33
|
+
"help",
|
|
34
|
+
"quit",
|
|
35
|
+
)
|
|
36
|
+
_URL_AFTER = {
|
|
37
|
+
"AZURE_OPENAI_API_KEY": "AZURE_OPENAI_ENDPOINT",
|
|
38
|
+
"ALPHAENGINE_API_KEY": "ALPHAENGINE_BASE_URL",
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
_CSS = """
|
|
20
42
|
Screen { background: #0b0d10; }
|
|
21
43
|
#status {
|
|
@@ -27,12 +49,12 @@ Screen { background: #0b0d10; }
|
|
|
27
49
|
}
|
|
28
50
|
#body { height: 1fr; }
|
|
29
51
|
#rail {
|
|
30
|
-
width:
|
|
31
|
-
min-width:
|
|
52
|
+
width: 28;
|
|
53
|
+
min-width: 28;
|
|
32
54
|
background: #101318;
|
|
33
55
|
color: #c5cdd6;
|
|
34
56
|
padding: 1 1;
|
|
35
|
-
overflow:
|
|
57
|
+
overflow: auto;
|
|
36
58
|
}
|
|
37
59
|
#transcript {
|
|
38
60
|
width: 1fr;
|
|
@@ -106,6 +128,7 @@ class QuantOSApp(App[None]):
|
|
|
106
128
|
keyed=keyed,
|
|
107
129
|
)
|
|
108
130
|
self._universes: list[str] = []
|
|
131
|
+
self._stored: set[str] = set()
|
|
109
132
|
self._lines: list[dict[str, Any]] = []
|
|
110
133
|
self.busy = False
|
|
111
134
|
self.rail_text = ""
|
|
@@ -133,18 +156,11 @@ class QuantOSApp(App[None]):
|
|
|
133
156
|
log = self.query_one("#transcript", RichLog)
|
|
134
157
|
log.write("Ask in plain English.")
|
|
135
158
|
log.write("A sentence picks a workflow. run <name> follows it exactly.")
|
|
159
|
+
log.write("login signs you in. enter model key pastes whichever key you use.")
|
|
136
160
|
log.write("")
|
|
137
161
|
log.write("help lists what you can type. quit leaves.")
|
|
138
162
|
self.query_one(Input).focus()
|
|
139
|
-
|
|
140
|
-
rows = self.desk.session.universes()
|
|
141
|
-
except Exception: # noqa: BLE001 — the ladder still stands without the list
|
|
142
|
-
rows = []
|
|
143
|
-
self._universes = [
|
|
144
|
-
str(row.get("name") or row.get("id") or "") for row in rows if isinstance(row, dict)
|
|
145
|
-
]
|
|
146
|
-
self._universes = [name for name in self._universes if name]
|
|
147
|
-
self._refresh_rail()
|
|
163
|
+
self._sync_account()
|
|
148
164
|
|
|
149
165
|
def _refresh_rail(self) -> None:
|
|
150
166
|
from ..model import available_models, keys_without_sdk
|
|
@@ -158,7 +174,7 @@ class QuantOSApp(App[None]):
|
|
|
158
174
|
elif keyed and stranded:
|
|
159
175
|
ask_note = "sdk missing"
|
|
160
176
|
elif keyed:
|
|
161
|
-
ask_note = "
|
|
177
|
+
ask_note = "enter model key"
|
|
162
178
|
else:
|
|
163
179
|
ask_note = "sign in first"
|
|
164
180
|
|
|
@@ -170,7 +186,8 @@ class QuantOSApp(App[None]):
|
|
|
170
186
|
else:
|
|
171
187
|
loaded = "nothing"
|
|
172
188
|
|
|
173
|
-
lines = [
|
|
189
|
+
lines = ["commands", *[f" {name}" for name in _COMMANDS], ""]
|
|
190
|
+
lines += [
|
|
174
191
|
f"{on} the maths",
|
|
175
192
|
" offline, always",
|
|
176
193
|
"",
|
|
@@ -186,11 +203,13 @@ class QuantOSApp(App[None]):
|
|
|
186
203
|
"universes",
|
|
187
204
|
]
|
|
188
205
|
if self._universes:
|
|
189
|
-
|
|
206
|
+
for name in self._universes[:12]:
|
|
207
|
+
mark = " stored" if name in self._stored else ""
|
|
208
|
+
lines.append(f" {_fit(name, 20 - len(mark))}{mark}")
|
|
190
209
|
else:
|
|
191
210
|
lines.append(" none in reach")
|
|
192
211
|
if not keyed:
|
|
193
|
-
lines += ["", "
|
|
212
|
+
lines += ["", "login", "signs you in"]
|
|
194
213
|
self.rail_text = "\n".join(lines)
|
|
195
214
|
self.query_one("#rail", Static).update(self.rail_text)
|
|
196
215
|
|
|
@@ -213,6 +232,12 @@ class QuantOSApp(App[None]):
|
|
|
213
232
|
if self.desk.pending_secret:
|
|
214
233
|
self._accept_secret(text)
|
|
215
234
|
return
|
|
235
|
+
if self.desk.pending_provider:
|
|
236
|
+
self._accept_provider(text)
|
|
237
|
+
return
|
|
238
|
+
if self.desk.pending_url:
|
|
239
|
+
self._accept_url(text)
|
|
240
|
+
return
|
|
216
241
|
self.busy = True
|
|
217
242
|
self.query_one("#transcript", RichLog).write(f"> {text}")
|
|
218
243
|
|
|
@@ -231,6 +256,43 @@ class QuantOSApp(App[None]):
|
|
|
231
256
|
if env is None or not value:
|
|
232
257
|
log.write("nothing entered.")
|
|
233
258
|
return
|
|
259
|
+
if env == "MODEL_KEY":
|
|
260
|
+
self._take_model_key(value, log, prompt)
|
|
261
|
+
return
|
|
262
|
+
self._store_credential(env, value, log, prompt)
|
|
263
|
+
|
|
264
|
+
def _take_model_key(self, value: str, log: RichLog, prompt: Input) -> None:
|
|
265
|
+
from .dispatch import env_for_model_key
|
|
266
|
+
|
|
267
|
+
found = env_for_model_key(value)
|
|
268
|
+
if found is None:
|
|
269
|
+
self.desk.held_key = value
|
|
270
|
+
self.desk.pending_provider = True
|
|
271
|
+
prompt.password = False
|
|
272
|
+
prompt.placeholder = "anthropic, openai, gemini, groq, openrouter, azure, gateway"
|
|
273
|
+
log.write("Which provider is this key for?")
|
|
274
|
+
return
|
|
275
|
+
self._store_credential(found, value, log, prompt)
|
|
276
|
+
|
|
277
|
+
def _accept_provider(self, text: str) -> None:
|
|
278
|
+
from ..cli import _KEY_PROMPTS
|
|
279
|
+
|
|
280
|
+
which = text.strip().lower()
|
|
281
|
+
prompt = self.query_one(Input)
|
|
282
|
+
log = self.query_one("#transcript", RichLog)
|
|
283
|
+
if which not in _KEY_PROMPTS or which == "quantos":
|
|
284
|
+
log.write("Try anthropic, openai, gemini, groq, openrouter, azure, or gateway.")
|
|
285
|
+
return
|
|
286
|
+
value = self.desk.held_key or ""
|
|
287
|
+
self.desk.held_key = None
|
|
288
|
+
self.desk.pending_provider = False
|
|
289
|
+
prompt.placeholder = "Ask in plain English, or run <workflow>"
|
|
290
|
+
if not value:
|
|
291
|
+
log.write("nothing entered.")
|
|
292
|
+
return
|
|
293
|
+
self._store_credential(_KEY_PROMPTS[which][0], value, log, prompt)
|
|
294
|
+
|
|
295
|
+
def _store_credential(self, env: str, value: str, log: RichLog, prompt: Input) -> None:
|
|
234
296
|
os.environ[env] = value
|
|
235
297
|
if env == "QUANTOS_API_KEY":
|
|
236
298
|
from ..cli import _session
|
|
@@ -247,6 +309,96 @@ class QuantOSApp(App[None]):
|
|
|
247
309
|
log.write(f"{env} accepted. stored at {path}")
|
|
248
310
|
except (OSError, ValueError) as exc:
|
|
249
311
|
log.write(f"{env} accepted for this process. ({exc})")
|
|
312
|
+
follow = _URL_AFTER.get(env)
|
|
313
|
+
if follow and not os.environ.get(follow):
|
|
314
|
+
self.desk.pending_url = follow
|
|
315
|
+
prompt.placeholder = f"paste {follow}"
|
|
316
|
+
log.write(f"Now paste {follow}. It is not hidden.")
|
|
317
|
+
elif env != "QUANTOS_API_KEY":
|
|
318
|
+
self._say_model_ready(log)
|
|
319
|
+
if env == "QUANTOS_API_KEY":
|
|
320
|
+
self._sync_account()
|
|
321
|
+
else:
|
|
322
|
+
self._refresh_rail()
|
|
323
|
+
|
|
324
|
+
def _accept_url(self, value: str) -> None:
|
|
325
|
+
env = self.desk.pending_url
|
|
326
|
+
self.desk.pending_url = None
|
|
327
|
+
prompt = self.query_one(Input)
|
|
328
|
+
prompt.placeholder = "Ask in plain English, or run <workflow>"
|
|
329
|
+
log = self.query_one("#transcript", RichLog)
|
|
330
|
+
if env is None or not value:
|
|
331
|
+
log.write("nothing entered.")
|
|
332
|
+
return
|
|
333
|
+
os.environ[env] = value
|
|
334
|
+
try:
|
|
335
|
+
from ..auth import save_key
|
|
336
|
+
|
|
337
|
+
save_key(env, value)
|
|
338
|
+
except (OSError, ValueError):
|
|
339
|
+
pass
|
|
340
|
+
log.write(f"{env} set.")
|
|
341
|
+
self._say_model_ready(log)
|
|
342
|
+
self._refresh_rail()
|
|
343
|
+
|
|
344
|
+
def _say_model_ready(self, log: RichLog) -> None:
|
|
345
|
+
from ..model import _SDK, available_models, keys_without_sdk
|
|
346
|
+
|
|
347
|
+
stranded = keys_without_sdk()
|
|
348
|
+
ready = available_models()
|
|
349
|
+
if stranded:
|
|
350
|
+
installs = " or ".join(f"pip install {_SDK[label]}" for label in stranded)
|
|
351
|
+
log.write(f"Key stored. The SDK is not installed yet: {installs}")
|
|
352
|
+
return
|
|
353
|
+
if ready:
|
|
354
|
+
label, model = ready[0]
|
|
355
|
+
log.write(f"Model ready: {label}/{model}. Ask in plain English.")
|
|
356
|
+
|
|
357
|
+
def _sync_account(self) -> None:
|
|
358
|
+
"""Refresh portal universes. One stored book loads itself."""
|
|
359
|
+
from ..cli import _narrow_to_universe, _stored_universes, narration_to
|
|
360
|
+
|
|
361
|
+
notes: list[str] = []
|
|
362
|
+
|
|
363
|
+
def sink(chunk: str) -> None:
|
|
364
|
+
notes.append(chunk)
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
rows = self.desk.session.universes()
|
|
368
|
+
except Exception: # noqa: BLE001 — offline or unsigned still boots
|
|
369
|
+
rows = []
|
|
370
|
+
self._stored = set()
|
|
371
|
+
names: list[str] = []
|
|
372
|
+
for row in rows:
|
|
373
|
+
if not isinstance(row, dict):
|
|
374
|
+
continue
|
|
375
|
+
name = str(row.get("name") or row.get("id") or "")
|
|
376
|
+
if not name:
|
|
377
|
+
continue
|
|
378
|
+
names.append(name)
|
|
379
|
+
if (row.get("definition") or {}).get("cache_id"):
|
|
380
|
+
self._stored.add(name)
|
|
381
|
+
self._universes = names
|
|
382
|
+
|
|
383
|
+
if self.desk.data is None and self.desk.keyed:
|
|
384
|
+
with narration_to(sink):
|
|
385
|
+
wanted, ready = _stored_universes(self.desk.session)
|
|
386
|
+
if wanted:
|
|
387
|
+
try:
|
|
388
|
+
self.desk.data = _narrow_to_universe(self.desk.session, wanted, None)
|
|
389
|
+
self.desk.loaded = f"universe:{wanted}"
|
|
390
|
+
except Exception as exc: # noqa: BLE001
|
|
391
|
+
notes.append(str(exc))
|
|
392
|
+
elif len(ready) > 1:
|
|
393
|
+
notes.append(
|
|
394
|
+
"Stored on your account: " + ", ".join(ready) + ". Type load <name> to use one."
|
|
395
|
+
)
|
|
396
|
+
log = self.query_one("#transcript", RichLog)
|
|
397
|
+
for note in notes:
|
|
398
|
+
plain = _plain(note)
|
|
399
|
+
if plain.strip():
|
|
400
|
+
self.transcript.append(plain)
|
|
401
|
+
log.write(plain)
|
|
250
402
|
self._refresh_rail()
|
|
251
403
|
|
|
252
404
|
def _run_line(self, text: str) -> None:
|
|
@@ -274,7 +426,19 @@ class QuantOSApp(App[None]):
|
|
|
274
426
|
prompt = self.query_one(Input)
|
|
275
427
|
prompt.password = True
|
|
276
428
|
prompt.placeholder = "paste the key (hidden)"
|
|
277
|
-
self.
|
|
429
|
+
elif self.desk.pending_provider:
|
|
430
|
+
prompt = self.query_one(Input)
|
|
431
|
+
prompt.password = False
|
|
432
|
+
prompt.placeholder = "anthropic, openai, gemini, groq, openrouter, azure, gateway"
|
|
433
|
+
elif self.desk.pending_url:
|
|
434
|
+
prompt = self.query_one(Input)
|
|
435
|
+
prompt.password = False
|
|
436
|
+
prompt.placeholder = f"paste {self.desk.pending_url}"
|
|
437
|
+
if self.desk.account_dirty:
|
|
438
|
+
self.desk.account_dirty = False
|
|
439
|
+
self._sync_account()
|
|
440
|
+
else:
|
|
441
|
+
self._refresh_rail()
|
|
278
442
|
self._load_lines()
|
|
279
443
|
self.busy = False
|
|
280
444
|
if quitting:
|
|
@@ -51,6 +51,10 @@ class Desk:
|
|
|
51
51
|
think: Callable[[str], str] | None = None
|
|
52
52
|
keyed: bool = False
|
|
53
53
|
pending_secret: str | None = None
|
|
54
|
+
pending_url: str | None = None
|
|
55
|
+
pending_provider: bool = False
|
|
56
|
+
held_key: str | None = None
|
|
57
|
+
account_dirty: bool = False
|
|
54
58
|
pinned_model: str | None = None
|
|
55
59
|
book: Any = None
|
|
56
60
|
|
|
@@ -84,6 +88,11 @@ def submit(desk: Desk, line: str) -> bool:
|
|
|
84
88
|
if line.startswith("/"):
|
|
85
89
|
line = line[1:].lstrip()
|
|
86
90
|
|
|
91
|
+
if line.lower() == "enter model key":
|
|
92
|
+
desk.pending_secret = "MODEL_KEY"
|
|
93
|
+
say(dim("Paste the model key. It stays hidden. The provider is read from the key."))
|
|
94
|
+
return False
|
|
95
|
+
|
|
87
96
|
verb, _, rest = line.partition(" ")
|
|
88
97
|
if verb in QUESTION_VERBS and not rest:
|
|
89
98
|
verb, rest = "run", QUESTION_VERBS[verb]
|
|
@@ -210,28 +219,58 @@ def _ns(desk: Desk, **extra: Any) -> Any:
|
|
|
210
219
|
def _logout(desk: Desk) -> None:
|
|
211
220
|
import os
|
|
212
221
|
|
|
222
|
+
from ..auth import MANAGED
|
|
213
223
|
from ..cli import _session, cmd_logout
|
|
214
224
|
|
|
215
225
|
cmd_logout(_ns(desk))
|
|
216
|
-
for name in
|
|
226
|
+
for name in MANAGED:
|
|
217
227
|
os.environ.pop(name, None)
|
|
218
228
|
desk.session, desk.url = _session(getattr(desk.args, "url", None), getattr(desk.args, "key", None))
|
|
219
229
|
desk.keyed = False
|
|
220
230
|
desk.pending_secret = None
|
|
231
|
+
desk.pending_url = None
|
|
232
|
+
desk.pending_provider = False
|
|
233
|
+
desk.held_key = None
|
|
234
|
+
desk.pinned_model = None
|
|
235
|
+
if str(desk.loaded or "").startswith("universe:"):
|
|
236
|
+
desk.data = None
|
|
237
|
+
desk.backtest_fn = None
|
|
238
|
+
desk.loaded = None
|
|
239
|
+
desk.account_dirty = True
|
|
221
240
|
|
|
222
241
|
|
|
223
242
|
def _key(desk: Desk, rest: str) -> None:
|
|
243
|
+
from ..cli import _KEY_PROMPTS
|
|
244
|
+
|
|
224
245
|
which = (rest.split() or [""])[0].lower()
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
246
|
+
if not which:
|
|
247
|
+
if not desk.keyed:
|
|
248
|
+
which = "quantos"
|
|
249
|
+
else:
|
|
250
|
+
say(dim("Already signed in. Type `enter model key` and paste it."))
|
|
251
|
+
return
|
|
252
|
+
if which not in _KEY_PROMPTS:
|
|
253
|
+
say(dim(f"Unknown {which!r}. Try: {', '.join(_KEY_PROMPTS)}"))
|
|
232
254
|
return
|
|
233
|
-
|
|
234
|
-
|
|
255
|
+
env, what = _KEY_PROMPTS[which]
|
|
256
|
+
desk.pending_secret = env
|
|
257
|
+
say(dim(f"Paste the {which} key. {what} It stays hidden, then this machine keeps it."))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def env_for_model_key(value: str) -> str | None:
|
|
261
|
+
"""Which env var this key belongs in, when the prefix says so."""
|
|
262
|
+
text = value.strip()
|
|
263
|
+
if text.startswith("sk-ant-"):
|
|
264
|
+
return "ANTHROPIC_API_KEY"
|
|
265
|
+
if text.startswith("sk-or-"):
|
|
266
|
+
return "OPENROUTER_API_KEY"
|
|
267
|
+
if text.startswith("gsk_"):
|
|
268
|
+
return "GROQ_API_KEY"
|
|
269
|
+
if text.startswith("AIza"):
|
|
270
|
+
return "GEMINI_API_KEY"
|
|
271
|
+
if text.startswith("sk-"):
|
|
272
|
+
return "OPENAI_API_KEY"
|
|
273
|
+
return None
|
|
235
274
|
|
|
236
275
|
|
|
237
276
|
def _pin_model(desk: Desk, rest: str) -> None:
|
|
@@ -85,6 +85,8 @@ def test_boot_shows_the_ladder_and_a_stored_universe() -> None:
|
|
|
85
85
|
app = QuantOSApp(session=_DeskSession(), url="fake://", data=_returns(), keyed=True, think=_think)
|
|
86
86
|
async with app.run_test(size=(140, 42)) as pilot:
|
|
87
87
|
await pilot.pause()
|
|
88
|
+
assert "enter model key" in app.rail_text
|
|
89
|
+
assert "load <name>" in app.rail_text
|
|
88
90
|
assert "the maths" in app.rail_text
|
|
89
91
|
assert "sp500" in app.rail_text
|
|
90
92
|
prompt = app.query_one(Input)
|
|
@@ -112,6 +114,96 @@ def test_a_scripted_run_opens_the_sharpe_line_at_full_length() -> None:
|
|
|
112
114
|
asyncio.run(body())
|
|
113
115
|
|
|
114
116
|
|
|
117
|
+
class _StoredBook(Session):
|
|
118
|
+
"""One portal universe whose closes were stored with the upload."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, names: list[str]) -> None:
|
|
121
|
+
super().__init__(base_url="fake://", api_key="ae_live_test")
|
|
122
|
+
self._names = names
|
|
123
|
+
|
|
124
|
+
def _get(self, path: str):
|
|
125
|
+
if path.endswith("/series"):
|
|
126
|
+
return {"prices": {"AAPL": [10.0, 11.0, 12.0, 13.0]}}
|
|
127
|
+
if path.endswith("/universes"):
|
|
128
|
+
return {
|
|
129
|
+
"universes": [
|
|
130
|
+
{
|
|
131
|
+
"id": f"u{i}",
|
|
132
|
+
"name": name,
|
|
133
|
+
"definition": {"cache_id": f"c{i}", "symbols": ["AAPL"]},
|
|
134
|
+
}
|
|
135
|
+
for i, name in enumerate(self._names)
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
if path.endswith("/workflows"):
|
|
139
|
+
return {"workflows": []}
|
|
140
|
+
return {}
|
|
141
|
+
|
|
142
|
+
def _post(self, path, body):
|
|
143
|
+
return {}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_a_signed_in_account_loads_its_one_stored_universe() -> None:
|
|
147
|
+
async def body() -> None:
|
|
148
|
+
app = QuantOSApp(session=_StoredBook(["sp500"]), url="fake://", keyed=True, think=_think)
|
|
149
|
+
async with app.run_test(size=(140, 42)) as pilot:
|
|
150
|
+
await pilot.pause()
|
|
151
|
+
assert app.desk.loaded == "universe:sp500"
|
|
152
|
+
assert "AAPL" in app.desk.data
|
|
153
|
+
assert "stored" in app.rail_text
|
|
154
|
+
assert any("portal" in line for line in app.transcript)
|
|
155
|
+
|
|
156
|
+
asyncio.run(body())
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_several_stored_universes_are_named_and_not_guessed() -> None:
|
|
160
|
+
async def body() -> None:
|
|
161
|
+
app = QuantOSApp(session=_StoredBook(["sp500", "ndx"]), url="fake://", keyed=True, think=_think)
|
|
162
|
+
async with app.run_test(size=(140, 42)) as pilot:
|
|
163
|
+
await pilot.pause()
|
|
164
|
+
assert app.desk.data is None
|
|
165
|
+
text = "\n".join(app.transcript)
|
|
166
|
+
assert "sp500" in text and "ndx" in text
|
|
167
|
+
|
|
168
|
+
asyncio.run(body())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_login_pastes_a_quantos_key_and_login_gemini_pastes_that_provider() -> None:
|
|
172
|
+
async def body() -> None:
|
|
173
|
+
app = QuantOSApp(session=_DeskSession(), url="fake://", keyed=False, think=_think)
|
|
174
|
+
async with app.run_test(size=(140, 42)) as pilot:
|
|
175
|
+
await pilot.pause()
|
|
176
|
+
field = app.query_one(Input)
|
|
177
|
+
field.focus()
|
|
178
|
+
field.value = "login"
|
|
179
|
+
await pilot.press("enter")
|
|
180
|
+
await _wait(pilot, app)
|
|
181
|
+
assert app.desk.pending_secret == "QUANTOS_API_KEY"
|
|
182
|
+
assert app.query_one(Input).password is True
|
|
183
|
+
|
|
184
|
+
app.desk.pending_secret = None
|
|
185
|
+
app.desk.keyed = True
|
|
186
|
+
app.query_one(Input).password = False
|
|
187
|
+
field.value = "enter model key"
|
|
188
|
+
await pilot.press("enter")
|
|
189
|
+
await _wait(pilot, app)
|
|
190
|
+
assert app.desk.pending_secret == "MODEL_KEY"
|
|
191
|
+
assert app.query_one(Input).password is True
|
|
192
|
+
|
|
193
|
+
asyncio.run(body())
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_a_model_key_prefix_names_the_provider() -> None:
|
|
197
|
+
from alphaengine.tui.dispatch import env_for_model_key
|
|
198
|
+
|
|
199
|
+
assert env_for_model_key("sk-ant-abc") == "ANTHROPIC_API_KEY"
|
|
200
|
+
assert env_for_model_key("sk-or-abc") == "OPENROUTER_API_KEY"
|
|
201
|
+
assert env_for_model_key("gsk_abc") == "GROQ_API_KEY"
|
|
202
|
+
assert env_for_model_key("AIzaabc") == "GEMINI_API_KEY"
|
|
203
|
+
assert env_for_model_key("sk-abc") == "OPENAI_API_KEY"
|
|
204
|
+
assert env_for_model_key("not-a-known-prefix") is None
|
|
205
|
+
|
|
206
|
+
|
|
115
207
|
def test_a_sentence_uses_the_fake_model_to_choose_a_workflow() -> None:
|
|
116
208
|
async def body() -> None:
|
|
117
209
|
app = QuantOSApp(session=_DeskSession(), url="fake://", data=_returns(), keyed=True, think=_think)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|