Spaces:
Runtime error
Runtime error
Upload 13 files
Browse files- app.py +4 -2
- llm_local.py +49 -0
- requirements.txt +1 -2
- signal_runner.py +73 -8
app.py
CHANGED
|
@@ -177,7 +177,7 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
|
|
| 177 |
force_cb = gr.Checkbox(value=True, label="Force fresh download", scale=1)
|
| 178 |
run_btn = gr.Button("▶ Run analysis", variant="primary", scale=1)
|
| 179 |
sig_summary = gr.Markdown(automation.STATE["signals_summary"])
|
| 180 |
-
sig_table = gr.Dataframe(label="
|
| 181 |
interactive=False, wrap=True)
|
| 182 |
gr.Markdown("**Decision log** — the engine's full multi-timeframe ruling chain "
|
| 183 |
"(engine output is in Chinese; use the button for an English explanation).",
|
|
@@ -234,7 +234,9 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
|
|
| 234 |
with gr.Tab("🧠 Model"):
|
| 235 |
gr.Markdown("All AI runs **locally** through **llama.cpp** (llama-cpp-python) with "
|
| 236 |
"Qwen3 GGUF weights — every option is far below the 32B-parameter cap, "
|
| 237 |
-
"and nothing leaves the machine. First load
|
|
|
|
|
|
|
| 238 |
model_pick = gr.Radio(choices=list(llm_local.MODEL_ZOO.keys()),
|
| 239 |
value=llm_local.DEFAULT_MODEL, label="Model")
|
| 240 |
load_btn = gr.Button("⬇ Load model", variant="primary")
|
|
|
|
| 177 |
force_cb = gr.Checkbox(value=True, label="Force fresh download", scale=1)
|
| 178 |
run_btn = gr.Button("▶ Run analysis", variant="primary", scale=1)
|
| 179 |
sig_summary = gr.Markdown(automation.STATE["signals_summary"])
|
| 180 |
+
sig_table = gr.Dataframe(label="Tomorrow's plan — long-hold mode (sorted: BUY → SELL → HOLD → WAIT)",
|
| 181 |
interactive=False, wrap=True)
|
| 182 |
gr.Markdown("**Decision log** — the engine's full multi-timeframe ruling chain "
|
| 183 |
"(engine output is in Chinese; use the button for an English explanation).",
|
|
|
|
| 234 |
with gr.Tab("🧠 Model"):
|
| 235 |
gr.Markdown("All AI runs **locally** through **llama.cpp** (llama-cpp-python) with "
|
| 236 |
"Qwen3 GGUF weights — every option is far below the 32B-parameter cap, "
|
| 237 |
+
"and nothing leaves the machine. **First load installs the llama.cpp "
|
| 238 |
+
"runtime + downloads the GGUF (one-time, usually 1–3 min; worst case "
|
| 239 |
+
"~15 min if it has to compile).** Signals/rotation/news never depend on it.")
|
| 240 |
model_pick = gr.Radio(choices=list(llm_local.MODEL_ZOO.keys()),
|
| 241 |
value=llm_local.DEFAULT_MODEL, label="Model")
|
| 242 |
load_btn = gr.Button("⬇ Load model", variant="primary")
|
llm_local.py
CHANGED
|
@@ -35,6 +35,52 @@ _loaded_name = None
|
|
| 35 |
|
| 36 |
_THINK_RE = re.compile(r"<think>.*?</think>", re.S)
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
def status() -> str:
|
| 40 |
if _llm is None:
|
|
@@ -53,6 +99,9 @@ def load_model(name: str) -> str:
|
|
| 53 |
with _lock:
|
| 54 |
if _loaded_name == name and _llm is not None:
|
| 55 |
return f"Already loaded: {name}"
|
|
|
|
|
|
|
|
|
|
| 56 |
try:
|
| 57 |
from llama_cpp import Llama
|
| 58 |
except Exception as e: # llama-cpp-python missing / failed to build
|
|
|
|
| 35 |
|
| 36 |
_THINK_RE = re.compile(r"<think>.*?</think>", re.S)
|
| 37 |
|
| 38 |
+
# llama-cpp-python is installed at RUNTIME, not at Space build time.
|
| 39 |
+
# Why: the HF build container has little RAM and gets OOM-killed compiling the
|
| 40 |
+
# C++ extension; the runtime container has the real hardware (8 vCPU / 32 GB).
|
| 41 |
+
# We try the official prebuilt CPU wheel first (seconds), and only compile from
|
| 42 |
+
# source as a fallback — with capped parallelism so memory stays bounded.
|
| 43 |
+
_WHEEL_INDEX = "https://abetlen.github.io/llama-cpp-python/whl/cpu"
|
| 44 |
+
_LLAMA_REQ = "llama-cpp-python>=0.3.8" # >=0.3.8 → Qwen3 architecture support
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _ensure_llama_cpp() -> str:
|
| 48 |
+
"""Install llama-cpp-python on first use. Returns '' on success, else error."""
|
| 49 |
+
try:
|
| 50 |
+
import llama_cpp # noqa: F401
|
| 51 |
+
return ""
|
| 52 |
+
except ImportError:
|
| 53 |
+
pass
|
| 54 |
+
import subprocess
|
| 55 |
+
import sys
|
| 56 |
+
env = dict(os.environ)
|
| 57 |
+
env["CMAKE_BUILD_PARALLEL_LEVEL"] = "4" # bound memory if a compile happens
|
| 58 |
+
base = [sys.executable, "-m", "pip", "install", "--user", "--prefer-binary"]
|
| 59 |
+
# 1) prebuilt CPU wheel from the official index (fast path)
|
| 60 |
+
r = subprocess.run(base + ["--extra-index-url", _WHEEL_INDEX,
|
| 61 |
+
"--only-binary", "llama-cpp-python", _LLAMA_REQ],
|
| 62 |
+
capture_output=True, text=True, env=env, timeout=600)
|
| 63 |
+
if r.returncode != 0:
|
| 64 |
+
# 2) fallback: allow source build (runtime box has plenty of RAM)
|
| 65 |
+
r = subprocess.run(base + ["--extra-index-url", _WHEEL_INDEX, _LLAMA_REQ],
|
| 66 |
+
capture_output=True, text=True, env=env, timeout=2400)
|
| 67 |
+
if r.returncode != 0:
|
| 68 |
+
return ("Could not install llama-cpp-python at runtime:\n"
|
| 69 |
+
+ (r.stderr or r.stdout or "")[-800:])
|
| 70 |
+
# make the freshly installed --user package importable in this process
|
| 71 |
+
import importlib
|
| 72 |
+
import site
|
| 73 |
+
for p in site.getusersitepackages() if isinstance(site.getusersitepackages(), list) \
|
| 74 |
+
else [site.getusersitepackages()]:
|
| 75 |
+
if p not in sys.path:
|
| 76 |
+
sys.path.insert(0, p)
|
| 77 |
+
importlib.invalidate_caches()
|
| 78 |
+
try:
|
| 79 |
+
import llama_cpp # noqa: F401
|
| 80 |
+
return ""
|
| 81 |
+
except Exception as e:
|
| 82 |
+
return f"Installed but import failed: {e}"
|
| 83 |
+
|
| 84 |
|
| 85 |
def status() -> str:
|
| 86 |
if _llm is None:
|
|
|
|
| 99 |
with _lock:
|
| 100 |
if _loaded_name == name and _llm is not None:
|
| 101 |
return f"Already loaded: {name}"
|
| 102 |
+
err = _ensure_llama_cpp()
|
| 103 |
+
if err:
|
| 104 |
+
return err
|
| 105 |
try:
|
| 106 |
from llama_cpp import Llama
|
| 107 |
except Exception as e: # llama-cpp-python missing / failed to build
|
requirements.txt
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
-
gradio>=
|
| 2 |
pandas>=2.0
|
| 3 |
numpy>=1.24
|
| 4 |
pyarrow>=14
|
| 5 |
yfinance>=0.2.40
|
| 6 |
apscheduler>=3.10
|
| 7 |
huggingface_hub>=0.23
|
| 8 |
-
llama-cpp-python>=0.2.90 --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
|
|
|
|
| 1 |
+
gradio>=5.49
|
| 2 |
pandas>=2.0
|
| 3 |
numpy>=1.24
|
| 4 |
pyarrow>=14
|
| 5 |
yfinance>=0.2.40
|
| 6 |
apscheduler>=3.10
|
| 7 |
huggingface_hub>=0.23
|
|
|
signal_runner.py
CHANGED
|
@@ -21,6 +21,72 @@ import data_us
|
|
| 21 |
|
| 22 |
DEFAULT_POOL = ["AAPL", "MSFT", "NVDA", "TSLA", "AMZN", "GOOGL", "META", "AMD", "NFLX", "JPM"]
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
OUT_DIR = "./_app_output"
|
| 25 |
os.makedirs(OUT_DIR, exist_ok=True)
|
| 26 |
|
|
@@ -61,18 +127,17 @@ def analyze_one(ticker: str, force: bool = False):
|
|
| 61 |
|
| 62 |
enh = chan_enhance.predict_enhance(res)
|
| 63 |
weight = enh.get("suggest_weight")
|
|
|
|
| 64 |
row = {
|
| 65 |
"Ticker": ticker,
|
| 66 |
-
"
|
|
|
|
|
|
|
| 67 |
"Signal": KIND_EN.get(res.final_kind, res.final_kind or "—"),
|
| 68 |
"Confidence": res.confidence,
|
| 69 |
"Close": f"${res.cur_price:,.2f}",
|
| 70 |
-
"
|
| 71 |
-
"
|
| 72 |
-
"Sell-trap armed": "Yes" if res.sell_armed else "—",
|
| 73 |
-
"Arm line": (f"${res.arm_zd:,.2f}" if res.arm_zd else "—"),
|
| 74 |
-
"Suggested weight": (f"{weight:.2f}" if weight else "—"),
|
| 75 |
-
"Note": (res.note or res.blocked_reason or "")[:160],
|
| 76 |
"_action_raw": res.action,
|
| 77 |
"_kind_raw": res.final_kind,
|
| 78 |
"_date": res.analysis_date.strftime("%Y-%m-%d"),
|
|
@@ -113,7 +178,7 @@ def run_signals(tickers=None, force: bool = False):
|
|
| 113 |
except Exception:
|
| 114 |
pass
|
| 115 |
else:
|
| 116 |
-
show = pd.DataFrame(columns=["Ticker", "
|
| 117 |
n_buy = sum(1 for r in rows if r["_action_raw"] == "BUY")
|
| 118 |
n_sell = sum(1 for r in rows if r["_action_raw"] == "SELL")
|
| 119 |
asof = rows[0]["_date"] if rows else "—"
|
|
|
|
| 21 |
|
| 22 |
DEFAULT_POOL = ["AAPL", "MSFT", "NVDA", "TSLA", "AMZN", "GOOGL", "META", "AMD", "NFLX", "JPM"]
|
| 23 |
|
| 24 |
+
# ── LONG-HOLD mode (user requirement for the US version) ────────────────
|
| 25 |
+
# Operating level = weekly: ride the pivot uplift, don't get shaken out early.
|
| 26 |
+
# mode='long' → daily S1/S2 in a big uptrend → HOLD (armed)
|
| 27 |
+
# require_sublevel_sell_confirm → unconfirmed daily sells in an uptrend → HOLD
|
| 28 |
+
# Real exits that still fire: S3 (pivot breakdown), structural stop,
|
| 29 |
+
# and the armed exit line once the nested-interval top is confirmed.
|
| 30 |
+
MultiLevelChan.CFG["mode"] = "long"
|
| 31 |
+
MultiLevelChan.CFG["require_sublevel_sell_confirm"] = True
|
| 32 |
+
|
| 33 |
+
STOP_MAX_LOSS = 0.05 # same global loss cap as the user's backtest
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _structural_stop(kind: str, res) -> float | None:
|
| 37 |
+
"""Simplified invalidation price, lifted from the user's backtest logic:
|
| 38 |
+
B1 → divergence low; B2 → retest low / B1 anchor; B3 → daily pivot ZD.
|
| 39 |
+
Capped so a single position can never lose much more than STOP_MAX_LOSS."""
|
| 40 |
+
sig = res.daily.signal if (res and res.daily) else None
|
| 41 |
+
ex = (sig.extras if sig is not None else None) or {}
|
| 42 |
+
close_p = float(res.cur_price)
|
| 43 |
+
stop = None
|
| 44 |
+
if kind == "B1":
|
| 45 |
+
stop = ex.get("c_new_low") or ex.get("b1_price")
|
| 46 |
+
elif kind == "B2":
|
| 47 |
+
stop = ex.get("cur_low") or ex.get("b1_price")
|
| 48 |
+
elif kind == "B3":
|
| 49 |
+
stop = res.daily.zd if (res.daily and res.daily.zd) else ex.get("pull_low")
|
| 50 |
+
if stop is None:
|
| 51 |
+
stop = close_p * (1 - STOP_MAX_LOSS)
|
| 52 |
+
stop = max(min(float(stop), close_p * 0.999), close_p * (1 - STOP_MAX_LOSS))
|
| 53 |
+
return round(stop, 2)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _next_day_plan(res) -> dict:
|
| 57 |
+
"""The simplified answer the user asked for:
|
| 58 |
+
Do I buy/sell TOMORROW, in what price zone, and where is it wrong?"""
|
| 59 |
+
kind, act = res.final_kind, res.action
|
| 60 |
+
px = float(res.cur_price)
|
| 61 |
+
if act == "BUY" and kind in ("B1", "B2", "B3"):
|
| 62 |
+
stop = _structural_stop(kind, res)
|
| 63 |
+
if kind == "B3" and res.daily and res.daily.zg:
|
| 64 |
+
lo = max(stop, float(res.daily.zg))
|
| 65 |
+
else:
|
| 66 |
+
lo = stop
|
| 67 |
+
hi = px * 1.015
|
| 68 |
+
return {"plan": "🟢 BUY tomorrow at open",
|
| 69 |
+
"zone": f"${lo:,.2f} – ${hi:,.2f}",
|
| 70 |
+
"stop": f"${stop:,.2f}",
|
| 71 |
+
"hint": "Long-hold entry: keep until S3 / stop / armed exit line."}
|
| 72 |
+
if act == "SELL":
|
| 73 |
+
hint = {"STOP": "Structural stop hit — exit to protect capital.",
|
| 74 |
+
"S3": "Pivot breakdown (S3) — the long-hold exit signal. Exit, don't average down."}
|
| 75 |
+
return {"plan": "🔴 SELL tomorrow at open",
|
| 76 |
+
"zone": f"≈ ${px:,.2f}",
|
| 77 |
+
"stop": "—",
|
| 78 |
+
"hint": hint.get(kind, "Confirmed top (divergence verified at sub-levels) — take profit.")}
|
| 79 |
+
if act == "HOLD":
|
| 80 |
+
if res.sell_armed and res.arm_zd:
|
| 81 |
+
return {"plan": "🟡 HOLD (exit line armed)",
|
| 82 |
+
"zone": "—",
|
| 83 |
+
"stop": f"${float(res.arm_zd):,.2f}",
|
| 84 |
+
"hint": f"Keep holding; sell only if price closes below ${float(res.arm_zd):,.2f}."}
|
| 85 |
+
return {"plan": "🟡 HOLD", "zone": "—", "stop": "—",
|
| 86 |
+
"hint": "Trend intact — long-hold, ignore daily noise."}
|
| 87 |
+
return {"plan": "⚪ WAIT", "zone": "—", "stop": "—",
|
| 88 |
+
"hint": (res.blocked_reason or res.note or "No actionable signal.")[:110]}
|
| 89 |
+
|
| 90 |
OUT_DIR = "./_app_output"
|
| 91 |
os.makedirs(OUT_DIR, exist_ok=True)
|
| 92 |
|
|
|
|
| 127 |
|
| 128 |
enh = chan_enhance.predict_enhance(res)
|
| 129 |
weight = enh.get("suggest_weight")
|
| 130 |
+
plan = _next_day_plan(res)
|
| 131 |
row = {
|
| 132 |
"Ticker": ticker,
|
| 133 |
+
"Tomorrow": plan["plan"],
|
| 134 |
+
"Buy zone": plan["zone"],
|
| 135 |
+
"Invalid below": plan["stop"],
|
| 136 |
"Signal": KIND_EN.get(res.final_kind, res.final_kind or "—"),
|
| 137 |
"Confidence": res.confidence,
|
| 138 |
"Close": f"${res.cur_price:,.2f}",
|
| 139 |
+
"Weight": (f"{weight:.2f}" if weight else "—"),
|
| 140 |
+
"Note": plan["hint"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
"_action_raw": res.action,
|
| 142 |
"_kind_raw": res.final_kind,
|
| 143 |
"_date": res.analysis_date.strftime("%Y-%m-%d"),
|
|
|
|
| 178 |
except Exception:
|
| 179 |
pass
|
| 180 |
else:
|
| 181 |
+
show = pd.DataFrame(columns=["Ticker", "Tomorrow", "Buy zone", "Invalid below", "Signal", "Confidence", "Close"])
|
| 182 |
n_buy = sum(1 for r in rows if r["_action_raw"] == "BUY")
|
| 183 |
n_sell = sum(1 for r in rows if r["_action_raw"] == "SELL")
|
| 184 |
asof = rows[0]["_date"] if rows else "—"
|