ranranrunforit commited on
Commit
e57b966
·
verified ·
1 Parent(s): 05546bf

Upload 33 files

Browse files
automation.py CHANGED
@@ -20,11 +20,14 @@ schedule fires unattended.
20
  """
21
  from __future__ import annotations
22
 
 
23
  import datetime as dt
24
  import threading
25
  import traceback
26
  from zoneinfo import ZoneInfo
27
 
 
 
28
  NY = ZoneInfo("America/New_York")
29
  RUN_HOUR, RUN_MINUTE = 18, 10
30
 
@@ -41,6 +44,27 @@ STATE = {
41
  }
42
  _lock = threading.Lock()
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  def _log(msg: str):
46
  stamp = dt.datetime.now(NY).strftime("%m-%d %H:%M:%S ET")
@@ -109,6 +133,7 @@ def run_pipeline(tickers=None, force: bool = True) -> str:
109
  _log(f"Auto-research skipped: {e}")
110
 
111
  STATE["last_run"] = dt.datetime.now(NY)
 
112
  _log("Pipeline finished.")
113
  return f"Done. {summary}"
114
  except Exception as e:
 
20
  """
21
  from __future__ import annotations
22
 
23
+ import os
24
  import datetime as dt
25
  import threading
26
  import traceback
27
  from zoneinfo import ZoneInfo
28
 
29
+ import paths
30
+
31
  NY = ZoneInfo("America/New_York")
32
  RUN_HOUR, RUN_MINUTE = 18, 10
33
 
 
44
  }
45
  _lock = threading.Lock()
46
 
47
+ _LAST_RUN_FILE = os.path.join(paths.OUTPUT_DIR, "last_run.txt")
48
+
49
+
50
+ def _save_last_run(when: dt.datetime):
51
+ try:
52
+ with open(_LAST_RUN_FILE, "w", encoding="utf-8") as f:
53
+ f.write(when.isoformat())
54
+ except Exception:
55
+ pass
56
+
57
+
58
+ def _load_last_run():
59
+ try:
60
+ with open(_LAST_RUN_FILE, encoding="utf-8") as f:
61
+ STATE["last_run"] = dt.datetime.fromisoformat(f.read().strip())
62
+ except Exception:
63
+ STATE["last_run"] = None
64
+
65
+
66
+ _load_last_run()
67
+
68
 
69
  def _log(msg: str):
70
  stamp = dt.datetime.now(NY).strftime("%m-%d %H:%M:%S ET")
 
133
  _log(f"Auto-research skipped: {e}")
134
 
135
  STATE["last_run"] = dt.datetime.now(NY)
136
+ _save_last_run(STATE["last_run"])
137
  _log("Pipeline finished.")
138
  return f"Done. {summary}"
139
  except Exception as e:
server.py CHANGED
@@ -237,6 +237,27 @@ async def market_status():
237
  return JSONResponse({"open": is_open, "label": label, "variant": variant})
238
 
239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  @app.get("/api/model/status")
241
  async def model_status():
242
  return JSONResponse({
 
237
  return JSONResponse({"open": is_open, "label": label, "variant": variant})
238
 
239
 
240
+ @app.get("/api/model/list")
241
+ async def model_list():
242
+ return JSONResponse({
243
+ "models": list(llm_local.MODEL_ZOO.keys()),
244
+ "analyst": llm_local.WORKERS["analyst"]["model"],
245
+ "analyst_ready": llm_local.WORKERS["analyst"]["llm"] is not None,
246
+ })
247
+
248
+
249
+ @app.post("/api/model/load")
250
+ async def model_load(req: Request):
251
+ body = await req.json()
252
+ name = body.get("model", "")
253
+ if name not in llm_local.MODEL_ZOO:
254
+ return JSONResponse({"status": "⚠️ Unknown model."})
255
+ import threading
256
+ threading.Thread(target=lambda: llm_local.load_model(name, worker="analyst"),
257
+ daemon=True).start()
258
+ return JSONResponse({"status": f"⏳ Loading {name} onto the Analyst sub-agent…"})
259
+
260
+
261
  @app.get("/api/model/status")
262
  async def model_status():
263
  return JSONResponse({
ui_kits/chan-compass/api.js CHANGED
@@ -55,6 +55,8 @@ window.CCApi = (function () {
55
  autoState: () => jget("/api/automation/state"),
56
  publishTraces: (repo) => jpost("/api/automation/publish-traces", { repo }),
57
  marketStatus: () => jget("/api/market/status"),
 
 
58
  modelStatus: () => jget("/api/model/status"),
59
  modelTest: () => jpost("/api/model/test", {}),
60
  modelTestStatus: () => jget("/api/model/test-status"),
 
55
  autoState: () => jget("/api/automation/state"),
56
  publishTraces: (repo) => jpost("/api/automation/publish-traces", { repo }),
57
  marketStatus: () => jget("/api/market/status"),
58
+ modelList: () => jget("/api/model/list"),
59
+ modelLoad: (model) => jpost("/api/model/load", { model }),
60
  modelStatus: () => jget("/api/model/status"),
61
  modelTest: () => jpost("/api/model/test", {}),
62
  modelTestStatus: () => jget("/api/model/test-status"),
ui_kits/chan-compass/app.jsx CHANGED
@@ -9,7 +9,8 @@ function _pascalG(name) {
9
  function _glyphChildren(node) {
10
  if (!node) return [];
11
  if (Array.isArray(node)) {
12
- if (node.length === 3 && Array.isArray(node[2])) return node[2];
 
13
  return node;
14
  }
15
  return node.tags || node.children || [];
@@ -29,6 +30,16 @@ function _glyphSvg(name, size, color) {
29
  + `stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
30
  }
31
  function Glyph({ name, size = 18, color, style }) {
 
 
 
 
 
 
 
 
 
 
32
  const html = _glyphSvg(name, size, color);
33
  return <span style={{ width:size, height:size, display:'inline-flex',
34
  alignItems:'center', justifyContent:'center', ...style }}
@@ -109,11 +120,14 @@ function App() {
109
  </main>
110
 
111
  <footer className="cc-footer">
112
- <span>Chan Compass · built for my family, not investment advice</span>
 
 
 
113
  <span className="cc-foot-sep">·</span>
114
  <span>data: Yahoo Finance</span>
115
  <span className="cc-foot-sep">·</span>
116
- <span>design language: Adobe Spectrum 2</span>
117
  </footer>
118
  </div>
119
  );
 
9
  function _glyphChildren(node) {
10
  if (!node) return [];
11
  if (Array.isArray(node)) {
12
+ if (typeof node[0] === "string" && node[0].toLowerCase() === "svg" && Array.isArray(node[2]))
13
+ return node[2];
14
  return node;
15
  }
16
  return node.tags || node.children || [];
 
30
  + `stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
31
  }
32
  function Glyph({ name, size = 18, color, style }) {
33
+ const [, force] = useS(0);
34
+ useE(() => {
35
+ if (window.lucide && window.lucide.icons) return;
36
+ let n = 0;
37
+ const id = setInterval(() => {
38
+ n++;
39
+ if ((window.lucide && window.lucide.icons) || n > 40) { clearInterval(id); force(x=>x+1); }
40
+ }, 100);
41
+ return () => clearInterval(id);
42
+ }, []);
43
  const html = _glyphSvg(name, size, color);
44
  return <span style={{ width:size, height:size, display:'inline-flex',
45
  alignItems:'center', justifyContent:'center', ...style }}
 
120
  </main>
121
 
122
  <footer className="cc-footer">
123
+ <span>Chan Compass</span>
124
+ <span className="cc-foot-sep">·</span>
125
+ <span>by <a href="https://huggingface.co/ranranrunforit" target="_blank" rel="noreferrer"
126
+ style={{color:"var(--accent)", textDecoration:"none"}}>@ranranrunforit</a>, made with care for my family</span>
127
  <span className="cc-foot-sep">·</span>
128
  <span>data: Yahoo Finance</span>
129
  <span className="cc-foot-sep">·</span>
130
+ <span>design: Adobe Spectrum 2</span>
131
  </footer>
132
  </div>
133
  );
ui_kits/chan-compass/views.jsx CHANGED
@@ -5,6 +5,22 @@ const { Button, Field, Checkbox, Switch, Card, InlineAlert, Badge, StatusLight,
5
  const { useState, useEffect, useRef } = React;
6
  const API = window.CCApi;
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  // kebab-case (data-lucide name) → PascalCase (lucide.icons key)
9
  function _pascal(name) {
10
  return String(name).split(/[-_]/).map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("");
@@ -12,8 +28,10 @@ function _pascal(name) {
12
  function _lucideChildren(node) {
13
  if (!node) return [];
14
  if (Array.isArray(node)) {
15
- // could be [ [tag,attrs], ... ] OR ["svg", attrs, [ [tag,attrs],... ]]
16
- if (node.length === 3 && Array.isArray(node[2])) return node[2];
 
 
17
  return node;
18
  }
19
  return node.tags || node.children || [];
@@ -33,6 +51,16 @@ function _lucideSvg(name, size, color) {
33
  + `stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
34
  }
35
  function Icon({ name, size = 18, color, style }) {
 
 
 
 
 
 
 
 
 
 
36
  const html = _lucideSvg(name, size, color);
37
  return <span style={{ width:size, height:size, display:"inline-flex",
38
  alignItems:"center", justifyContent:"center", ...style }}
@@ -84,13 +112,13 @@ function Markdown({ text }) {
84
 
85
  /* ─────────── Signals ─────────── */
86
  function SignalsView() {
87
- const [pool, setPool] = useState("AAPL, MSFT, NVDA, TSLA, AMZN, GOOGL, META, AMD");
88
  const [force, setForce] = useState(false);
89
  const [running, setRunning] = useState(false);
90
- const [rows, setRows] = useState([]);
91
- const [summary, setSummary] = useState("");
92
- const [sel, setSel] = useState(null);
93
- const [aiText, setAiText] = useState("");
94
  const [aiBusy, setAiBusy] = useState(false);
95
 
96
  const run = async () => {
@@ -175,11 +203,11 @@ function SignalsView() {
175
 
176
  /* ─────────── Rotation ─────────── */
177
  function RotationView() {
178
- const [d1, setD1] = useState([]);
179
- const [d5, setD5] = useState([]);
180
- const [d20, setD20] = useState([]);
181
- const [asof, setAsof] = useState("");
182
- const [ai, setAi] = useState(""); const [busy, setBusy] = useState(false);
183
  const [loading, setLoading] = useState(false);
184
  const refresh = async () => {
185
  setLoading(true);
@@ -232,11 +260,11 @@ function RotationView() {
232
 
233
  /* ─────────── News ─────────── */
234
  function NewsView() {
235
- const [hold, setHold] = useState("");
236
- const [out, setOut] = useState("");
237
  const [busy, setBusy] = useState(false);
238
  const [saved, setSaved] = useState("");
239
- useEffect(()=>{ API.holdings().then(r=>setHold((r.holdings||[]).join(", "))).catch(()=>{}); },[]);
240
  const save = async () => {
241
  setSaved("Saving…");
242
  try { const r = await API.saveHoldings(hold);
@@ -267,12 +295,12 @@ function NewsView() {
267
 
268
  /* ─────────── Research ─────────── */
269
  function ResearchView() {
270
- const [tic, setTic] = useState("");
271
- const [progress, setProgress] = useState("");
272
- const [report, setReport] = useState("");
273
- const [reports, setReports] = useState([]);
274
  const [busy, setBusy] = useState(false);
275
- const [sel, setSel] = useState(null);
276
  useEffect(()=>{ API.reports().then(r=>setReports(r.reports||[])).catch(()=>{}); },[]);
277
  const run = async () => {
278
  if (!tic) return;
@@ -362,10 +390,16 @@ function ModelView() {
362
  const [workers, setWorkers] = useState({});
363
  const [test, setTest] = useState("");
364
  const [ft, setFt] = useState(""); const [dl, setDl] = useState("");
 
 
 
 
365
  useEffect(()=>{
366
  const tick = ()=> API.modelStatus().then(r=>{setStatus(r.status); setWorkers(r.workers||{});}).catch(()=>{});
367
  tick(); const id=setInterval(tick, 3000); return ()=>clearInterval(id);
368
  },[]);
 
 
369
  useEffect(()=>{ API.finetuneStatus().then(r=>setFt(r.status)).catch(()=>{}); },[]);
370
  const runTest = async () => {
371
  setTest("⏳ Testing sub-agents… (this can take a moment on CPU)");
@@ -377,11 +411,17 @@ function ModelView() {
377
  } catch (_) { clearInterval(poll); }
378
  }, 1500);
379
  };
 
 
 
 
 
 
380
  const [dlUrl, setDlUrl] = useState("");
381
  const exportDs = async () => {
382
  const r = await API.exportDataset();
383
  if (r.download) { setDl("Exported " + r.count + " pairs."); setDlUrl(r.download); }
384
- else { setDl("No pairs captured yet — run a few Signals AI summaries first."); setDlUrl(""); }
385
  };
386
  return (
387
  <div style={{display:"flex", flexDirection:"column", gap:"var(--space-300)"}}>
@@ -401,7 +441,25 @@ function ModelView() {
401
  </div>
402
  {test && <div className="cc-llm-out" style={{marginTop:12}}><Markdown text={test}/></div>}
403
  </Card>
404
- <Card title="Fine-tuning dataset" subtitle="Every Signals AI summary is captured as a training pair on /data">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  <div className="dim" style={{marginBottom:10}}><Markdown text={ft}/></div>
406
  <Button variant="accent" onClick={exportDs}><Icon name="download" size={15}/> Export dataset (JSONL)</Button>
407
  {dl && <div className="dim" style={{marginTop:8, fontSize:13}}>{dl}{dlUrl &&
 
5
  const { useState, useEffect, useRef } = React;
6
  const API = window.CCApi;
7
 
8
+ /* Persistent store — survives tab unmount/remount so data isn't lost when the
9
+ user switches tabs and comes back. Backed by a module-level object. */
10
+ window.CCStore = window.CCStore || {};
11
+ function usePersistent(key, initial) {
12
+ const [v, setV] = useState(() => (key in window.CCStore ? window.CCStore[key] : initial));
13
+ const set = (nv) => {
14
+ setV(prev => {
15
+ const next = typeof nv === "function" ? nv(prev) : nv;
16
+ window.CCStore[key] = next;
17
+ return next;
18
+ });
19
+ };
20
+ return [v, set];
21
+ }
22
+
23
+
24
  // kebab-case (data-lucide name) → PascalCase (lucide.icons key)
25
  function _pascal(name) {
26
  return String(name).split(/[-_]/).map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("");
 
28
  function _lucideChildren(node) {
29
  if (!node) return [];
30
  if (Array.isArray(node)) {
31
+ // wrapped form: ["svg", attrs, [children]] → children at [2]
32
+ if (typeof node[0] === "string" && node[0].toLowerCase() === "svg" && Array.isArray(node[2]))
33
+ return node[2];
34
+ // bare iconNode: [ [tag, attrs], [tag, attrs], ... ]
35
  return node;
36
  }
37
  return node.tags || node.children || [];
 
51
  + `stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
52
  }
53
  function Icon({ name, size = 18, color, style }) {
54
+ const [, force] = useState(0);
55
+ useEffect(() => {
56
+ if (window.lucide && window.lucide.icons) return;
57
+ let n = 0;
58
+ const id = setInterval(() => {
59
+ n++;
60
+ if ((window.lucide && window.lucide.icons) || n > 40) { clearInterval(id); force(x=>x+1); }
61
+ }, 100);
62
+ return () => clearInterval(id);
63
+ }, []);
64
  const html = _lucideSvg(name, size, color);
65
  return <span style={{ width:size, height:size, display:"inline-flex",
66
  alignItems:"center", justifyContent:"center", ...style }}
 
112
 
113
  /* ─────────── Signals ─────────── */
114
  function SignalsView() {
115
+ const [pool, setPool] = usePersistent("sig.pool", "AAPL, MSFT, NVDA, TSLA, AMZN, GOOGL, META, AMD");
116
  const [force, setForce] = useState(false);
117
  const [running, setRunning] = useState(false);
118
+ const [rows, setRows] = usePersistent("sig.rows", []);
119
+ const [summary, setSummary] = usePersistent("sig.summary", "");
120
+ const [sel, setSel] = usePersistent("sig.sel", null);
121
+ const [aiText, setAiText] = usePersistent("sig.ai", "");
122
  const [aiBusy, setAiBusy] = useState(false);
123
 
124
  const run = async () => {
 
203
 
204
  /* ─────────── Rotation ─────────── */
205
  function RotationView() {
206
+ const [d1, setD1] = usePersistent("rot.d1", []);
207
+ const [d5, setD5] = usePersistent("rot.d5", []);
208
+ const [d20, setD20] = usePersistent("rot.d20", []);
209
+ const [asof, setAsof] = usePersistent("rot.asof", "");
210
+ const [ai, setAi] = usePersistent("rot.ai", ""); const [busy, setBusy] = useState(false);
211
  const [loading, setLoading] = useState(false);
212
  const refresh = async () => {
213
  setLoading(true);
 
260
 
261
  /* ─────────── News ─────────── */
262
  function NewsView() {
263
+ const [hold, setHold] = usePersistent("news.hold", "");
264
+ const [out, setOut] = usePersistent("news.out", "");
265
  const [busy, setBusy] = useState(false);
266
  const [saved, setSaved] = useState("");
267
+ useEffect(()=>{ if(!("news.hold" in window.CCStore)) API.holdings().then(r=>setHold((r.holdings||[]).join(", "))).catch(()=>{}); },[]);
268
  const save = async () => {
269
  setSaved("Saving…");
270
  try { const r = await API.saveHoldings(hold);
 
295
 
296
  /* ─────────── Research ─────────── */
297
  function ResearchView() {
298
+ const [tic, setTic] = usePersistent("res.tic", "");
299
+ const [progress, setProgress] = usePersistent("res.progress", "");
300
+ const [report, setReport] = usePersistent("res.report", "");
301
+ const [reports, setReports] = usePersistent("res.reports", []);
302
  const [busy, setBusy] = useState(false);
303
+ const [sel, setSel] = usePersistent("res.sel", null);
304
  useEffect(()=>{ API.reports().then(r=>setReports(r.reports||[])).catch(()=>{}); },[]);
305
  const run = async () => {
306
  if (!tic) return;
 
390
  const [workers, setWorkers] = useState({});
391
  const [test, setTest] = useState("");
392
  const [ft, setFt] = useState(""); const [dl, setDl] = useState("");
393
+ const [models, setModels] = useState([]);
394
+ const [analyst, setAnalyst] = useState("");
395
+ const [pick, setPick] = useState("");
396
+ const [loadMsg, setLoadMsg] = useState("");
397
  useEffect(()=>{
398
  const tick = ()=> API.modelStatus().then(r=>{setStatus(r.status); setWorkers(r.workers||{});}).catch(()=>{});
399
  tick(); const id=setInterval(tick, 3000); return ()=>clearInterval(id);
400
  },[]);
401
+ useEffect(()=>{ API.modelList().then(r=>{ setModels(r.models||[]); setAnalyst(r.analyst||"");
402
+ setPick(p=>p||r.analyst||""); }).catch(()=>{}); },[]);
403
  useEffect(()=>{ API.finetuneStatus().then(r=>setFt(r.status)).catch(()=>{}); },[]);
404
  const runTest = async () => {
405
  setTest("⏳ Testing sub-agents… (this can take a moment on CPU)");
 
411
  } catch (_) { clearInterval(poll); }
412
  }, 1500);
413
  };
414
+ const loadModel = async () => {
415
+ if (!pick) return;
416
+ setLoadMsg("⏳ Loading…");
417
+ try { const r = await API.modelLoad(pick); setLoadMsg(r.status); }
418
+ catch(e){ setLoadMsg("❌ " + e.message); }
419
+ };
420
  const [dlUrl, setDlUrl] = useState("");
421
  const exportDs = async () => {
422
  const r = await API.exportDataset();
423
  if (r.download) { setDl("Exported " + r.count + " pairs."); setDlUrl(r.download); }
424
+ else { setDl("No pairs captured yet — run a few Signals AI interprets first."); setDlUrl(""); }
425
  };
426
  return (
427
  <div style={{display:"flex", flexDirection:"column", gap:"var(--space-300)"}}>
 
441
  </div>
442
  {test && <div className="cc-llm-out" style={{marginTop:12}}><Markdown text={test}/></div>}
443
  </Card>
444
+ <Card title="Analyst model" subtitle="Pick the model for the Auto Research analyst (all ≤32B). The 1.7B sub-agents are fixed.">
445
+ <div className="cc-radios">
446
+ {models.map(m=>(
447
+ <label key={m} className={"cc-radio" + (pick===m?" sel":"")} onClick={()=>setPick(m)}
448
+ style={{cursor:"pointer"}}>
449
+ <span className="cc-radio-dot"></span>
450
+ <span className="mono">{m}</span>
451
+ {analyst===m && <Badge variant="positive">current</Badge>}
452
+ </label>
453
+ ))}
454
+ </div>
455
+ <div style={{display:"flex", gap:"var(--space-150)", marginTop:"var(--space-200)", alignItems:"center"}}>
456
+ <Button variant="accent" onClick={loadModel} disabled={!pick || pick===analyst}>
457
+ <Icon name="download" size={15}/> Load model
458
+ </Button>
459
+ {loadMsg && <span className="dim" style={{fontSize:13}}>{loadMsg}</span>}
460
+ </div>
461
+ </Card>
462
+ <Card title="Fine-tuning dataset" subtitle="Every Signals AI interpret is captured as a training pair on /data">
463
  <div className="dim" style={{marginBottom:10}}><Markdown text={ft}/></div>
464
  <Button variant="accent" onClick={exportDs}><Icon name="download" size={15}/> Export dataset (JSONL)</Button>
465
  {dl && <div className="dim" style={{marginTop:8, fontSize:13}}>{dl}{dlUrl &&