ranranrunforit commited on
Commit
434d9ad
·
verified ·
1 Parent(s): 6247c12

Upload 33 files

Browse files
automation.py CHANGED
@@ -43,8 +43,53 @@ STATE = {
43
  "log": [],
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):
@@ -134,11 +179,12 @@ def run_pipeline(tickers=None, force: bool = True) -> str:
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:
140
  traceback.print_exc()
141
- _log(f"Pipeline error: {e}")
142
  return f"Pipeline error: {e}"
143
  finally:
144
  STATE["running"] = False
@@ -164,8 +210,9 @@ def start_scheduler():
164
  def schedule_info() -> str:
165
  now = dt.datetime.now(NY)
166
  last = STATE["last_run"].strftime("%Y-%m-%d %H:%M ET") if STATE["last_run"] else "never"
 
167
  return (f"**Schedule:** Mon–Fri **{RUN_HOUR:02d}:{RUN_MINUTE:02d} America/New_York** "
168
- f"(market closes 16:00 ET; ).\n\n"
169
  f"**Now (ET):** {now.strftime('%Y-%m-%d %H:%M')} · **Last run:** {last}\n\n"
170
- f"⚠️ On free Space hardware the app sleeps when idle and the timer can't fire; "
171
- f"use **Run now**, or upgrade to always-on hardware for unattended updates.")
 
43
  "log": [],
44
  }
45
  _lock = threading.Lock()
46
+ _SCHED = None # holds the BackgroundScheduler so it isn't garbage-collected
47
 
48
  _LAST_RUN_FILE = os.path.join(paths.OUTPUT_DIR, "last_run.txt")
49
+ _RESULTS_FILE = os.path.join(paths.OUTPUT_DIR, "last_results.json")
50
+
51
+
52
+ def _df_to_records(df):
53
+ try:
54
+ return df.to_dict(orient="records") if df is not None and hasattr(df, "to_dict") else []
55
+ except Exception:
56
+ return []
57
+
58
+
59
+ def save_results():
60
+ """Persist the latest pipeline output (signals + rotation + news) to /data so
61
+ the app shows the last run's results after a restart, with no recompute."""
62
+ import json
63
+ try:
64
+ import rotation
65
+ d1, d5, d20, asof = STATE.get("rotation", (None, None, None, "—"))
66
+ payload = {
67
+ "signals_rows": _df_to_records(STATE.get("signals_df")),
68
+ "signals_summary": STATE.get("signals_summary", ""),
69
+ "rotation": {
70
+ "asof": asof,
71
+ "d1": _df_to_records(rotation.fmt_table(d1)) if d1 is not None else [],
72
+ "d5": _df_to_records(rotation.fmt_table(d5)) if d5 is not None else [],
73
+ "d20": _df_to_records(rotation.fmt_table(d20)) if d20 is not None else [],
74
+ },
75
+ "rotation_narrative": STATE.get("rotation_narrative", ""),
76
+ "news_md": STATE.get("news_md", ""),
77
+ "saved_at": dt.datetime.now(NY).isoformat(),
78
+ }
79
+ with open(_RESULTS_FILE, "w", encoding="utf-8") as f:
80
+ json.dump(payload, f, ensure_ascii=False)
81
+ except Exception as e:
82
+ _log(f"save_results failed: {e}")
83
+
84
+
85
+ def load_results() -> dict:
86
+ """Read the persisted last-run results (for the frontend on page load)."""
87
+ import json
88
+ try:
89
+ with open(_RESULTS_FILE, encoding="utf-8") as f:
90
+ return json.load(f)
91
+ except Exception:
92
+ return {}
93
 
94
 
95
  def _save_last_run(when: dt.datetime):
 
179
 
180
  STATE["last_run"] = dt.datetime.now(NY)
181
  _save_last_run(STATE["last_run"])
182
+ save_results()
183
+ _log("Pipeline finished — Last run updated, results saved.")
184
  return f"Done. {summary}"
185
  except Exception as e:
186
  traceback.print_exc()
187
+ _log(f"Pipeline error (Last run not updated): {e}")
188
  return f"Pipeline error: {e}"
189
  finally:
190
  STATE["running"] = False
 
210
  def schedule_info() -> str:
211
  now = dt.datetime.now(NY)
212
  last = STATE["last_run"].strftime("%Y-%m-%d %H:%M ET") if STATE["last_run"] else "never"
213
+ armed = "✅ Scheduler armed" if _SCHED is not None else "⏳ Scheduler starting…"
214
  return (f"**Schedule:** Mon–Fri **{RUN_HOUR:02d}:{RUN_MINUTE:02d} America/New_York** "
215
+ f"(after the 16:00 ET close).\n\n"
216
  f"**Now (ET):** {now.strftime('%Y-%m-%d %H:%M')} · **Last run:** {last}\n\n"
217
+ f"{armed} it runs automatically every weekday at "
218
+ f"{RUN_HOUR:02d}:{RUN_MINUTE:02d} ET. You can also trigger it any time with **Run now**.")
server.py CHANGED
@@ -59,6 +59,11 @@ def _df_records(df):
59
 
60
 
61
  # ───────────────────────── Signals ─────────────────────────
 
 
 
 
 
62
  @app.post("/api/signals/run")
63
  async def signals_run(req: Request):
64
  body = await req.json()
@@ -340,7 +345,7 @@ async def send_email(req: Request):
340
  # ───────────────────────── startup ─────────────────────────
341
  def _boot():
342
  try:
343
- automation.start_scheduler()
344
  except Exception:
345
  pass
346
  if os.environ.get("AUTO_LOAD_MODEL", "1") == "1":
 
59
 
60
 
61
  # ───────────────────────── Signals ─────────────────────────
62
+ @app.get("/api/last-results")
63
+ async def last_results():
64
+ return JSONResponse(automation.load_results())
65
+
66
+
67
  @app.post("/api/signals/run")
68
  async def signals_run(req: Request):
69
  body = await req.json()
 
345
  # ───────────────────────── startup ─────────────────────────
346
  def _boot():
347
  try:
348
+ automation._SCHED = automation.start_scheduler()
349
  except Exception:
350
  pass
351
  if os.environ.get("AUTO_LOAD_MODEL", "1") == "1":
ui_kits/chan-compass/api.js CHANGED
@@ -39,6 +39,7 @@ window.CCApi = (function () {
39
  }
40
 
41
  return {
 
42
  runSignals: (pool, force) => jpost("/api/signals/run", { pool, force }),
43
  signalSummary: (ticker, onChunk) =>
44
  stream("/api/signals/summary?ticker=" + encodeURIComponent(ticker), onChunk),
 
39
  }
40
 
41
  return {
42
+ lastResults: () => jget("/api/last-results"),
43
  runSignals: (pool, force) => jpost("/api/signals/run", { pool, force }),
44
  signalSummary: (ticker, onChunk) =>
45
  stream("/api/signals/summary?ticker=" + encodeURIComponent(ticker), onChunk),
ui_kits/chan-compass/app.jsx CHANGED
@@ -77,10 +77,34 @@ class Boundary extends React.Component {
77
  function App() {
78
  const [tab, setTab] = useS('signals');
79
  const [mkt, setMkt] = useS({label:"…", variant:"neutral"});
 
80
  useE(()=>{
81
  const tick = ()=> window.CCApi && window.CCApi.marketStatus().then(setMkt).catch(()=>{});
82
  tick(); const id=setInterval(tick, 60000); return ()=>clearInterval(id);
83
  },[]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  const items = [
85
  { id:'signals', label:'Signals', icon:<Glyph name="trending-up" size={16}/> },
86
  { id:'rotation', label:'Sector Rotation', icon:<Glyph name="refresh-cw" size={16}/> },
@@ -116,7 +140,8 @@ function App() {
116
  </div>
117
 
118
  <main className="cc-main">
119
- <Boundary key={tab}>{View ? <View/> : null}</Boundary>
 
120
  </main>
121
 
122
  <footer className="cc-footer">
 
77
  function App() {
78
  const [tab, setTab] = useS('signals');
79
  const [mkt, setMkt] = useS({label:"…", variant:"neutral"});
80
+ const [seeded, setSeeded] = useS(false);
81
  useE(()=>{
82
  const tick = ()=> window.CCApi && window.CCApi.marketStatus().then(setMkt).catch(()=>{});
83
  tick(); const id=setInterval(tick, 60000); return ()=>clearInterval(id);
84
  },[]);
85
+ // On first load, seed the views with the LAST pipeline results from /data, so
86
+ // the app shows what was computed (e.g. by the 18:10 ET schedule) without a
87
+ // recompute. Only fills store keys that aren't already set this session.
88
+ useE(()=>{
89
+ if (!window.CCApi) return;
90
+ window.CCApi.lastResults().then(r=>{
91
+ const S = window.CCStore;
92
+ if (r && r.signals_rows && r.signals_rows.length) {
93
+ if (!("sig.rows" in S)) S["sig.rows"] = r.signals_rows;
94
+ if (!("sig.summary" in S)) S["sig.summary"] = r.signals_summary || "";
95
+ if (!("sig.sel" in S) && r.signals_rows[0]) S["sig.sel"] = r.signals_rows[0].Ticker;
96
+ }
97
+ if (r && r.rotation) {
98
+ if (!("rot.d1" in S)) S["rot.d1"] = r.rotation.d1 || [];
99
+ if (!("rot.d5" in S)) S["rot.d5"] = r.rotation.d5 || [];
100
+ if (!("rot.d20" in S)) S["rot.d20"] = r.rotation.d20 || [];
101
+ if (!("rot.asof" in S)) S["rot.asof"] = r.rotation.asof || "";
102
+ if (!("rot.ai" in S) && r.rotation_narrative) S["rot.ai"] = r.rotation_narrative;
103
+ }
104
+ if (r && r.news_md && !("news.out" in S)) S["news.out"] = r.news_md;
105
+ setSeeded(true);
106
+ }).catch(()=>setSeeded(true));
107
+ },[]);
108
  const items = [
109
  { id:'signals', label:'Signals', icon:<Glyph name="trending-up" size={16}/> },
110
  { id:'rotation', label:'Sector Rotation', icon:<Glyph name="refresh-cw" size={16}/> },
 
140
  </div>
141
 
142
  <main className="cc-main">
143
+ {seeded ? <Boundary key={tab}>{View ? <View/> : null}</Boundary>
144
+ : <div className="dim" style={{padding:24}}>Loading your latest results…</div>}
145
  </main>
146
 
147
  <footer className="cc-footer">
ui_kits/chan-compass/views.jsx CHANGED
@@ -149,8 +149,17 @@ function SignalsView() {
149
  <Kpi label="WAIT" value={counts.WAIT||0}/>
150
  </div>
151
  <Card title="Tomorrow's plan" subtitle="Long-hold mode - the unchanged Chan engine runs over each ticker">
152
- <div style={{display:"flex", gap:"var(--space-200)", alignItems:"flex-end", marginBottom:"6px"}}>
153
- <Field label="Ticker pool" mono value={pool} onChange={e=>setPool(e.target.value)} className="grow"/>
 
 
 
 
 
 
 
 
 
154
  <Checkbox checked={force} onChange={e=>setForce(e.target.checked)}>Force fresh download</Checkbox>
155
  <Button variant="accent" onClick={run} disabled={running}>
156
  {running ? <><Icon name="loader" size={15}/> Running...</> : <><Icon name="play" size={15}/> Run analysis</>}
 
149
  <Kpi label="WAIT" value={counts.WAIT||0}/>
150
  </div>
151
  <Card title="Tomorrow's plan" subtitle="Long-hold mode - the unchanged Chan engine runs over each ticker">
152
+ <div style={{display:"flex", gap:"var(--space-200)", alignItems:"flex-end", marginBottom:"var(--space-250)"}}>
153
+ <div className="grow" style={{display:"flex", flexDirection:"column", gap:4}}>
154
+ <label className="s2-field__label">Ticker pool</label>
155
+ <span className="dim" style={{fontSize:12}}>Comma separated</span>
156
+ <input className="s2-field__input" value={pool}
157
+ onChange={e=>setPool(e.target.value)}
158
+ style={{fontFamily:"var(--font-mono)", height:"var(--control-height-md, 36px)",
159
+ padding:"0 12px", border:"1px solid var(--border-field, #b1b1b1)",
160
+ borderRadius:"var(--field-radius, 8px)", background:"var(--surface-card, #fff)",
161
+ color:"var(--text-body, #292929)", fontSize:14, width:"100%", boxSizing:"border-box"}}/>
162
+ </div>
163
  <Checkbox checked={force} onChange={e=>setForce(e.target.checked)}>Force fresh download</Checkbox>
164
  <Button variant="accent" onClick={run} disabled={running}>
165
  {running ? <><Icon name="loader" size={15}/> Running...</> : <><Icon name="play" size={15}/> Run analysis</>}