Spaces:
Running
Running
| //@version=6 | |
| indicator("Munger Engine Strategy", overlay=true) | |
| // ========================================== | |
| // INPUTS | |
| // ========================================== | |
| grp_fund = "Fundamentals" | |
| roe_min = input.float(15.0, "Min ROE (%)", group=grp_fund) | |
| de_max = input.float(50.0, "Max Debt/Equity (%)", group=grp_fund) | |
| grp_strat = "Strategy (Weekly)" | |
| sma_len = input.int(200, "SMA Length", group=grp_strat) | |
| sma_len_100 = input.int(100, "SMA 100 Length", group=grp_strat) | |
| sma_len_50 = input.int(50, "SMA 50 Length", group=grp_strat) | |
| lookback = input.int(4, "Touch Lookback (Weeks)", group=grp_strat) | |
| show_markers = input.bool(true, "Show Entry Markers (with Tooltip)", group=grp_strat) | |
| grp_risk = "Risk Management (Daily ATR)" | |
| atr_len = input.int(14, "ATR Length", group=grp_risk) | |
| atr_mult = input.float(2.0, "Stop Loss ATR Mult", group=grp_risk) | |
| rr_ratio = input.float(3.0, "Risk/Reward Ratio", group=grp_risk) | |
| account_equity = input.float(100000.0, "Account Equity ($)", group=grp_risk) | |
| risk_per_trade_pct = input.float(0.5, "Risk Per Trade (%)", group=grp_risk) / 100 | |
| entry_limit_buffer_bps = input.float(75.0, "Entry Limit Buffer (bps)", group=grp_risk) | |
| // ========================================== | |
| // DATA FETCHING (FUNDAMENTALS) | |
| // ========================================== | |
| // Fetch Quarterly data for ROE and Debt/Equity | |
| // We manually calculate D/E to avoid "Symbol resolve error" on some tickers/exchanges | |
| roe = request.financial(syminfo.tickerid, "RETURN_ON_EQUITY", "FQ") | |
| term_debt = request.financial(syminfo.tickerid, "TOTAL_DEBT", "FQ") | |
| term_equity = request.financial(syminfo.tickerid, "TOTAL_EQUITY", "FQ") | |
| // Calculate Debt to Equity (%) | |
| float de = na | |
| if not na(term_debt) and not na(term_equity) and term_equity != 0 | |
| de := (term_debt / term_equity) * 100 | |
| // Quality Check | |
| is_quality = (not na(roe) and roe > roe_min) and (not na(de) and de < de_max) | |
| // ========================================== | |
| // CALCULATION (WEEKLY TIMEFRAME) | |
| // ========================================== | |
| // We define a tuple function to run inside request.security to ensure logic runs on Weekly bars | |
| // regardless of the chart's timeframe. | |
| calc_weekly_strategy() => | |
| float ma = ta.sma(close, sma_len) | |
| float ma100 = ta.sma(close, sma_len_100) | |
| float ma50 = ta.sma(close, sma_len_50) | |
| bool touched = false | |
| // Check if Low touched MA in the last N weeks | |
| for i = 0 to lookback - 1 | |
| if low[i] <= ma[i] | |
| touched := true | |
| bool green = close > open | |
| // Cond: Close of previous week AND current week > SMA200 | |
| // Note: in 'calc_weekly_strategy' context, close[1] is previous week's close | |
| bool above_ma_now = close > ma | |
| bool above_ma_prev = close[1] > ma[1] | |
| [] | |
| // Request Weekly Data | |
| // lookahead=barmerge.lookahead_on forces strict non-repainting historical data if desired, | |
| // but for an indicator 'off' or default is usually fine for current state. | |
| [] = request.security(syminfo.tickerid, "W", calc_weekly_strategy()) | |
| // Fetch Daily ATR for Risk Calculation (matches engine logic) | |
| d_atr = request.security(syminfo.tickerid, "D", ta.atr(atr_len)) | |
| // ========================================== | |
| // SIGNALS | |
| // ========================================== | |
| buy_signal = is_quality and w_touched and w_green and w_above_now and w_above_prev | |
| // ========================================== | |
| // PLOTTING | |
| // ========================================== | |
| plot(w_ma, "Weekly SMA 200", color=color.blue, linewidth=2) | |
| plot(w_ma100, "Weekly SMA 100", color=color.orange, linewidth=1) | |
| plot(w_ma50, "Weekly SMA 50", color=color.yellow, linewidth=1) | |
| // Plot Signal | |
| // We combine Marker and Distance Info into a single label with tooltip for "Hover" effect | |
| if buy_signal and show_markers | |
| // Distance | |
| dist = ((close - w_ma) / w_ma) * 100 | |
| txt_dist = str.tostring(dist, "#.2") + "%" | |
| // Trade Plan Calculation | |
| entry_stop = high + syminfo.mintick | |
| entry_limit = entry_stop * (1 + entry_limit_buffer_bps / 10000) | |
| risk = d_atr * atr_mult | |
| sl_price = entry_stop - risk | |
| tp_price = entry_stop + (risk * rr_ratio) | |
| // Position Sizing | |
| risk_dollars = account_equity * risk_per_trade_pct | |
| // Check for valid risk to avoid division by zero | |
| float shares = 0.0 | |
| if risk > 0 | |
| shares := math.floor(risk_dollars / risk) | |
| position_value = shares * entry_stop | |
| // Build Tooltip | |
| tt = "Distance to SMA: " + txt_dist + "\n" + | |
| "------------------------\n" + | |
| "TRADE PLAN (Daily ATR)\n" + | |
| "Order Type: STOP-LIMIT\n" + | |
| "Buy Stop: " + str.tostring(entry_stop, format.mintick) + "\n" + | |
| "Buy Limit: " + str.tostring(entry_limit, format.mintick) + "\n" + | |
| "Stop Loss: " + str.tostring(sl_price, format.mintick) + "\n" + | |
| "Take Profit: " + str.tostring(tp_price, format.mintick) + "\n" + | |
| "Risk (R): " + str.tostring(risk, format.mintick) + "\n" + | |
| "------------------------\n" + | |
| "POSITION SIZING\n" + | |
| "Shares: " + str.tostring(shares, "#") + "\n" + | |
| "Value: $" + str.tostring(position_value, "#.##") + "\n" + | |
| "Risked: $" + str.tostring(risk_dollars, "#.##") | |
| label.new(bar_index, low, text="", color=color.green, style=label.style_triangleup, size=size.tiny, yloc=yloc.belowbar, tooltip=tt) | |
| // ========================================== | |
| // INFO PANEL | |
| // ========================================== | |
| var table panel = table.new(position.top_right, 2, 4, border_width=1) | |
| if barstate.islast | |
| // Header | |
| table.cell(panel, 0, 0, "Metric", bgcolor=color.gray, text_color=color.white) | |
| table.cell(panel, 1, 0, "Value", bgcolor=color.gray, text_color=color.white) | |
| // ROE | |
| c_roe = (not na(roe) and roe > roe_min) ? color.new(color.green, 60) : color.new(color.red, 60) | |
| table.cell(panel, 0, 1, "ROE", bgcolor=c_roe) | |
| table.cell(panel, 1, 1, str.tostring(roe, format.percent), bgcolor=c_roe) | |
| // D/E | |
| c_de = (not na(de) and de < de_max) ? color.new(color.green, 60) : color.new(color.red, 60) | |
| table.cell(panel, 0, 2, "Debt/Eq", bgcolor=c_de) | |
| table.cell(panel, 1, 2, str.tostring(de, format.percent), bgcolor=c_de) | |
| // Status | |
| status_txt = is_quality ? "QUALITY" : "AVOID" | |
| c_status = is_quality ? color.green : color.red | |
| table.cell(panel, 0, 3, "Status", bgcolor=c_status, text_color=color.white) | |
| table.cell(panel, 1, 3, status_txt, bgcolor=c_status, text_color=color.white) |