Spaces:
Running
Running
| """ | |
| Pine Script v5 Strategy Generator. | |
| Generates production-ready TradingView Pine Script v5 code from: | |
| 1. Natural language descriptions (via LLM) | |
| 2. Pre-built strategy templates | |
| 3. Custom parameter configurations | |
| Supports 12+ built-in templates covering all strategy types: | |
| - Trend following, mean reversion, momentum, breakout | |
| - Multi-timeframe, oscillator, volatility-based | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from typing import Any, Dict, List, Optional | |
| import aiohttp | |
| from app.config import get_settings | |
| logger = logging.getLogger(__name__) | |
| _settings = get_settings() | |
| # ── Strategy Templates ─────────────────────────────────────────────────── | |
| STRATEGY_TEMPLATES: Dict[str, Dict[str, Any]] = { | |
| "sma_crossover": { | |
| "id": "sma_crossover", | |
| "name": "SMA Crossover", | |
| "category": "Momentum", | |
| "description": "Classic dual moving average crossover strategy. Buys on golden cross, sells on death cross.", | |
| "parameters": {"fast_length": 20, "slow_length": 50}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("SMA Crossover Strategy", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| // Inputs | |
| fast_length = input.int({fast_length}, title="Fast SMA Length", minval=1) | |
| slow_length = input.int({slow_length}, title="Slow SMA Length", minval=1) | |
| use_stop_loss = input.bool(true, title="Use Stop Loss") | |
| stop_loss_pct = input.float(5.0, title="Stop Loss %", minval=0.1, step=0.1) | |
| use_take_profit = input.bool(true, title="Use Take Profit") | |
| take_profit_pct = input.float(10.0, title="Take Profit %", minval=0.1, step=0.1) | |
| // Calculations | |
| fast_sma = ta.sma(close, fast_length) | |
| slow_sma = ta.sma(close, slow_length) | |
| // Conditions | |
| long_condition = ta.crossover(fast_sma, slow_sma) | |
| short_condition = ta.crossunder(fast_sma, slow_sma) | |
| // Strategy execution | |
| if long_condition | |
| strategy.entry("Long", strategy.long) | |
| if short_condition | |
| strategy.close("Long") | |
| // Risk management | |
| if use_stop_loss or use_take_profit | |
| strategy.exit("Exit", "Long", stop=use_stop_loss ? strategy.position_avg_price * (1 - stop_loss_pct / 100) : na, limit=use_take_profit ? strategy.position_avg_price * (1 + take_profit_pct / 100) : na) | |
| // Plotting | |
| plot(fast_sma, color=color.new(color.blue, 0), title="Fast SMA", linewidth=2) | |
| plot(slow_sma, color=color.new(color.red, 0), title="Slow SMA", linewidth=2) | |
| bgcolor(strategy.position_size > 0 ? color.new(color.green, 90) : na) | |
| ''', | |
| }, | |
| "rsi_reversal": { | |
| "id": "rsi_reversal", | |
| "name": "RSI Mean Reversion", | |
| "category": "Mean Reversion", | |
| "description": "Buys when RSI is oversold, sells when overbought. Classic counter-trend strategy.", | |
| "parameters": {"rsi_length": 14, "oversold": 30, "overbought": 70}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("RSI Mean Reversion", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| // Inputs | |
| rsi_length = input.int({rsi_length}, title="RSI Length", minval=2) | |
| oversold_level = input.int({oversold}, title="Oversold Level", minval=1, maxval=50) | |
| overbought_level = input.int({overbought}, title="Overbought Level", minval=50, maxval=99) | |
| stop_loss_pct = input.float(3.0, title="Stop Loss %", minval=0.1, step=0.1) | |
| take_profit_pct = input.float(6.0, title="Take Profit %", minval=0.1, step=0.1) | |
| // Calculations | |
| rsi_val = ta.rsi(close, rsi_length) | |
| // Conditions | |
| long_condition = ta.crossover(rsi_val, oversold_level) | |
| exit_condition = ta.crossunder(rsi_val, overbought_level) | |
| // Execution | |
| if long_condition | |
| strategy.entry("Long", strategy.long) | |
| if exit_condition | |
| strategy.close("Long") | |
| strategy.exit("SL/TP", "Long", stop=strategy.position_avg_price * (1 - stop_loss_pct / 100), limit=strategy.position_avg_price * (1 + take_profit_pct / 100)) | |
| // Plot | |
| hline(oversold_level, color=color.green, linestyle=hline.style_dashed) | |
| hline(overbought_level, color=color.red, linestyle=hline.style_dashed) | |
| ''', | |
| }, | |
| "macd_signal": { | |
| "id": "macd_signal", | |
| "name": "MACD Signal Line Crossover", | |
| "category": "Momentum", | |
| "description": "Buys on MACD bullish crossover, closes on bearish crossover with histogram confirmation.", | |
| "parameters": {"fast": 12, "slow": 26, "signal": 9}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("MACD Strategy", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| fast_len = input.int({fast}, title="MACD Fast Length") | |
| slow_len = input.int({slow}, title="MACD Slow Length") | |
| sig_len = input.int({signal}, title="Signal Length") | |
| stop_loss_pct = input.float(4.0, title="Stop Loss %") | |
| take_profit_pct = input.float(8.0, title="Take Profit %") | |
| [macdLine, signalLine, hist] = ta.macd(close, fast_len, slow_len, sig_len) | |
| long_cond = ta.crossover(macdLine, signalLine) and hist > 0 | |
| exit_cond = ta.crossunder(macdLine, signalLine) | |
| if long_cond | |
| strategy.entry("Long", strategy.long) | |
| if exit_cond | |
| strategy.close("Long") | |
| strategy.exit("Exit", "Long", stop=strategy.position_avg_price * (1 - stop_loss_pct / 100), limit=strategy.position_avg_price * (1 + take_profit_pct / 100)) | |
| ''', | |
| }, | |
| "bollinger_breakout": { | |
| "id": "bollinger_breakout", | |
| "name": "Bollinger Band Breakout", | |
| "category": "Volatility", | |
| "description": "Buys when price breaks above upper band, sells on return to middle band. Captures volatility expansion.", | |
| "parameters": {"bb_length": 20, "bb_std": 2.0}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Bollinger Breakout", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| length = input.int({bb_length}, title="BB Length") | |
| mult = input.float({bb_std}, title="BB StdDev") | |
| stop_loss_pct = input.float(3.0, title="Stop Loss %") | |
| basis = ta.sma(close, length) | |
| upper = basis + mult * ta.stdev(close, length) | |
| lower = basis - mult * ta.stdev(close, length) | |
| long_cond = ta.crossover(close, upper) | |
| exit_cond = ta.crossunder(close, basis) | |
| if long_cond | |
| strategy.entry("Long", strategy.long) | |
| if exit_cond | |
| strategy.close("Long") | |
| strategy.exit("SL", "Long", stop=strategy.position_avg_price * (1 - stop_loss_pct / 100)) | |
| plot(basis, color=color.orange, title="Basis") | |
| plot(upper, color=color.blue, title="Upper") | |
| plot(lower, color=color.blue, title="Lower") | |
| ''', | |
| }, | |
| "supertrend": { | |
| "id": "supertrend", | |
| "name": "Supertrend", | |
| "category": "Trend Following", | |
| "description": "ATR-based trend following with dynamic support/resistance. Excellent for capturing strong trends.", | |
| "parameters": {"atr_length": 10, "factor": 3.0}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Supertrend Strategy", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| atr_len = input.int({atr_length}, title="ATR Length") | |
| factor = input.float({factor}, title="Factor") | |
| [supertrend, direction] = ta.supertrend(factor, atr_len) | |
| long_cond = ta.crossover(close, supertrend) | |
| short_cond = ta.crossunder(close, supertrend) | |
| if long_cond | |
| strategy.entry("Long", strategy.long) | |
| if short_cond | |
| strategy.close("Long") | |
| plot(supertrend, color=direction < 0 ? color.green : color.red, title="Supertrend", linewidth=2) | |
| ''', | |
| }, | |
| "ema_ribbon": { | |
| "id": "ema_ribbon", | |
| "name": "EMA Ribbon", | |
| "category": "Trend Following", | |
| "description": "Multiple EMA fan for trend confirmation. All EMAs aligned = strong trend signal.", | |
| "parameters": {"ema1": 8, "ema2": 13, "ema3": 21, "ema4": 34, "ema5": 55}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("EMA Ribbon Strategy", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| e1 = input.int({ema1}, title="EMA 1") | |
| e2 = input.int({ema2}, title="EMA 2") | |
| e3 = input.int({ema3}, title="EMA 3") | |
| e4 = input.int({ema4}, title="EMA 4") | |
| e5 = input.int({ema5}, title="EMA 5") | |
| stop_loss_pct = input.float(4.0, title="Stop Loss %") | |
| ema1 = ta.ema(close, e1) | |
| ema2 = ta.ema(close, e2) | |
| ema3 = ta.ema(close, e3) | |
| ema4 = ta.ema(close, e4) | |
| ema5 = ta.ema(close, e5) | |
| bullish_ribbon = ema1 > ema2 and ema2 > ema3 and ema3 > ema4 and ema4 > ema5 | |
| bearish_ribbon = ema1 < ema2 and ema2 < ema3 and ema3 < ema4 and ema4 < ema5 | |
| if bullish_ribbon and not bullish_ribbon[1] | |
| strategy.entry("Long", strategy.long) | |
| if bearish_ribbon | |
| strategy.close("Long") | |
| strategy.exit("SL", "Long", stop=strategy.position_avg_price * (1 - stop_loss_pct / 100)) | |
| plot(ema1, color=color.new(#26A69A, 0), linewidth=1) | |
| plot(ema2, color=color.new(#2196F3, 0), linewidth=1) | |
| plot(ema3, color=color.new(#FF9800, 0), linewidth=1) | |
| plot(ema4, color=color.new(#E91E63, 0), linewidth=1) | |
| plot(ema5, color=color.new(#9C27B0, 0), linewidth=1) | |
| ''', | |
| }, | |
| "stochastic_rsi_combo": { | |
| "id": "stochastic_rsi_combo", | |
| "name": "Stochastic + RSI Combo", | |
| "category": "Oscillator", | |
| "description": "Dual oscillator confirmation: requires both Stochastic K/D crossover and RSI alignment.", | |
| "parameters": {"rsi_len": 14, "stoch_k": 14, "stoch_d": 3}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Stoch + RSI Combo", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| rsi_len = input.int({rsi_len}, title="RSI Length") | |
| stoch_k = input.int({stoch_k}, title="Stoch K") | |
| stoch_d = input.int({stoch_d}, title="Stoch D Smoothing") | |
| stop_loss_pct = input.float(4.0, title="Stop Loss %") | |
| rsi_val = ta.rsi(close, rsi_len) | |
| k = ta.stoch(close, high, low, stoch_k) | |
| d = ta.sma(k, stoch_d) | |
| long_cond = ta.crossover(k, d) and k < 30 and rsi_val < 40 | |
| exit_cond = (k > 80 and rsi_val > 70) or ta.crossunder(k, d) | |
| if long_cond | |
| strategy.entry("Long", strategy.long) | |
| if exit_cond | |
| strategy.close("Long") | |
| strategy.exit("SL", "Long", stop=strategy.position_avg_price * (1 - stop_loss_pct / 100)) | |
| ''', | |
| }, | |
| "mean_reversion_zscore": { | |
| "id": "mean_reversion_zscore", | |
| "name": "Z-Score Mean Reversion", | |
| "category": "Statistical", | |
| "description": "Statistical mean reversion using Z-score. Enters when price deviates significantly from mean.", | |
| "parameters": {"lookback": 50, "entry_z": 2.0, "exit_z": 0.0}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Z-Score Mean Reversion", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| lookback = input.int({lookback}, title="Lookback Period") | |
| entry_z = input.float({entry_z}, title="Entry Z-Score") | |
| exit_z = input.float({exit_z}, title="Exit Z-Score") | |
| stop_loss_pct = input.float(5.0, title="Stop Loss %") | |
| mean = ta.sma(close, lookback) | |
| sd = ta.stdev(close, lookback) | |
| z_score = (close - mean) / sd | |
| long_cond = z_score < -entry_z | |
| exit_long = z_score > -exit_z | |
| if long_cond | |
| strategy.entry("Long", strategy.long) | |
| if exit_long | |
| strategy.close("Long") | |
| strategy.exit("SL", "Long", stop=strategy.position_avg_price * (1 - stop_loss_pct / 100)) | |
| hline(0, color=color.gray) | |
| ''', | |
| }, | |
| "atr_trailing_stop": { | |
| "id": "atr_trailing_stop", | |
| "name": "ATR Trailing Stop", | |
| "category": "Risk-Managed", | |
| "description": "Trend-following with dynamic ATR-based trailing stop. Captures trends with tight risk control.", | |
| "parameters": {"atr_length": 14, "atr_multiplier": 2.5, "ema_length": 50}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("ATR Trailing Stop", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| atr_len = input.int({atr_length}, title="ATR Length") | |
| atr_mult = input.float({atr_multiplier}, title="ATR Multiplier") | |
| ema_len = input.int({ema_length}, title="EMA Length") | |
| atr_val = ta.atr(atr_len) | |
| ema_val = ta.ema(close, ema_len) | |
| long_cond = close > ema_val and close > close[1] | |
| if long_cond and strategy.position_size == 0 | |
| strategy.entry("Long", strategy.long) | |
| trail_stop = strategy.position_avg_price > 0 ? close - atr_val * atr_mult : na | |
| strategy.exit("Trail", "Long", trail_offset=atr_val * atr_mult / syminfo.mintick) | |
| plot(ema_val, color=color.blue, title="EMA") | |
| ''', | |
| }, | |
| "multi_timeframe": { | |
| "id": "multi_timeframe", | |
| "name": "Multi-Timeframe Confirmation", | |
| "category": "Advanced", | |
| "description": "Uses higher timeframe trend confirmation with lower timeframe entry. Professional-grade approach.", | |
| "parameters": {"htf": "D", "ltf_ema": 20, "htf_ema": 50}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Multi-TF Confirmation", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| htf = input.timeframe("{htf}", title="Higher Timeframe") | |
| ltf_ema_len = input.int({ltf_ema}, title="LTF EMA Length") | |
| htf_ema_len = input.int({htf_ema}, title="HTF EMA Length") | |
| stop_loss_pct = input.float(3.0, title="Stop Loss %") | |
| take_profit_pct = input.float(6.0, title="Take Profit %") | |
| ltf_ema = ta.ema(close, ltf_ema_len) | |
| htf_close = request.security(syminfo.tickerid, htf, close) | |
| htf_ema = request.security(syminfo.tickerid, htf, ta.ema(close, htf_ema_len)) | |
| htf_bullish = htf_close > htf_ema | |
| ltf_signal = ta.crossover(close, ltf_ema) | |
| long_cond = htf_bullish and ltf_signal | |
| exit_cond = close < ltf_ema and not htf_bullish | |
| if long_cond | |
| strategy.entry("Long", strategy.long) | |
| if exit_cond | |
| strategy.close("Long") | |
| strategy.exit("SL/TP", "Long", stop=strategy.position_avg_price * (1 - stop_loss_pct / 100), limit=strategy.position_avg_price * (1 + take_profit_pct / 100)) | |
| plot(ltf_ema, color=color.blue, title="LTF EMA") | |
| bgcolor(htf_bullish ? color.new(color.green, 95) : color.new(color.red, 95)) | |
| ''', | |
| }, | |
| "vwap_strategy": { | |
| "id": "vwap_strategy", | |
| "name": "VWAP Bounce", | |
| "category": "Intraday", | |
| "description": "Institutional VWAP-based strategy. Buys on pullback to VWAP with volume confirmation.", | |
| "parameters": {"vol_mult": 1.5}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("VWAP Bounce", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| vol_mult = input.float({vol_mult}, title="Volume Multiplier") | |
| stop_loss_pct = input.float(2.0, title="Stop Loss %") | |
| take_profit_pct = input.float(4.0, title="Take Profit %") | |
| vwap_val = ta.vwap | |
| vol_avg = ta.sma(volume, 20) | |
| high_vol = volume > vol_avg * vol_mult | |
| bounce = close > vwap_val and close[1] <= vwap_val[1] and high_vol | |
| if bounce | |
| strategy.entry("Long", strategy.long) | |
| if close < vwap_val and strategy.position_size > 0 | |
| strategy.close("Long") | |
| strategy.exit("SL/TP", "Long", stop=strategy.position_avg_price * (1 - stop_loss_pct / 100), limit=strategy.position_avg_price * (1 + take_profit_pct / 100)) | |
| plot(vwap_val, color=color.purple, title="VWAP", linewidth=2) | |
| ''', | |
| }, | |
| "ichimoku_cloud": { | |
| "id": "ichimoku_cloud", | |
| "name": "Ichimoku Cloud", | |
| "category": "Multi-Signal", | |
| "description": "Complete Ichimoku Kinko Hyo system with cloud, conversion, and base line signals.", | |
| "parameters": {"conv": 9, "base": 26, "span": 52}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Ichimoku Cloud Strategy", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| conv_len = input.int({conv}, title="Conversion Length") | |
| base_len = input.int({base}, title="Base Length") | |
| span_len = input.int({span}, title="Span B Length") | |
| stop_loss_pct = input.float(4.0, title="Stop Loss %") | |
| donchian(len) => math.avg(ta.lowest(len), ta.highest(len)) | |
| conv_line = donchian(conv_len) | |
| base_line = donchian(base_len) | |
| lead_a = math.avg(conv_line, base_line) | |
| lead_b = donchian(span_len) | |
| above_cloud = close > lead_a and close > lead_b | |
| tk_cross = ta.crossover(conv_line, base_line) | |
| long_cond = tk_cross and above_cloud | |
| exit_cond = ta.crossunder(conv_line, base_line) or close < lead_b | |
| if long_cond | |
| strategy.entry("Long", strategy.long) | |
| if exit_cond | |
| strategy.close("Long") | |
| strategy.exit("SL", "Long", stop=strategy.position_avg_price * (1 - stop_loss_pct / 100)) | |
| plot(conv_line, color=color.blue, title="Conversion") | |
| plot(base_line, color=color.red, title="Base") | |
| p1 = plot(lead_a, offset=base_len, color=color.green, title="Lead A") | |
| p2 = plot(lead_b, offset=base_len, color=color.red, title="Lead B") | |
| fill(p1, p2, color=lead_a > lead_b ? color.new(color.green, 90) : color.new(color.red, 90)) | |
| ''', | |
| }, | |
| # ── Hedge-Focused Strategies ────────────────────────────────────────── | |
| "portfolio_hedge": { | |
| "id": "portfolio_hedge", | |
| "name": "Dynamic Portfolio Hedge", | |
| "category": "Hedging", | |
| "description": "Automatically sizes inverse hedging position based on SMA trend + volatility regime. Increases hedge in high-vol downtrends, reduces in calm uptrends.", | |
| "parameters": {"trend_sma": 50, "vol_lookback": 20, "max_hedge_pct": 50, "min_hedge_pct": 5}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Dynamic Portfolio Hedge", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| // Inputs | |
| trend_len = input.int({trend_sma}, title="Trend SMA Length") | |
| vol_len = input.int({vol_lookback}, title="Volatility Lookback") | |
| max_hedge = input.float({max_hedge_pct}, title="Max Hedge %", minval=1, maxval=100) | |
| min_hedge = input.float({min_hedge_pct}, title="Min Hedge %", minval=0, maxval=50) | |
| // Core calculations | |
| trend_sma = ta.sma(close, trend_len) | |
| ret = math.log(close / close[1]) | |
| hist_vol = ta.stdev(ret, vol_len) * math.sqrt(252) * 100 | |
| norm_vol = math.min(hist_vol / 30.0, 2.0) // Normalized: 30% vol = 1.0 | |
| // Regime detection | |
| below_trend = close < trend_sma | |
| trend_distance = (close - trend_sma) / trend_sma * 100 | |
| // Dynamic hedge sizing | |
| hedge_score = 0.0 | |
| hedge_score := below_trend ? math.min(max_hedge, min_hedge + math.abs(trend_distance) * norm_vol * 10) : min_hedge | |
| // Signals | |
| hedge_up = hedge_score > hedge_score[1] * 1.2 and hedge_score > 15 | |
| hedge_down = hedge_score < hedge_score[1] * 0.7 and hedge_score < 10 | |
| if hedge_up and strategy.position_size <= 0 | |
| strategy.entry("Hedge Short", strategy.short, qty=strategy.equity * hedge_score / 100 / close) | |
| if hedge_down | |
| strategy.close("Hedge Short") | |
| // Visuals | |
| plot(trend_sma, color=color.blue, title="Trend SMA", linewidth=2) | |
| plot(hedge_score, color=color.orange, title="Hedge Score %", display=display.pane) | |
| bgcolor(below_trend ? color.new(color.red, 95) : color.new(color.green, 95)) | |
| hline(max_hedge, color=color.red, linestyle=hline.style_dashed, title="Max Hedge") | |
| hline(min_hedge, color=color.green, linestyle=hline.style_dashed, title="Min Hedge") | |
| ''', | |
| }, | |
| "pairs_trading_hedge": { | |
| "id": "pairs_trading_hedge", | |
| "name": "Pairs Trading Hedge", | |
| "category": "Hedging", | |
| "description": "Statistical arbitrage between correlated pairs. Uses z-score of price ratio for mean reversion entries. Market-neutral hedging approach.", | |
| "parameters": {"lookback": 60, "entry_z": 2.0, "exit_z": 0.5, "stop_z": 3.5}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Pairs Trading Hedge", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| // Inputs | |
| lookback = input.int({lookback}, title="Lookback Period") | |
| entry_z = input.float({entry_z}, title="Entry Z-Score") | |
| exit_z = input.float({exit_z}, title="Exit Z-Score") | |
| stop_z = input.float({stop_z}, title="Stop Loss Z-Score") | |
| // Spread calculation (self-referencing mean reversion) | |
| mean_price = ta.sma(close, lookback) | |
| std_price = ta.stdev(close, lookback) | |
| z_score = (close - mean_price) / std_price | |
| // Entry signals | |
| long_entry = z_score < -entry_z // Oversold: buy | |
| short_entry = z_score > entry_z // Overbought: short (hedge) | |
| // Exit signals | |
| long_exit = z_score > -exit_z | |
| short_exit = z_score < exit_z | |
| // Stop loss | |
| long_stop = z_score < -stop_z | |
| short_stop = z_score > stop_z | |
| // Execution | |
| if long_entry | |
| strategy.entry("Long Pair", strategy.long) | |
| if short_entry | |
| strategy.entry("Short Hedge", strategy.short) | |
| if long_exit or long_stop | |
| strategy.close("Long Pair") | |
| if short_exit or short_stop | |
| strategy.close("Short Hedge") | |
| // Visualization | |
| plot(z_score, color=color.blue, title="Z-Score", display=display.pane) | |
| hline(entry_z, color=color.red, linestyle=hline.style_dashed) | |
| hline(-entry_z, color=color.green, linestyle=hline.style_dashed) | |
| hline(0, color=color.gray) | |
| bgcolor(z_score > entry_z ? color.new(color.red, 90) : z_score < -entry_z ? color.new(color.green, 90) : na) | |
| ''', | |
| }, | |
| "tail_risk_hedge": { | |
| "id": "tail_risk_hedge", | |
| "name": "Tail Risk Protection", | |
| "category": "Hedging", | |
| "description": "Simulates protective put strategy. Activates hedging when implied volatility spikes and trend breaks down. Protects against black swan events.", | |
| "parameters": {"vol_threshold": 25, "trend_ma": 200, "hedge_trigger_pct": 5}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Tail Risk Protection", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| // Inputs | |
| vol_thresh = input.float({vol_threshold}, title="Volatility Threshold %") | |
| trend_ma_len = input.int({trend_ma}, title="Trend MA Length") | |
| hedge_trigger = input.float({hedge_trigger_pct}, title="Hedge Trigger Drop %") | |
| // Calculations | |
| ret = math.log(close / close[1]) | |
| hist_vol = ta.stdev(ret, 20) * math.sqrt(252) * 100 | |
| trend_line = ta.sma(close, trend_ma_len) | |
| recent_high = ta.highest(close, 20) | |
| drawdown_pct = (recent_high - close) / recent_high * 100 | |
| // Tail risk detection | |
| vol_spike = hist_vol > vol_thresh | |
| trend_break = close < trend_line | |
| sharp_drop = drawdown_pct > hedge_trigger | |
| tail_risk = vol_spike and (trend_break or sharp_drop) | |
| // Hedge activation | |
| if tail_risk and strategy.position_size >= 0 | |
| strategy.entry("Tail Hedge", strategy.short) | |
| // Release hedge when volatility normalizes + trend recovers | |
| if hist_vol < vol_thresh * 0.7 and close > trend_line | |
| strategy.close("Tail Hedge") | |
| // Visuals | |
| plot(trend_line, color=color.blue, title="200 MA", linewidth=2) | |
| plot(hist_vol, color=vol_spike ? color.red : color.green, title="Hist Vol %", display=display.pane) | |
| bgcolor(tail_risk ? color.new(color.red, 85) : na) | |
| plotshape(tail_risk and not tail_risk[1], title="Hedge Activated", style=shape.triangledown, location=location.abovebar, color=color.red, size=size.small) | |
| ''', | |
| }, | |
| "correlation_hedge": { | |
| "id": "correlation_hedge", | |
| "name": "Correlation Hedge", | |
| "category": "Hedging", | |
| "description": "Trades inverse-correlated asset when primary shows weakness. Uses rolling correlation and momentum divergence for hedge timing.", | |
| "parameters": {"corr_lookback": 30, "momentum_len": 14, "hedge_threshold": -0.5}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Correlation Hedge Strategy", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.1) | |
| // Inputs | |
| corr_len = input.int({corr_lookback}, title="Correlation Lookback") | |
| mom_len = input.int({momentum_len}, title="Momentum Length") | |
| stop_loss_pct = input.float(5.0, title="Stop Loss %") | |
| take_profit_pct = input.float(10.0, title="Take Profit %") | |
| // Momentum analysis | |
| momentum = ta.mom(close, mom_len) | |
| mom_sma = ta.sma(momentum, mom_len) | |
| momentum_weakening = momentum < 0 and momentum < mom_sma | |
| // Volatility regime | |
| ret = math.log(close / close[1]) | |
| hist_vol = ta.stdev(ret, 20) * math.sqrt(252) * 100 | |
| rising_vol = hist_vol > ta.sma(hist_vol, 50) | |
| // Trend analysis | |
| ma_50 = ta.sma(close, 50) | |
| ma_200 = ta.sma(close, 200) | |
| bearish_trend = ma_50 < ma_200 | |
| // Hedge activation: weakness + rising vol + bearish trend | |
| hedge_signal = momentum_weakening and rising_vol and bearish_trend | |
| hedge_exit = momentum > 0 and not rising_vol | |
| if hedge_signal and strategy.position_size >= 0 | |
| strategy.entry("Corr Hedge", strategy.short) | |
| if hedge_exit | |
| strategy.close("Corr Hedge") | |
| strategy.exit("Exit", "Corr Hedge", stop=strategy.position_avg_price * (1 + stop_loss_pct / 100), limit=strategy.position_avg_price * (1 - take_profit_pct / 100)) | |
| // Visuals | |
| plot(ma_50, color=color.blue, title="50 MA") | |
| plot(ma_200, color=color.red, title="200 MA") | |
| plot(momentum, color=momentum > 0 ? color.green : color.red, title="Momentum", display=display.pane) | |
| bgcolor(hedge_signal ? color.new(color.orange, 90) : na) | |
| ''', | |
| }, | |
| # ── Global Market & Multi-Asset Strategies ──────────────────────────── | |
| "forex_session_breakout": { | |
| "id": "forex_session_breakout", | |
| "name": "Forex Session Breakout", | |
| "category": "Forex", | |
| "description": "Trades breakouts from London, New York, and Tokyo session ranges. Optimized for major forex pairs like EUR/USD, GBP/JPY.", | |
| "parameters": {"london_start": 8, "london_end": 16, "ny_start": 13, "ny_end": 21, "atr_mult": 1.5}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Forex Session Breakout", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.02) | |
| // Inputs | |
| london_start = input.int({london_start}, title="London Open (UTC)", minval=0, maxval=23) | |
| london_end = input.int({london_end}, title="London Close (UTC)", minval=0, maxval=23) | |
| ny_start = input.int({ny_start}, title="NY Open (UTC)", minval=0, maxval=23) | |
| atr_len = input.int(14, title="ATR Length") | |
| atr_mult = input.float({atr_mult}, title="ATR Multiplier", step=0.1) | |
| stop_atr = input.float(1.0, title="Stop Loss ATR Multiplier", step=0.1) | |
| // Session detection (UTC) | |
| cur_hour = hour(time, "UTC") | |
| in_london = cur_hour >= london_start and cur_hour < london_end | |
| in_ny = cur_hour >= ny_start and cur_hour < ny_start + 8 | |
| // Session high/low tracking | |
| var float session_high = na | |
| var float session_low = na | |
| new_session = (cur_hour == london_start or cur_hour == ny_start) and (cur_hour[1] != cur_hour) | |
| if new_session | |
| session_high := high | |
| session_low := low | |
| else if in_london or in_ny | |
| session_high := math.max(nz(session_high), high) | |
| session_low := math.min(nz(session_low), low) | |
| atr = ta.atr(atr_len) | |
| breakout_up = ta.crossover(close, session_high + atr * atr_mult * 0.1) | |
| breakout_down = ta.crossunder(close, session_low - atr * atr_mult * 0.1) | |
| if breakout_up and (in_london or in_ny) | |
| strategy.entry("Long", strategy.long) | |
| if breakout_down and (in_london or in_ny) | |
| strategy.entry("Short", strategy.short) | |
| strategy.exit("Exit L", "Long", stop=strategy.position_avg_price - atr * stop_atr, limit=strategy.position_avg_price + atr * atr_mult) | |
| strategy.exit("Exit S", "Short", stop=strategy.position_avg_price + atr * stop_atr, limit=strategy.position_avg_price - atr * atr_mult) | |
| bgcolor(in_london ? color.new(color.blue, 95) : in_ny ? color.new(color.orange, 95) : na) | |
| plot(session_high, color=color.green, style=plot.style_circles, title="Session High") | |
| plot(session_low, color=color.red, style=plot.style_circles, title="Session Low") | |
| ''', | |
| }, | |
| "crypto_momentum": { | |
| "id": "crypto_momentum", | |
| "name": "Crypto Momentum Hunter", | |
| "category": "Crypto", | |
| "description": "24/7 crypto momentum strategy using EMA + volume surge detection. Works on BTC, ETH, SOL, and altcoin pairs.", | |
| "parameters": {"fast_ema": 9, "slow_ema": 21, "vol_surge_mult": 2.0}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Crypto Momentum Hunter", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.075) | |
| // Inputs (crypto-optimized defaults) | |
| fast_ema_len = input.int({fast_ema}, title="Fast EMA", minval=1) | |
| slow_ema_len = input.int({slow_ema}, title="Slow EMA", minval=1) | |
| vol_surge = input.float({vol_surge_mult}, title="Volume Surge Multiplier", step=0.1) | |
| rsi_len = input.int(14, title="RSI Length") | |
| trailing_pct = input.float(5.0, title="Trailing Stop %", step=0.5) | |
| // Indicators | |
| fast_ema = ta.ema(close, fast_ema_len) | |
| slow_ema = ta.ema(close, slow_ema_len) | |
| rsi = ta.rsi(close, rsi_len) | |
| vol_avg = ta.sma(volume, 20) | |
| vol_spike = volume > vol_avg * vol_surge | |
| // Trend + momentum + volume confirmation | |
| bullish = ta.crossover(fast_ema, slow_ema) and rsi > 50 and vol_spike | |
| bearish = ta.crossunder(fast_ema, slow_ema) and rsi < 50 | |
| // Entries | |
| if bullish | |
| strategy.entry("Long", strategy.long) | |
| if bearish | |
| strategy.close("Long") | |
| strategy.entry("Short", strategy.short) | |
| if ta.crossover(fast_ema, slow_ema) and strategy.position_size < 0 | |
| strategy.close("Short") | |
| // Trailing stop | |
| strategy.exit("Trail L", "Long", trail_price=strategy.position_avg_price, trail_offset=close * trailing_pct / 100 / syminfo.mintick) | |
| strategy.exit("Trail S", "Short", trail_price=strategy.position_avg_price, trail_offset=close * trailing_pct / 100 / syminfo.mintick) | |
| // Visuals | |
| plot(fast_ema, color=color.new(color.lime, 0), title="Fast EMA") | |
| plot(slow_ema, color=color.new(color.orange, 0), title="Slow EMA") | |
| plotshape(vol_spike, style=shape.diamond, location=location.belowbar, color=color.yellow, size=size.tiny, title="Vol Surge") | |
| bgcolor(strategy.position_size > 0 ? color.new(color.green, 93) : strategy.position_size < 0 ? color.new(color.red, 93) : na) | |
| ''', | |
| }, | |
| "nifty_orb_intraday": { | |
| "id": "nifty_orb_intraday", | |
| "name": "Nifty/Indian Market ORB", | |
| "category": "Intraday", | |
| "description": "Opening Range Breakout for Indian markets (NSE/BSE). Captures first 15-min range breakout on NIFTY50, Bank Nifty, or individual stocks.", | |
| "parameters": {"orb_minutes": 15, "target_mult": 2.0, "sl_buffer_pct": 0.1}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("ORB - Indian Market Intraday", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.03) | |
| // Inputs | |
| orb_minutes = input.int({orb_minutes}, title="ORB Minutes", options=[5, 15, 30]) | |
| target_mult = input.float({target_mult}, title="Target Multiplier (x Range)", step=0.1) | |
| sl_buffer = input.float({sl_buffer_pct}, title="SL Buffer %", step=0.05) | |
| session_end = input.int(1500, title="Exit by (HHMM)", tooltip="Close all positions by this time. Indian market closes at 1530.") | |
| // Session tracking (IST = UTC+5:30) | |
| is_new_day = ta.change(time("D")) != 0 | |
| cur_time_int = hour * 100 + minute | |
| // ORB range calculation | |
| var float orb_high = na | |
| var float orb_low = na | |
| var bool orb_set = false | |
| if is_new_day | |
| orb_high := high | |
| orb_low := low | |
| orb_set := false | |
| else if not orb_set | |
| orb_high := math.max(nz(orb_high), high) | |
| orb_low := math.min(nz(orb_low), low) | |
| if bar_index - ta.valuewhen(is_new_day, bar_index, 0) >= orb_minutes / timeframe.multiplier | |
| orb_set := true | |
| orb_range = orb_high - orb_low | |
| target_pts = orb_range * target_mult | |
| // Breakout entries (only after ORB is set, before session end) | |
| long_entry = orb_set and ta.crossover(close, orb_high * (1 + sl_buffer / 100)) and cur_time_int < session_end | |
| short_entry = orb_set and ta.crossunder(close, orb_low * (1 - sl_buffer / 100)) and cur_time_int < session_end | |
| if long_entry | |
| strategy.entry("ORB Long", strategy.long) | |
| if short_entry | |
| strategy.entry("ORB Short", strategy.short) | |
| // Targets and stops | |
| strategy.exit("Exit L", "ORB Long", stop=orb_low, limit=orb_high + target_pts) | |
| strategy.exit("Exit S", "ORB Short", stop=orb_high, limit=orb_low - target_pts) | |
| // Force close at session end | |
| if cur_time_int >= session_end | |
| strategy.close_all(comment="Session End") | |
| // Visuals | |
| plot(orb_set ? orb_high : na, color=color.green, style=plot.style_linebr, linewidth=2, title="ORB High") | |
| plot(orb_set ? orb_low : na, color=color.red, style=plot.style_linebr, linewidth=2, title="ORB Low") | |
| bgcolor(is_new_day ? color.new(color.white, 95) : na) | |
| ''', | |
| }, | |
| "commodity_trend": { | |
| "id": "commodity_trend", | |
| "name": "Commodity Trend Follower", | |
| "category": "Commodities", | |
| "description": "Donchian Channel breakout system for commodities (Gold, Silver, Crude Oil, Natural Gas). Classic turtle-trading inspired approach.", | |
| "parameters": {"entry_len": 55, "exit_len": 20, "atr_stop": 3.0}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Commodity Trend Follower", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.05) | |
| // Inputs | |
| entry_len = input.int({entry_len}, title="Entry Channel Length (Donchian)") | |
| exit_len = input.int({exit_len}, title="Exit Channel Length") | |
| atr_stop = input.float({atr_stop}, title="ATR Trailing Stop Multiplier", step=0.1) | |
| atr_len = input.int(20, title="ATR Length") | |
| // Donchian Channels | |
| entry_high = ta.highest(high, entry_len) | |
| entry_low = ta.lowest(low, entry_len) | |
| exit_high = ta.highest(high, exit_len) | |
| exit_low = ta.lowest(low, exit_len) | |
| atr = ta.atr(atr_len) | |
| // Trend filter — only trade in direction of 100-period SMA | |
| sma_100 = ta.sma(close, 100) | |
| up_trend = close > sma_100 | |
| dn_trend = close < sma_100 | |
| // Entries — breakout of Donchian channel | |
| long_entry = ta.crossover(close, entry_high[1]) and up_trend | |
| short_entry = ta.crossunder(close, entry_low[1]) and dn_trend | |
| if long_entry | |
| strategy.entry("Long", strategy.long) | |
| if short_entry | |
| strategy.entry("Short", strategy.short) | |
| // Exits — opposite Donchian channel or ATR trailing stop | |
| if strategy.position_size > 0 and close < exit_low[1] | |
| strategy.close("Long", comment="Donchian Exit") | |
| if strategy.position_size < 0 and close > exit_high[1] | |
| strategy.close("Short", comment="Donchian Exit") | |
| strategy.exit("Trail L", "Long", stop=strategy.position_avg_price - atr * atr_stop) | |
| strategy.exit("Trail S", "Short", stop=strategy.position_avg_price + atr * atr_stop) | |
| // Visuals | |
| upper = plot(entry_high, color=color.new(color.green, 50), title="Entry High") | |
| lower = plot(entry_low, color=color.new(color.red, 50), title="Entry Low") | |
| fill(upper, lower, color=color.new(color.gray, 95)) | |
| plot(sma_100, color=color.white, linewidth=2, title="100 SMA Trend") | |
| ''', | |
| }, | |
| "futures_scalping": { | |
| "id": "futures_scalping", | |
| "name": "Futures Intraday Scalper", | |
| "category": "Futures", | |
| "description": "High-frequency intraday scalping for futures (ES, NQ, NF, BNF, CL). Uses VWAP + EMA pullback entries with tight risk management.", | |
| "parameters": {"ema_len": 9, "risk_reward": 2.0, "atr_sl_mult": 1.0}, | |
| "code": ''' | |
| //@version=5 | |
| strategy("Futures Intraday Scalper", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.02) | |
| // Inputs | |
| ema_len = input.int({ema_len}, title="Fast EMA Length") | |
| rr_ratio = input.float({risk_reward}, title="Risk:Reward Ratio", step=0.1) | |
| atr_sl_mult = input.float({atr_sl_mult}, title="ATR Stop Multiplier", step=0.1) | |
| atr_len = input.int(14, title="ATR Length") | |
| max_trades = input.int(6, title="Max Trades Per Day") | |
| // Indicators | |
| ema_fast = ta.ema(close, ema_len) | |
| ema_mid = ta.ema(close, 21) | |
| vwap = ta.vwap(hlc3) | |
| atr = ta.atr(atr_len) | |
| rsi = ta.rsi(close, 7) | |
| // Count today's trades | |
| var int trades_today = 0 | |
| if ta.change(time("D")) != 0 | |
| trades_today := 0 | |
| // Bullish scalp: price above VWAP, pulls back to EMA, then bounces | |
| bull_setup = close > vwap and low <= ema_fast and close > ema_fast and rsi > 40 and rsi < 70 | |
| bear_setup = close < vwap and high >= ema_fast and close < ema_fast and rsi < 60 and rsi > 30 | |
| // Entries with trade count limit | |
| if bull_setup and trades_today < max_trades | |
| strategy.entry("Scalp L", strategy.long) | |
| trades_today += 1 | |
| if bear_setup and trades_today < max_trades | |
| strategy.entry("Scalp S", strategy.short) | |
| trades_today += 1 | |
| // Tight exits | |
| sl = atr * atr_sl_mult | |
| tp = sl * rr_ratio | |
| strategy.exit("Exit L", "Scalp L", stop=strategy.position_avg_price - sl, limit=strategy.position_avg_price + tp) | |
| strategy.exit("Exit S", "Scalp S", stop=strategy.position_avg_price + sl, limit=strategy.position_avg_price - tp) | |
| // Visuals | |
| plot(vwap, color=color.new(color.yellow, 0), linewidth=2, title="VWAP") | |
| plot(ema_fast, color=color.new(color.cyan, 0), title="Fast EMA") | |
| plot(ema_mid, color=color.new(color.gray, 30), title="21 EMA") | |
| bgcolor(close > vwap ? color.new(color.green, 97) : color.new(color.red, 97)) | |
| ''', | |
| }, | |
| } | |
| # ── LLM Generator ─────────────────────────────────────────────────────── | |
| PINE_SCRIPT_SYSTEM_PROMPT = """You are an expert TradingView Pine Script v5 developer. | |
| Generate ONLY valid Pine Script v5 code. Follow these rules: | |
| 1. Always start with //@version=5 | |
| 2. Use strategy() for backtestable strategies | |
| 3. Use input.int(), input.float(), input.bool() for user parameters | |
| 4. Use ta.* namespace for all built-in indicators | |
| 5. Use strategy.entry() and strategy.exit() for order management | |
| 6. Include stop loss and take profit via strategy.exit() | |
| 7. Add plot() calls for visual indicators on chart | |
| 8. Use proper variable naming (snake_case) | |
| 9. Add commission via strategy() declaration | |
| 10. Make the code clean, well-commented, and production-ready | |
| Output ONLY the Pine Script code, nothing else.""" | |
| async def generate_from_description( | |
| description: str, | |
| parameters: Optional[Dict[str, Any]] = None, | |
| ) -> Dict[str, Any]: | |
| """ | |
| Generate Pine Script v5 code from a natural language description. | |
| Uses Groq LLM for intelligent code generation. | |
| """ | |
| if not _settings.groq_api_key: | |
| # Fallback: find closest template match | |
| return _template_fallback(description) | |
| user_prompt = f"Generate a complete TradingView Pine Script v5 strategy for:\n\n{description}" | |
| if parameters: | |
| user_prompt += f"\n\nUse these parameters: {parameters}" | |
| try: | |
| async with aiohttp.ClientSession() as session: | |
| async with session.post( | |
| "https://api.groq.com/openai/v1/chat/completions", | |
| headers={ | |
| "Authorization": f"Bearer {_settings.groq_api_key}", | |
| "Content-Type": "application/json", | |
| }, | |
| json={ | |
| "model": "llama-3.3-70b-versatile", | |
| "messages": [ | |
| {"role": "system", "content": PINE_SCRIPT_SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| "temperature": 0.2, | |
| "max_tokens": 3000, | |
| }, | |
| timeout=aiohttp.ClientTimeout(total=30), | |
| ) as resp: | |
| if resp.status != 200: | |
| return _template_fallback(description) | |
| data = await resp.json() | |
| code = data["choices"][0]["message"]["content"] | |
| # Clean code (remove markdown fences if present) | |
| if "```" in code: | |
| parts = code.split("```") | |
| for part in parts: | |
| if "//@version=5" in part: | |
| code = part.replace("pine", "", 1).strip() | |
| break | |
| return { | |
| "code": code, | |
| "source": "llm", | |
| "description": description, | |
| "valid": "//@version=5" in code, | |
| } | |
| except Exception as e: | |
| logger.warning("LLM generation failed: %s", e) | |
| return _template_fallback(description) | |
| def generate_from_template( | |
| template_id: str, | |
| parameters: Optional[Dict[str, Any]] = None, | |
| ) -> Dict[str, Any]: | |
| """Generate Pine Script from a pre-built template.""" | |
| template = STRATEGY_TEMPLATES.get(template_id) | |
| if not template: | |
| raise ValueError(f"Template '{template_id}' not found") | |
| # Merge default params with overrides | |
| params = {**template["parameters"]} | |
| if parameters: | |
| params.update(parameters) | |
| # Format code with parameters | |
| code = template["code"].strip() | |
| for key, value in params.items(): | |
| code = code.replace(f"{{{key}}}", str(value)) | |
| return { | |
| "code": code, | |
| "source": "template", | |
| "template_id": template_id, | |
| "template_name": template["name"], | |
| "parameters": params, | |
| "valid": True, | |
| } | |
| async def customize_code( | |
| existing_code: str, | |
| modification: str, | |
| ) -> Dict[str, Any]: | |
| """Modify existing Pine Script via LLM.""" | |
| if not _settings.groq_api_key: | |
| return {"code": existing_code, "source": "unchanged", "error": "LLM unavailable"} | |
| try: | |
| async with aiohttp.ClientSession() as session: | |
| async with session.post( | |
| "https://api.groq.com/openai/v1/chat/completions", | |
| headers={ | |
| "Authorization": f"Bearer {_settings.groq_api_key}", | |
| "Content-Type": "application/json", | |
| }, | |
| json={ | |
| "model": "llama-3.3-70b-versatile", | |
| "messages": [ | |
| {"role": "system", "content": PINE_SCRIPT_SYSTEM_PROMPT}, | |
| {"role": "user", "content": f"Modify this Pine Script code:\n\n```\n{existing_code}\n```\n\nModification: {modification}\n\nOutput only the complete modified code."}, | |
| ], | |
| "temperature": 0.2, | |
| "max_tokens": 3000, | |
| }, | |
| timeout=aiohttp.ClientTimeout(total=30), | |
| ) as resp: | |
| if resp.status != 200: | |
| return {"code": existing_code, "source": "unchanged", "error": "LLM failed"} | |
| data = await resp.json() | |
| code = data["choices"][0]["message"]["content"] | |
| if "```" in code: | |
| parts = code.split("```") | |
| for part in parts: | |
| if "//@version=5" in part: | |
| code = part.replace("pine", "", 1).strip() | |
| break | |
| return {"code": code, "source": "llm_modified", "valid": "//@version=5" in code} | |
| except Exception as e: | |
| logger.warning("LLM customization failed: %s", e) | |
| return {"code": existing_code, "source": "unchanged", "error": str(e)} | |
| def get_all_templates() -> List[Dict[str, Any]]: | |
| """Return all available templates (without full code for listing).""" | |
| return [ | |
| { | |
| "id": t["id"], | |
| "name": t["name"], | |
| "category": t["category"], | |
| "description": t["description"], | |
| "parameters": t["parameters"], | |
| } | |
| for t in STRATEGY_TEMPLATES.values() | |
| ] | |
| def _template_fallback(description: str) -> Dict[str, Any]: | |
| """Keyword-based template matching when LLM is unavailable.""" | |
| desc_lower = description.lower() | |
| best_match = "sma_crossover" # default | |
| keywords = { | |
| "rsi": "rsi_reversal", | |
| "macd": "macd_signal", | |
| "bollinger": "bollinger_breakout", | |
| "supertrend": "supertrend", | |
| "ema": "ema_ribbon", | |
| "stochastic": "stochastic_rsi_combo", | |
| "z-score": "mean_reversion_zscore", | |
| "mean reversion": "mean_reversion_zscore", | |
| "trailing": "atr_trailing_stop", | |
| "atr": "atr_trailing_stop", | |
| "multi": "multi_timeframe", | |
| "timeframe": "multi_timeframe", | |
| "vwap": "vwap_strategy", | |
| "ichimoku": "ichimoku_cloud", | |
| "cloud": "ichimoku_cloud", | |
| "crossover": "sma_crossover", | |
| "moving average": "sma_crossover", | |
| "hedge": "portfolio_hedge", | |
| "hedging": "portfolio_hedge", | |
| "portfolio hedge": "portfolio_hedge", | |
| "pairs": "pairs_trading_hedge", | |
| "pairs trading": "pairs_trading_hedge", | |
| "arbitrage": "pairs_trading_hedge", | |
| "tail risk": "tail_risk_hedge", | |
| "black swan": "tail_risk_hedge", | |
| "protection": "tail_risk_hedge", | |
| "protective": "tail_risk_hedge", | |
| "correlation": "correlation_hedge", | |
| "inverse": "correlation_hedge", | |
| "forex": "forex_session_breakout", | |
| "session": "forex_session_breakout", | |
| "london": "forex_session_breakout", | |
| "currency": "forex_session_breakout", | |
| "crypto": "crypto_momentum", | |
| "bitcoin": "crypto_momentum", | |
| "btc": "crypto_momentum", | |
| "eth": "crypto_momentum", | |
| "altcoin": "crypto_momentum", | |
| "nifty": "nifty_orb_intraday", | |
| "indian": "nifty_orb_intraday", | |
| "nse": "nifty_orb_intraday", | |
| "orb": "nifty_orb_intraday", | |
| "opening range": "nifty_orb_intraday", | |
| "bank nifty": "nifty_orb_intraday", | |
| "commodity": "commodity_trend", | |
| "gold": "commodity_trend", | |
| "crude": "commodity_trend", | |
| "oil": "commodity_trend", | |
| "silver": "commodity_trend", | |
| "donchian": "commodity_trend", | |
| "turtle": "commodity_trend", | |
| "futures": "futures_scalping", | |
| "scalp": "futures_scalping", | |
| "scalping": "futures_scalping", | |
| "intraday": "nifty_orb_intraday", | |
| "f&o": "futures_scalping", | |
| } | |
| for keyword, template_id in keywords.items(): | |
| if keyword in desc_lower: | |
| best_match = template_id | |
| break | |
| result = generate_from_template(best_match) | |
| result["source"] = "template_fallback" | |
| result["matched_keyword"] = best_match | |
| return result | |