ranranrunforit commited on
Commit
5f60a5e
·
verified ·
1 Parent(s): 46b9138

Upload 13 files

Browse files
Files changed (4) hide show
  1. app.py +4 -2
  2. llm_local.py +49 -0
  3. requirements.txt +1 -2
  4. 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="Next-session plan (sorted: BUY → SELL → HOLD → WATCH)",
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 downloads the GGUF once.")
 
 
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>=4.44
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
- "Action": ACTION_BADGE.get(res.action, res.action),
 
 
67
  "Signal": KIND_EN.get(res.final_kind, res.final_kind or "—"),
68
  "Confidence": res.confidence,
69
  "Close": f"${res.cur_price:,.2f}",
70
- "Weekly": TREND_EN.get(res.weekly.trend if res.weekly else "", "?"),
71
- "Daily": TREND_EN.get(res.daily.trend if res.daily else "", "?"),
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", "Action", "Signal", "Confidence", "Close"])
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 "—"