Spaces:
Runtime error
Runtime error
Upload 18 files
Browse files- app.py +77 -13
- llm_local.py +5 -3
app.py
CHANGED
|
@@ -11,6 +11,7 @@ github.com/adobe/react-spectrum), approximated in Gradio CSS:
|
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
import os
|
|
|
|
| 14 |
import threading
|
| 15 |
|
| 16 |
import gradio as gr
|
|
@@ -44,6 +45,11 @@ def ui_run_signals(tickers_text, force):
|
|
| 44 |
automation.STATE["signals_df"] = df
|
| 45 |
automation.STATE["signals_details"] = details
|
| 46 |
automation.STATE["signals_summary"] = summary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
choices = sorted(details.keys())
|
| 48 |
return (
|
| 49 |
df if df is not None else pd.DataFrame(),
|
|
@@ -52,6 +58,50 @@ def ui_run_signals(tickers_text, force):
|
|
| 52 |
)
|
| 53 |
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
def ui_show_detail(ticker):
|
| 56 |
if not ticker:
|
| 57 |
return "Select a ticker after running the analysis."
|
|
@@ -66,28 +116,42 @@ def ui_explain_detail(ticker):
|
|
| 66 |
if not raw:
|
| 67 |
yield "Run the analysis and select a ticker first."
|
| 68 |
return
|
| 69 |
-
#
|
| 70 |
-
#
|
| 71 |
-
#
|
| 72 |
-
#
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
| 75 |
prompt = ("You are an equity analyst. Write a SHORT plain-English summary "
|
| 76 |
"(≤100 words) for a long-term holder of a US stock: the situation "
|
| 77 |
"today, whether to act or wait, and the key price levels.\n"
|
| 78 |
-
"
|
| 79 |
-
"
|
| 80 |
-
"
|
| 81 |
-
"characters, do not quote the log, no disclaimers.\n\n"
|
| 82 |
f"FACT LINE:\n{raw}")
|
| 83 |
-
if
|
| 84 |
-
prompt += f"\n\
|
| 85 |
yield "🤖 _Translator sub-agent (Qwen3-1.7B · llama.cpp) is summarizing…_"
|
| 86 |
final = ""
|
| 87 |
-
for acc in llm_local.chat_stream(prompt, max_tokens=
|
| 88 |
worker="translator"):
|
| 89 |
final = acc
|
| 90 |
yield "🤖 **AI narrative (Translator sub-agent · Qwen3-1.7B):**\n\n" + acc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
# capture (raw read → narrative) as a fine-tuning pair (🎯 Well-Tuned)
|
| 92 |
try:
|
| 93 |
import finetune_data
|
|
|
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
import os
|
| 14 |
+
import re
|
| 15 |
import threading
|
| 16 |
|
| 17 |
import gradio as gr
|
|
|
|
| 45 |
automation.STATE["signals_df"] = df
|
| 46 |
automation.STATE["signals_details"] = details
|
| 47 |
automation.STATE["signals_summary"] = summary
|
| 48 |
+
# Pre-translate every ruling chain to English in the BACKGROUND on the
|
| 49 |
+
# dedicated rchain sub-agent, so when the user clicks AI summary the English
|
| 50 |
+
# chain is already cached → faster, and the merge step gets English-only
|
| 51 |
+
# input (no Chinese can leak into the output).
|
| 52 |
+
_kick_chain_translation(details)
|
| 53 |
choices = sorted(details.keys())
|
| 54 |
return (
|
| 55 |
df if df is not None else pd.DataFrame(),
|
|
|
|
| 58 |
)
|
| 59 |
|
| 60 |
|
| 61 |
+
def _chain_core(ticker: str) -> str:
|
| 62 |
+
"""The ruling chain minus the huge per-signal diagnostics block."""
|
| 63 |
+
chain = automation.STATE.get("signals_details", {}).get(ticker, "")
|
| 64 |
+
return chain.split("日线买卖点逐项诊断")[0].strip()[:2000] if chain else ""
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _translate_chain(ticker: str) -> str:
|
| 68 |
+
"""Translate one ruling chain to English on the dedicated rchain sub-agent;
|
| 69 |
+
cache the result. Returns the English text (or '' if no model/chain)."""
|
| 70 |
+
cache = automation.STATE.setdefault("chain_en", {})
|
| 71 |
+
if ticker in cache:
|
| 72 |
+
return cache[ticker]
|
| 73 |
+
core = _chain_core(ticker)
|
| 74 |
+
if not core:
|
| 75 |
+
return ""
|
| 76 |
+
if not (llm_local.is_loaded("rchain") or llm_local.is_loaded("translator")):
|
| 77 |
+
return ""
|
| 78 |
+
wk = "rchain" if llm_local.is_loaded("rchain") else "translator"
|
| 79 |
+
prompt = ("Translate this Chinese multi-timeframe Chan-theory decision log "
|
| 80 |
+
"into concise English. Keep the structure (one line per level: "
|
| 81 |
+
"yearly / monthly / weekly / daily / 60m / 30m …). Output ENGLISH "
|
| 82 |
+
"ONLY, no Chinese characters, no commentary.\n\n" + core)
|
| 83 |
+
en = llm_local.chat(prompt, max_tokens=380, temperature=0.1, worker=wk)
|
| 84 |
+
if en and not en.startswith(("(", "⏳")):
|
| 85 |
+
cache[ticker] = en
|
| 86 |
+
return en
|
| 87 |
+
return ""
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _kick_chain_translation(details: dict):
|
| 91 |
+
"""Background: translate all ruling chains to English right after analysis,
|
| 92 |
+
so AI summary is instant and English-only later."""
|
| 93 |
+
import threading
|
| 94 |
+
automation.STATE["chain_en"] = {} # reset for the new run
|
| 95 |
+
|
| 96 |
+
def _run():
|
| 97 |
+
for tk in list(details.keys()):
|
| 98 |
+
try:
|
| 99 |
+
_translate_chain(tk)
|
| 100 |
+
except Exception:
|
| 101 |
+
pass
|
| 102 |
+
threading.Thread(target=_run, daemon=True).start()
|
| 103 |
+
|
| 104 |
+
|
| 105 |
def ui_show_detail(ticker):
|
| 106 |
if not ticker:
|
| 107 |
return "Select a ticker after running the analysis."
|
|
|
|
| 116 |
if not raw:
|
| 117 |
yield "Run the analysis and select a ticker first."
|
| 118 |
return
|
| 119 |
+
# Use the pre-translated English ruling chain (translated in the background
|
| 120 |
+
# right after Run analysis). If it isn't ready yet, translate it now on the
|
| 121 |
+
# rchain sub-agent. Both the fact line AND the chain are now English, so the
|
| 122 |
+
# merge step cannot leak Chinese into the output.
|
| 123 |
+
chain_en = automation.STATE.get("chain_en", {}).get(ticker or "")
|
| 124 |
+
if not chain_en:
|
| 125 |
+
yield "🤖 _Translating the multi-timeframe ruling chain to English…_"
|
| 126 |
+
chain_en = _translate_chain(ticker or "")
|
| 127 |
prompt = ("You are an equity analyst. Write a SHORT plain-English summary "
|
| 128 |
"(≤100 words) for a long-term holder of a US stock: the situation "
|
| 129 |
"today, whether to act or wait, and the key price levels.\n"
|
| 130 |
+
"Base it on the FACT LINE (numbers) and the REASONING (an English "
|
| 131 |
+
"translation of the multi-timeframe Chan-theory verdict). Output "
|
| 132 |
+
"ENGLISH ONLY, do not quote the inputs, no disclaimers.\n\n"
|
|
|
|
| 133 |
f"FACT LINE:\n{raw}")
|
| 134 |
+
if chain_en:
|
| 135 |
+
prompt += f"\n\nREASONING:\n{chain_en}"
|
| 136 |
yield "🤖 _Translator sub-agent (Qwen3-1.7B · llama.cpp) is summarizing…_"
|
| 137 |
final = ""
|
| 138 |
+
for acc in llm_local.chat_stream(prompt, max_tokens=240, temperature=0.2,
|
| 139 |
worker="translator"):
|
| 140 |
final = acc
|
| 141 |
yield "🤖 **AI narrative (Translator sub-agent · Qwen3-1.7B):**\n\n" + acc
|
| 142 |
+
# English-only guarantee: if the model still slipped in Chinese, re-run once
|
| 143 |
+
# asking for a strict English rewrite.
|
| 144 |
+
if re.search(r"[\u4e00-\u9fff]", final):
|
| 145 |
+
fix = ("Rewrite the following as an English-only equity summary "
|
| 146 |
+
"(≤100 words). Remove ALL Chinese characters, keep the meaning "
|
| 147 |
+
"and numbers, no disclaimers:\n\n" + final)
|
| 148 |
+
fixed = ""
|
| 149 |
+
for acc in llm_local.chat_stream(fix, max_tokens=240, temperature=0.1,
|
| 150 |
+
worker="translator"):
|
| 151 |
+
fixed = acc
|
| 152 |
+
yield "🤖 **AI narrative (Translator sub-agent · Qwen3-1.7B):**\n\n" + acc
|
| 153 |
+
if fixed:
|
| 154 |
+
final = fixed
|
| 155 |
# capture (raw read → narrative) as a fine-tuning pair (🎯 Well-Tuned)
|
| 156 |
try:
|
| 157 |
import finetune_data
|
llm_local.py
CHANGED
|
@@ -51,6 +51,7 @@ _NCPU = max(2, (os.cpu_count() or 4))
|
|
| 51 |
# the 4B Analyst writes reports. Total ≈ 9 GB on a 32 GB Space.
|
| 52 |
WORKER_LABEL = {
|
| 53 |
"translator": "Translator sub-agent (Signals · Explain)",
|
|
|
|
| 54 |
"narrator": "Narrator sub-agent (Sector Rotation)",
|
| 55 |
"reporter": "Reporter sub-agent (News · Research support)",
|
| 56 |
"analyst": "Analyst sub-agent (Auto Research)",
|
|
@@ -64,6 +65,7 @@ def _mk(model):
|
|
| 64 |
|
| 65 |
WORKERS = {
|
| 66 |
"translator": _mk(FAST_MODEL),
|
|
|
|
| 67 |
"narrator": _mk(FAST_MODEL),
|
| 68 |
"reporter": _mk(FAST_MODEL),
|
| 69 |
"analyst": _mk(DEFAULT_MODEL),
|
|
@@ -206,7 +208,7 @@ def load_model(name: str, worker: str = "analyst") -> str:
|
|
| 206 |
def auto_load_all():
|
| 207 |
"""Startup: tiny agents first (one small GGUF download serves all three),
|
| 208 |
then the Analyst. Runs in a background thread."""
|
| 209 |
-
for key in ("translator", "narrator", "reporter", "analyst"):
|
| 210 |
load_model(WORKERS[key]["model"], worker=key)
|
| 211 |
|
| 212 |
|
|
@@ -219,7 +221,7 @@ def is_loaded(worker: str = None) -> bool:
|
|
| 219 |
|
| 220 |
def status() -> str:
|
| 221 |
lines = []
|
| 222 |
-
for key in ("translator", "narrator", "reporter", "analyst"):
|
| 223 |
w = WORKERS[key]
|
| 224 |
label = WORKER_LABEL[key]
|
| 225 |
if w["llm"] is not None:
|
|
@@ -297,7 +299,7 @@ def quick_test() -> str:
|
|
| 297 |
"""Sanity check both sub-agents."""
|
| 298 |
import time
|
| 299 |
outs = []
|
| 300 |
-
for key in ("translator", "narrator", "reporter", "analyst"):
|
| 301 |
if WORKERS[key]["llm"] is None:
|
| 302 |
outs.append(f"{WORKER_LABEL[key]}: not loaded ({WORKERS[key]['stage']})")
|
| 303 |
continue
|
|
|
|
| 51 |
# the 4B Analyst writes reports. Total ≈ 9 GB on a 32 GB Space.
|
| 52 |
WORKER_LABEL = {
|
| 53 |
"translator": "Translator sub-agent (Signals · Explain)",
|
| 54 |
+
"rchain": "Ruling-Chain Translator sub-agent (中文→EN)",
|
| 55 |
"narrator": "Narrator sub-agent (Sector Rotation)",
|
| 56 |
"reporter": "Reporter sub-agent (News · Research support)",
|
| 57 |
"analyst": "Analyst sub-agent (Auto Research)",
|
|
|
|
| 65 |
|
| 66 |
WORKERS = {
|
| 67 |
"translator": _mk(FAST_MODEL),
|
| 68 |
+
"rchain": _mk(FAST_MODEL),
|
| 69 |
"narrator": _mk(FAST_MODEL),
|
| 70 |
"reporter": _mk(FAST_MODEL),
|
| 71 |
"analyst": _mk(DEFAULT_MODEL),
|
|
|
|
| 208 |
def auto_load_all():
|
| 209 |
"""Startup: tiny agents first (one small GGUF download serves all three),
|
| 210 |
then the Analyst. Runs in a background thread."""
|
| 211 |
+
for key in ("translator", "rchain", "narrator", "reporter", "analyst"):
|
| 212 |
load_model(WORKERS[key]["model"], worker=key)
|
| 213 |
|
| 214 |
|
|
|
|
| 221 |
|
| 222 |
def status() -> str:
|
| 223 |
lines = []
|
| 224 |
+
for key in ("translator", "rchain", "narrator", "reporter", "analyst"):
|
| 225 |
w = WORKERS[key]
|
| 226 |
label = WORKER_LABEL[key]
|
| 227 |
if w["llm"] is not None:
|
|
|
|
| 299 |
"""Sanity check both sub-agents."""
|
| 300 |
import time
|
| 301 |
outs = []
|
| 302 |
+
for key in ("translator", "rchain", "narrator", "reporter", "analyst"):
|
| 303 |
if WORKERS[key]["llm"] is None:
|
| 304 |
outs.append(f"{WORKER_LABEL[key]}: not loaded ({WORKERS[key]['stage']})")
|
| 305 |
continue
|