""" Multi-Indicator Z-Score Scorer — Gradio app. Pure Pine Script port. No ML, no entity tracking. """ import gradio as gr import numpy as np import pandas as pd import plotly.graph_objects as go from plotly.subplots import make_subplots import tempfile from scorer import Config, score_series def run_scoring(csv_file, prices_text, rsi_len, macd_fast, macd_slow, macd_sig, stoch_len, stoch_smooth, trend_len, z_lookback, w_rsi, w_macd, w_stoch, w_trend, buy_level, sell_level): # Accept either CSV upload OR manual prices if csv_file is not None: df = pd.read_csv(csv_file.name) if "close" not in df.columns: return "CSV must have a 'close' column.", None, None close = df["close"].to_numpy(dtype=float) high = df["high"].to_numpy(dtype=float) if "high" in df.columns else None low = df["low"].to_numpy(dtype=float) if "low" in df.columns else None elif prices_text and prices_text.strip(): try: prices = [float(x.strip()) for x in prices_text.replace("\n", ",").split(",") if x.strip()] except ValueError: return "Invalid prices. Use comma or newline separated numbers.", None, None if len(prices) < 20: return f"Need at least 20 prices (got {len(prices)}).", None, None close = np.array(prices, dtype=float) high = None low = None else: return "Upload a CSV or paste prices to begin.", None, None cfg = Config( rsi_len=int(rsi_len), macd_fast=int(macd_fast), macd_slow=int(macd_slow), macd_sig=int(macd_sig), stoch_len=int(stoch_len), stoch_smooth=int(stoch_smooth), trend_len=int(trend_len), z_lookback=int(z_lookback), w_rsi=float(w_rsi), w_macd=float(w_macd), w_stoch=float(w_stoch), w_trend=float(w_trend), buy_level=float(buy_level), sell_level=float(sell_level), ) results = score_series(close, high, low, cfg) composite = np.array([r.composite_z for r in results]) signals = [r.signal for r in results] buys = sum(1 for s in signals if s == "buy") sells = sum(1 for s in signals if s == "sell") latest = results[-1] summary_md = ( "### Summary\n\n" "**Bars analyzed:** " + str(len(close)) + "\n\n" "**Buy signals:** " + str(buys) + "\n\n" "**Sell signals:** " + str(sells) + "\n\n" "### Latest Bar\n\n" "| Metric | Value |\n" "|--------|-------|\n" "| Signal | **" + latest.signal.upper() + "** |\n" "| Composite Z | `" + format(latest.composite_z, "+.3f") + "` |\n" "| RSI Z | `" + format(latest.z_rsi, "+.3f") + "` (raw: " + format(latest.rsi, ".1f") + ") |\n" "| MACD Z | `" + format(latest.z_macd, "+.3f") + "` |\n" "| Stoch Z | `" + format(latest.z_stoch, "+.3f") + "` (raw: " + format(latest.stoch_k, ".1f") + ") |\n" "| Trend Z | `" + format(latest.z_trend, "+.3f") + "` |\n" "| Buy Level | `" + str(cfg.buy_level) + "` |\n" "| Sell Level | `" + str(cfg.sell_level) + "` |\n" ) fig = make_subplots( rows=2, cols=1, shared_xaxes=True, row_heights=[0.6, 0.4], subplot_titles=("Price", "Composite Z-Score"), vertical_spacing=0.08, ) x = list(range(len(close))) fig.add_trace( go.Scatter(x=x, y=close.tolist(), mode="lines", name="Close", line=dict(color="#60a5fa", width=1.5)), row=1, col=1, ) buy_x = [i for i, s in enumerate(signals) if s == "buy"] sell_x = [i for i, s in enumerate(signals) if s == "sell"] if buy_x: fig.add_trace( go.Scatter(x=buy_x, y=[float(close[i]) for i in buy_x], mode="markers", name="Buy", marker=dict(color="#10b981", size=10, symbol="triangle-up")), row=1, col=1, ) if sell_x: fig.add_trace( go.Scatter(x=sell_x, y=[float(close[i]) for i in sell_x], mode="markers", name="Sell", marker=dict(color="#ef4444", size=10, symbol="triangle-down")), row=1, col=1, ) fig.add_trace( go.Scatter(x=x, y=composite.tolist(), mode="lines", name="Composite Z", line=dict(color="#a78bfa", width=1.5)), row=2, col=1, ) fig.add_hline(y=cfg.buy_level, line_dash="dash", line_color="#10b981", row=2, col=1) fig.add_hline(y=cfg.sell_level, line_dash="dash", line_color="#ef4444", row=2, col=1) fig.add_hline(y=0, line_dash="dot", line_color="#6b7280", row=2, col=1) fig.update_layout( template="plotly_dark", height=600, showlegend=True, margin=dict(l=10, r=10, t=40, b=10), ) out_df = pd.DataFrame({ "close": close, "composite_z": composite, "z_rsi": [r.z_rsi for r in results], "z_macd": [r.z_macd for r in results], "z_stoch": [r.z_stoch for r in results], "z_trend": [r.z_trend for r in results], "signal": signals, }) tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) out_df.to_csv(tmp.name, index=False) return summary_md, fig, tmp.name with gr.Blocks(title="Multi-Indicator Z-Score Scorer") as demo: gr.Markdown("# Multi-Indicator Z-Score Scorer") gr.Markdown( "Pure Pine script port. RSI + MACD + Stochastic + Trend, " "each normalized to z-scores over a lookback window, weighted into a composite. " "Buy/sell on threshold crossovers. **No ML, no entity tracking.**" ) with gr.Row(): with gr.Column(scale=1): gr.Markdown("### Input (CSV or manual prices)") csv_input = gr.File(label="CSV (needs 'close' column)", file_types=[".csv"]) prices_input = gr.Textbox( label="OR paste prices (comma/newline separated, min 20)", lines=6, placeholder="100.0, 101.5, 102.3, 103.1, ...", ) gr.Markdown("### Indicators") rsi_len = gr.Slider(2, 50, value=14, step=1, label="RSI Length") macd_fast = gr.Slider(2, 50, value=12, step=1, label="MACD Fast") macd_slow = gr.Slider(5, 100, value=26, step=1, label="MACD Slow") macd_sig = gr.Slider(2, 50, value=9, step=1, label="MACD Signal") stoch_len = gr.Slider(2, 50, value=14, step=1, label="Stoch Length") stoch_smooth = gr.Slider(1, 10, value=3, step=1, label="Stoch Smooth K") trend_len = gr.Slider(5, 200, value=50, step=1, label="Trend EMA Length") gr.Markdown("### Weights & Thresholds") z_lookback = gr.Slider(10, 500, value=100, step=5, label="Z-Score Lookback") w_rsi = gr.Slider(0.0, 3.0, value=1.0, step=0.1, label="RSI Weight") w_macd = gr.Slider(0.0, 3.0, value=1.0, step=0.1, label="MACD Weight") w_stoch = gr.Slider(0.0, 3.0, value=1.0, step=0.1, label="Stoch Weight") w_trend = gr.Slider(0.0, 3.0, value=1.0, step=0.1, label="Trend Weight") buy_level = gr.Slider(0.1, 5.0, value=1.5, step=0.1, label="Buy Level") sell_level = gr.Slider(-5.0, -0.1, value=-1.5, step=0.1, label="Sell Level") btn = gr.Button("Score", variant="primary", size="lg") with gr.Column(scale=2): summary = gr.Markdown("Upload a CSV or paste prices to begin.") chart = gr.Plot() download = gr.File(label="Download Scores CSV") btn.click( run_scoring, inputs=[csv_input, prices_input, rsi_len, macd_fast, macd_slow, macd_sig, stoch_len, stoch_smooth, trend_len, z_lookback, w_rsi, w_macd, w_stoch, w_trend, buy_level, sell_level], outputs=[summary, chart, download], ) gr.Markdown("---") gr.Markdown("Based on `multi_indicator_zscore.pine`") if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)