ranranrunforit commited on
Commit
1c06038
·
verified ·
1 Parent(s): ba3fe9e

Upload 16 files

Browse files
Files changed (2) hide show
  1. app.py +54 -31
  2. signal_runner.py +55 -20
app.py CHANGED
@@ -52,30 +52,29 @@ def ui_run_signals(tickers_text, force):
52
 
53
  def ui_show_detail(ticker):
54
  if not ticker:
55
- return "Select a ticker after running the pipeline."
56
- return automation.STATE["signals_details"].get(ticker, "No detail for this ticker yet.")
 
 
 
57
 
58
 
59
  def ui_explain_detail(ticker):
60
- txt = automation.STATE["signals_details"].get(ticker or "", "")
61
- if not txt:
62
  yield "Run the analysis and select a ticker first."
63
  return
64
- # keep only the ruling chain the per-signal diagnostics block is huge and
65
- # would cost a minute of silent CPU prompt-processing for no benefit
66
- core = txt.split("日线买卖点逐项诊断")[0][:1500]
67
- prompt = ("Below is a Chan-theory multi-timeframe decision log in Chinese for a "
68
- "US stock. Respond in ENGLISH ONLY translate every Chinese term; no "
69
- "Chinese characters may appear in your answer. Do NOT quote the log. "
70
- "Give a SHORT summary in exactly this format:\n"
71
- "**Action:** <BUY/SELL/HOLD/WAIT + one clause>\n"
72
- "**Why:** <2-3 short bullets: which timeframe gates passed/failed>\n"
73
- "**Invalidation:** <one line: what price/event flips the call>\n"
74
- "Max 80 words total.\n\n" + core)
75
- yield ("🤖 _Translator sub-agent (Qwen3-1.7B · llama.cpp) is reading the "
76
- "decision log — first words in ~5-15s…_")
77
- for acc in llm_local.chat_stream(prompt, max_tokens=220, temperature=0.1, worker="translator"):
78
- yield "🤖 **Translator sub-agent (Qwen3-1.7B · llama.cpp):**\n\n" + acc
79
 
80
 
81
  def ui_refresh_rotation():
@@ -136,6 +135,26 @@ def ui_automation_panel():
136
  return automation.schedule_info(), "\n".join(automation.STATE["log"][-30:]) or "(no log yet)"
137
 
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  # ─────────────────────────────────────────── layout ──
140
  _GR_MAJOR = int(gr.__version__.split(".")[0])
141
  _style_kw = {} if _GR_MAJOR >= 6 else {"theme": theme, "css": S2_CSS}
@@ -165,13 +184,13 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
165
  sig_summary = gr.Markdown(automation.STATE["signals_summary"])
166
  sig_table = gr.Dataframe(label="Tomorrow's plan — long-hold mode (sorted: BUY → SELL → HOLD → WAIT)",
167
  interactive=False, wrap=True)
168
- gr.Markdown("**Decision log** — the engine's full multi-timeframe ruling chain "
169
- "(engine output is in Chinese; use the button for an English explanation).",
170
  elem_classes=["s2-footnote"])
171
  with gr.Row():
172
  detail_pick = gr.Dropdown(choices=[], label="Ticker", scale=2)
173
- explain_btn = gr.Button("🌐 Explain in English (local LLM)", scale=1)
174
- detail_box = gr.Textbox(lines=14, label="Ruling chain", elem_id="detail-log")
175
  explain_box = gr.Markdown(elem_classes=["ai-panel"])
176
 
177
  with gr.Tab("🔄 Sector Rotation"):
@@ -226,10 +245,13 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
226
  auto_md = gr.Markdown(automation.schedule_info())
227
  with gr.Row():
228
  auto_now = gr.Button("⚡ Run now", variant="primary")
229
- auto_refresh = gr.Button("↻ Refresh status")
230
  auto_msg = gr.Markdown()
231
- auto_log = gr.Textbox(lines=12, label="Pipeline log", elem_id="detail-log")
 
232
  traces_md = gr.Markdown(research_agent.list_traces())
 
 
 
233
 
234
  with gr.Tab("🧠 Model"):
235
  gr.Markdown("All AI runs **locally** through **llama.cpp** (llama-cpp-python) with "
@@ -245,9 +267,13 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
245
  value=llm_local.DEFAULT_MODEL, label="Analyst (deep) model")
246
  with gr.Row():
247
  load_btn = gr.Button("⬇ Load model", variant="primary")
248
- status_btn = gr.Button(" Refresh status")
249
- test_btn = gr.Button("⚡ Test sub-agents")
250
  model_status = gr.Markdown(llm_local.status())
 
 
 
 
 
251
 
252
  gr.Markdown("Chan Compass · educational tool, not investment advice · "
253
  "data: Yahoo Finance · design language: Adobe Spectrum 2",
@@ -265,11 +291,8 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
265
  res_btn.click(ui_research, res_in, [res_progress, res_out, rep_pick])
266
  rep_open.click(ui_open_report, rep_pick, rep_view)
267
  auto_now.click(lambda: automation.run_pipeline(force=True), None, auto_msg)
268
- auto_refresh.click(ui_automation_panel, None, [auto_md, auto_log])
269
- auto_refresh.click(research_agent.list_traces, None, traces_md)
270
  load_btn.click(ui_load_model, model_pick, model_status)
271
- status_btn.click(lambda: llm_local.status(), None, model_status)
272
- test_btn.click(lambda: llm_local.quick_test(), None, model_status)
273
 
274
  automation.start_scheduler()
275
 
 
52
 
53
  def ui_show_detail(ticker):
54
  if not ticker:
55
+ return "Select a ticker after running the analysis."
56
+ raw = signal_runner.stock_raw_read(ticker)
57
+ if not raw:
58
+ return "No data for this ticker yet — run the analysis first."
59
+ return f"**Raw read:**\n\n{raw}"
60
 
61
 
62
  def ui_explain_detail(ticker):
63
+ raw = signal_runner.stock_raw_read(ticker or "")
64
+ if not raw:
65
  yield "Run the analysis and select a ticker first."
66
  return
67
+ # Same pattern as Sector Rotation: feed the deterministic ENGLISH raw read
68
+ # to the agent no Chinese in, no Chinese out, fast (short prompt).
69
+ prompt = ("You are an equity analyst. Based ONLY on this factual read of a US "
70
+ "stock's multi-timeframe Chan-theory verdict, write a short plain-"
71
+ "English summary for a long-term holder: what's the situation today, "
72
+ "should they act or wait, and the key price levels. ≤90 words, no "
73
+ "disclaimers.\n\nRAW READ:\n" + raw)
74
+ yield "🤖 _Translator sub-agent (Qwen3-1.7B · llama.cpp) is summarizing…_"
75
+ for acc in llm_local.chat_stream(prompt, max_tokens=240, temperature=0.2,
76
+ worker="translator"):
77
+ yield "🤖 **AI narrative (Translator sub-agent · Qwen3-1.7B):**\n\n" + acc
 
 
 
 
78
 
79
 
80
  def ui_refresh_rotation():
 
135
  return automation.schedule_info(), "\n".join(automation.STATE["log"][-30:]) or "(no log yet)"
136
 
137
 
138
+ _SELFTEST = {"done": False, "result": ""}
139
+
140
+
141
+ def _auto_selftest():
142
+ """Runs once, automatically, the moment every sub-agent is loaded — shows a
143
+ self-test verdict without the user clicking anything."""
144
+ if _SELFTEST["done"]:
145
+ return _SELFTEST["result"]
146
+ workers = llm_local.WORKERS
147
+ if not all(w["llm"] is not None for w in workers.values()):
148
+ ready = sum(1 for w in workers.values() if w["llm"] is not None)
149
+ return f"⏳ Loading sub-agents… {ready}/{len(workers)} ready (self-test will run automatically)."
150
+ out = llm_local.quick_test()
151
+ ok = "not loaded" not in out and "error" not in out.lower()
152
+ _SELFTEST["done"] = True
153
+ _SELFTEST["result"] = (("✅ **Every agent is OK now** — self-test passed:\n\n" + out)
154
+ if ok else ("⚠️ Self-test finished with issues:\n\n" + out))
155
+ return _SELFTEST["result"]
156
+
157
+
158
  # ─────────────────────────────────────────── layout ──
159
  _GR_MAJOR = int(gr.__version__.split(".")[0])
160
  _style_kw = {} if _GR_MAJOR >= 6 else {"theme": theme, "css": S2_CSS}
 
184
  sig_summary = gr.Markdown(automation.STATE["signals_summary"])
185
  sig_table = gr.Dataframe(label="Tomorrow's plan — long-hold mode (sorted: BUY → SELL → HOLD → WAIT)",
186
  interactive=False, wrap=True)
187
+ gr.Markdown("**Stock summary** — pick a ticker for a plain-English raw read, "
188
+ "then let the Translator sub-agent write an AI narrative.",
189
  elem_classes=["s2-footnote"])
190
  with gr.Row():
191
  detail_pick = gr.Dropdown(choices=[], label="Ticker", scale=2)
192
+ explain_btn = gr.Button("🤖 AI summary (local LLM)", scale=1)
193
+ detail_box = gr.Markdown(label="Raw read")
194
  explain_box = gr.Markdown(elem_classes=["ai-panel"])
195
 
196
  with gr.Tab("🔄 Sector Rotation"):
 
245
  auto_md = gr.Markdown(automation.schedule_info())
246
  with gr.Row():
247
  auto_now = gr.Button("⚡ Run now", variant="primary")
 
248
  auto_msg = gr.Markdown()
249
+ auto_log = gr.Textbox(lines=14, label="Pipeline log (live — updates every 2s)",
250
+ elem_id="detail-log")
251
  traces_md = gr.Markdown(research_agent.list_traces())
252
+ auto_log_timer = gr.Timer(2.0)
253
+ auto_log_timer.tick(lambda: "\n".join(automation.STATE["log"][-40:])
254
+ or "(no log yet)", None, auto_log)
255
 
256
  with gr.Tab("🧠 Model"):
257
  gr.Markdown("All AI runs **locally** through **llama.cpp** (llama-cpp-python) with "
 
267
  value=llm_local.DEFAULT_MODEL, label="Analyst (deep) model")
268
  with gr.Row():
269
  load_btn = gr.Button("⬇ Load model", variant="primary")
270
+ test_btn = gr.Button(" Test sub-agents now")
 
271
  model_status = gr.Markdown(llm_local.status())
272
+ model_test_out = gr.Markdown()
273
+ model_timer = gr.Timer(2.0)
274
+ model_timer.tick(lambda: llm_local.status(), None, model_status)
275
+ autotest_timer = gr.Timer(3.0)
276
+ autotest_timer.tick(_auto_selftest, None, model_test_out)
277
 
278
  gr.Markdown("Chan Compass · educational tool, not investment advice · "
279
  "data: Yahoo Finance · design language: Adobe Spectrum 2",
 
291
  res_btn.click(ui_research, res_in, [res_progress, res_out, rep_pick])
292
  rep_open.click(ui_open_report, rep_pick, rep_view)
293
  auto_now.click(lambda: automation.run_pipeline(force=True), None, auto_msg)
 
 
294
  load_btn.click(ui_load_model, model_pick, model_status)
295
+ test_btn.click(lambda: llm_local.quick_test(), None, model_test_out)
 
296
 
297
  automation.start_scheduler()
298
 
signal_runner.py CHANGED
@@ -56,48 +56,54 @@ def _structural_stop(kind: str, res) -> float | None:
56
 
57
 
58
  def _next_day_plan(res) -> dict:
59
- """The simplified answer the user asked for:
60
- Do I buy/sell TOMORROW, in what price zone, and where is it wrong?"""
 
61
  kind, act = res.final_kind, res.action
62
  px = float(res.cur_price)
63
  if act == "BUY" and kind in ("B1", "B2", "B3"):
64
  stop = _structural_stop(kind, res)
65
- if kind == "B3" and res.daily and res.daily.zg:
66
- lo = max(stop, float(res.daily.zg))
67
- else:
68
- lo = stop
 
 
 
 
 
 
 
69
  hi = px * 1.015
70
  return {"plan": "🟢 BUY tomorrow at open",
71
- "zone": f"${lo:,.2f} – ${hi:,.2f}",
 
72
  "stop": f"${stop:,.2f}",
73
- "hint": "Long-hold entry: keep until S3 / stop / armed exit line."}
74
  if act == "SELL":
75
  hint = {"STOP": "Structural stop hit — exit to protect capital.",
76
  "S3": "Pivot breakdown (S3) — the long-hold exit signal. Exit, don't average down."}
77
- return {"plan": "🔴 SELL tomorrow at open",
78
- "zone": f"≈ ${px:,.2f}",
79
- "stop": "—",
80
  "hint": hint.get(kind, "Confirmed top (divergence verified at sub-levels) — take profit.")}
81
  if act == "HOLD":
82
  if res.sell_armed and res.arm_zd:
83
- return {"plan": "🟡 HOLD (exit line armed)",
84
- "zone": "—",
85
  "stop": f"${float(res.arm_zd):,.2f}",
86
  "hint": f"Keep holding; sell only if price closes below ${float(res.arm_zd):,.2f}."}
87
- return {"plan": "🟡 HOLD", "zone": "—", "stop": "—",
88
  "hint": "Trend intact — long-hold, ignore daily noise."}
89
- # WAIT — give an English reason (the engine's detailed log is Chinese; the
90
- # full text stays in the Decision log, the table stays 100% English)
91
  if kind:
92
- hint = (f"{kind} signal seen on the daily chart but NOT confirmed by "
93
- f"sub-level (30m) structure — wait for confirmation, don't chase.")
94
  elif res.blocked_reason:
95
  hint = "Signal blocked by a higher-timeframe gate (weekly/monthly direction). Stay out."
96
  else:
97
  wk = TREND_EN.get(res.weekly.trend if res.weekly else "", "?")
98
  dy = TREND_EN.get(res.daily.trend if res.daily else "", "?")
99
  hint = f"No buy/sell point today (weekly {wk}, daily {dy}). Stay in cash / keep watching."
100
- return {"plan": "⚪ WAIT", "zone": "—", "stop": "—", "hint": hint}
101
 
102
  import paths
103
 
@@ -118,6 +124,34 @@ TREND_EN = {"up_trend": "Up", "down_trend": "Down", "consolidation": "Range",
118
  "expanding": "Expanding", "unknown": "?", "": "?"}
119
 
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  def analyze_one(ticker: str, force: bool = False):
122
  """Run the simplified long-hold Chan analysis for one ticker.
123
  Full nested-interval set: monthly/weekly (resampled) + daily +
@@ -147,6 +181,7 @@ def analyze_one(ticker: str, force: bool = False):
147
  row = {
148
  "Ticker": ticker,
149
  "Tomorrow": plan["plan"],
 
150
  "Buy zone": plan["zone"],
151
  "Invalid below": plan["stop"],
152
  "Signal": KIND_EN.get(res.final_kind, res.final_kind or "—"),
@@ -198,7 +233,7 @@ def run_signals(tickers=None, force: bool = False):
198
  except Exception:
199
  pass
200
  else:
201
- show = pd.DataFrame(columns=["Ticker", "Tomorrow", "Buy zone", "Invalid below", "Signal", "Confidence", "Close"])
202
  n_buy = sum(1 for r in rows if r["_action_raw"] == "BUY")
203
  n_sell = sum(1 for r in rows if r["_action_raw"] == "SELL")
204
  asof = rows[0]["_date"] if rows else "—"
 
56
 
57
 
58
  def _next_day_plan(res) -> dict:
59
+ """The simplified answer: do I buy/sell tomorrow, the exact BUY POINT, the
60
+ acceptable entry zone, and the invalidation price. All prices come straight
61
+ from the engine's signal.extras (same source as the user's backtest)."""
62
  kind, act = res.final_kind, res.action
63
  px = float(res.cur_price)
64
  if act == "BUY" and kind in ("B1", "B2", "B3"):
65
  stop = _structural_stop(kind, res)
66
+ # The BUY POINT = the engine's structural level for this signal type:
67
+ # B1 divergence low · B2 retest low · B3 pivot upper band (ZG).
68
+ sig = res.daily.signal if res.daily else None
69
+ ex = (sig.extras if sig else None) or {}
70
+ if kind == "B3":
71
+ point = float(res.daily.zg) if (res.daily and res.daily.zg) else stop
72
+ elif kind == "B1":
73
+ point = float(ex.get("c_new_low") or stop)
74
+ else: # B2
75
+ point = float(ex.get("cur_low") or ex.get("b1_price") or stop)
76
+ lo = min(point, stop) if kind == "B3" else point
77
  hi = px * 1.015
78
  return {"plan": "🟢 BUY tomorrow at open",
79
+ "point": f"${point:,.2f}",
80
+ "zone": f"${min(lo, hi):,.2f} – ${hi:,.2f}",
81
  "stop": f"${stop:,.2f}",
82
+ "hint": f"Long-hold entry near ${point:,.2f}; keep until S3 / stop / armed exit."}
83
  if act == "SELL":
84
  hint = {"STOP": "Structural stop hit — exit to protect capital.",
85
  "S3": "Pivot breakdown (S3) — the long-hold exit signal. Exit, don't average down."}
86
+ return {"plan": "🔴 SELL tomorrow at open", "point": f"≈ ${px:,.2f}",
87
+ "zone": f"≈ ${px:,.2f}", "stop": "—",
 
88
  "hint": hint.get(kind, "Confirmed top (divergence verified at sub-levels) — take profit.")}
89
  if act == "HOLD":
90
  if res.sell_armed and res.arm_zd:
91
+ return {"plan": "🟡 HOLD (exit line armed)", "point": "—", "zone": "—",
 
92
  "stop": f"${float(res.arm_zd):,.2f}",
93
  "hint": f"Keep holding; sell only if price closes below ${float(res.arm_zd):,.2f}."}
94
+ return {"plan": "🟡 HOLD", "point": "—", "zone": "—", "stop": "—",
95
  "hint": "Trend intact — long-hold, ignore daily noise."}
96
+ # WAIT
 
97
  if kind:
98
+ hint = (f"{kind} signal on the daily chart but NOT yet confirmed down the "
99
+ f"nested sub-levels (60m→30m→15m→5m) — wait, don't chase.")
100
  elif res.blocked_reason:
101
  hint = "Signal blocked by a higher-timeframe gate (weekly/monthly direction). Stay out."
102
  else:
103
  wk = TREND_EN.get(res.weekly.trend if res.weekly else "", "?")
104
  dy = TREND_EN.get(res.daily.trend if res.daily else "", "?")
105
  hint = f"No buy/sell point today (weekly {wk}, daily {dy}). Stay in cash / keep watching."
106
+ return {"plan": "⚪ WAIT", "point": "—", "zone": "—", "stop": "—", "hint": hint}
107
 
108
  import paths
109
 
 
124
  "expanding": "Expanding", "unknown": "?", "": "?"}
125
 
126
 
127
+ def stock_raw_read(ticker: str) -> str:
128
+ """Plain-English factual snapshot of one ticker's Chan verdict — the
129
+ deterministic 'Raw read' the Signals AI narrative summarizes (mirrors the
130
+ Sector-Rotation pattern: raw read → agent → narrative)."""
131
+ row = automation_state_row(ticker)
132
+ if not row:
133
+ return ""
134
+ parts = [
135
+ f"{ticker}: close {row['Close']}, signal {row['Signal']}, "
136
+ f"confidence {row['Confidence']}.",
137
+ f"Plan tomorrow: {row['Tomorrow']}.",
138
+ ]
139
+ if row.get("Buy point") not in ("—", None):
140
+ parts.append(f"Buy point {row['Buy point']}, zone {row['Buy zone']}, "
141
+ f"invalid below {row['Invalid below']}.")
142
+ parts.append(f"Note: {row['Note']}")
143
+ return " ".join(parts)
144
+
145
+
146
+ def automation_state_row(ticker: str):
147
+ import automation
148
+ df = automation.STATE.get("signals_df")
149
+ if df is None or "Ticker" not in getattr(df, "columns", []):
150
+ return None
151
+ m = df[df["Ticker"] == ticker]
152
+ return m.iloc[0].to_dict() if len(m) else None
153
+
154
+
155
  def analyze_one(ticker: str, force: bool = False):
156
  """Run the simplified long-hold Chan analysis for one ticker.
157
  Full nested-interval set: monthly/weekly (resampled) + daily +
 
181
  row = {
182
  "Ticker": ticker,
183
  "Tomorrow": plan["plan"],
184
+ "Buy point": plan["point"],
185
  "Buy zone": plan["zone"],
186
  "Invalid below": plan["stop"],
187
  "Signal": KIND_EN.get(res.final_kind, res.final_kind or "—"),
 
233
  except Exception:
234
  pass
235
  else:
236
+ show = pd.DataFrame(columns=["Ticker", "Tomorrow", "Buy point", "Buy zone", "Invalid below", "Signal", "Confidence", "Close"])
237
  n_buy = sum(1 for r in rows if r["_action_raw"] == "BUY")
238
  n_sell = sum(1 for r in rows if r["_action_raw"] == "SELL")
239
  asof = rows[0]["_date"] if rows else "—"