Spaces:
Paused
Paused
| """ | |
| 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) | |