Spaces:
Runtime error
Runtime error
Upload 16 files
Browse files- app.py +54 -31
- 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
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
|
| 59 |
def ui_explain_detail(ticker):
|
| 60 |
-
|
| 61 |
-
if not
|
| 62 |
yield "Run the analysis and select a ticker first."
|
| 63 |
return
|
| 64 |
-
#
|
| 65 |
-
#
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
"
|
| 69 |
-
"
|
| 70 |
-
"
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 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("**
|
| 169 |
-
"
|
| 170 |
elem_classes=["s2-footnote"])
|
| 171 |
with gr.Row():
|
| 172 |
detail_pick = gr.Dropdown(choices=[], label="Ticker", scale=2)
|
| 173 |
-
explain_btn = gr.Button("
|
| 174 |
-
detail_box = gr.
|
| 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=
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 60 |
-
|
|
|
|
| 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 |
-
|
| 66 |
-
|
| 67 |
-
else
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
hi = px * 1.015
|
| 70 |
return {"plan": "🟢 BUY tomorrow at open",
|
| 71 |
-
"
|
|
|
|
| 72 |
"stop": f"${stop:,.2f}",
|
| 73 |
-
"hint": "Long-hold entry: keep until S3 / stop / armed exit
|
| 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
|
| 90 |
-
# full text stays in the Decision log, the table stays 100% English)
|
| 91 |
if kind:
|
| 92 |
-
hint = (f"{kind} signal
|
| 93 |
-
f"sub-
|
| 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 "—"
|