const predef = require("./tools/predef"); const meta = require("./tools/meta"); const { du, op, px } = require("./tools/graphics"); class RetracementZones { init() { this.highs = []; this.lows = []; this.closes = []; this.buyVols = []; this.sellVols = []; this.totalBuyVol = 0; this.totalSellVol = 0; this._prevDraw = 0; this._lastSellEntry = 0; this._lastBuyEntry = 0; this._prevSwingHigh = 0; this.sellBlocked = false; this.sellPotPrice = 0; this.sellBlockPrice = 0; this.sellRetraceDone = false; this._prevSwingLow = 0; this.buyBlocked = false; this.buyPotPrice = 0; this.buyBlockPrice = 0; this.buyRetraceDone = false; this.sellDangerPrice = 0; this.sellDangerOrders = 0; this.sellDangerPts = 0; this.buyDangerPrice = 0; this.buyDangerOrders = 0; this.buyDangerPts = 0; this.sellPotOrders = 0; this.buyPotOrders = 0; this.sellZoneVol = 0; this.buyZoneVol = 0; this.recentBuyVols = []; this.recentSellVols = []; this.recentBuyTotal = 0; this.recentSellTotal = 0; } map(d, index) { try { var tickSize = this.contractInfo.tickSize || 0.25; var precision = this._getPrecision(tickSize); var lookbackBars = this.props.lookbackBars; var retracePct = this.props.retracePct; var minTrendPoints = this.props.minTrendPoints; var zoneHeight = this.props.zoneHeight; var retraceTargetPts = this.props.retraceTargetPts; var potZoneHeight = this.props.potZoneHeight; var minVolume = this.props.minVolume; var buyColor = this.props.buyColor; var sellColor = this.props.sellColor; var zoneOpacity = this.props.zoneOpacity; var fontSize = this.props.fontSize; var microRetracePts = this.props.microRetracePts || 10; var close = d.close(); var open = d.open(); var high = d.high(); var low = d.low(); var vol = d.volume(); if (close === undefined || close === null || isNaN(close)) return this._safeReturn(); if (open === undefined || open === null || isNaN(open)) open = close; if (high === undefined || high === null || isNaN(high)) high = close; if (low === undefined || low === null || isNaN(low)) low = close; if (vol === undefined || vol === null || isNaN(vol)) vol = 0; var barBuyVol = 0; var barSellVol = 0; if (vol > 0) { if (typeof d.offerVolume === "function") { barBuyVol = d.offerVolume() || 0; barSellVol = d.bidVolume() || 0; } else if (typeof d.askVolume === "function") { barBuyVol = d.askVolume() || 0; barSellVol = d.bidVolume() || 0; } else { if (close >= open) barBuyVol = vol; else barSellVol = vol; } } if (isNaN(barBuyVol)) barBuyVol = 0; if (isNaN(barSellVol)) barSellVol = 0; this.buyVols.push(barBuyVol); this.sellVols.push(barSellVol); this.totalBuyVol += barBuyVol; this.totalSellVol += barSellVol; this.highs.push(high); this.lows.push(low); this.closes.push(close); if (this.highs.length > lookbackBars) { this.highs.shift(); this.lows.shift(); this.closes.shift(); this.totalBuyVol -= this.buyVols.shift(); this.totalSellVol -= this.sellVols.shift(); } if (this.totalBuyVol < 0) this.totalBuyVol = 0; if (this.totalSellVol < 0) this.totalSellVol = 0; var recentWindow = 10; this.recentBuyVols.push(barBuyVol); this.recentSellVols.push(barSellVol); this.recentBuyTotal += barBuyVol; this.recentSellTotal += barSellVol; if (this.recentBuyVols.length > recentWindow) { this.recentBuyTotal -= this.recentBuyVols.shift(); this.recentSellTotal -= this.recentSellVols.shift(); } if (this.recentBuyTotal < 0) this.recentBuyTotal = 0; if (this.recentSellTotal < 0) this.recentSellTotal = 0; if (this.highs.length < lookbackBars) { return this._safeReturn(); } var swingHigh = Math.max.apply(null, this.highs); var swingLow = Math.min.apply(null, this.lows); if (isNaN(swingHigh) || isNaN(swingLow)) return this._safeReturn(); var range = swingHigh - swingLow; if (range < minTrendPoints || range <= 0) { return this._safeReturn(); } var retracement = range * retracePct / 100; var retracePts = Math.round(retracement); var halfH = zoneHeight * tickSize * 0.5; var potHalfH = zoneHeight * tickSize * 0.5; var blockSensitivity = this.props.blockSensitivity; var threshold = Math.max(range * blockSensitivity / 100, 2 * tickSize); var maxDrift = retraceTargetPts * 0.5; var potDetectRadius = potZoneHeight * tickSize; var sellCalc = this._roundPrice(swingLow + retracement, tickSize); var buyCalc = this._roundPrice(swingHigh - retracement, tickSize); if (isNaN(sellCalc)) sellCalc = this._lastSellEntry; if (isNaN(buyCalc)) buyCalc = this._lastBuyEntry; if (isNaN(sellCalc) || isNaN(buyCalc)) return this._safeReturn(); var sellDisplay = this._roundPrice(swingHigh + halfH * 2, tickSize); var buyDisplay = this._roundPrice(swingLow - halfH * 2, tickSize); var isCurrentBar = typeof d.isLast === "function" && d.isLast(); // ============================================================ // BLOCAGE PAR DOMINANCE RECENTE (10 dernieres barres) // Qui gagne en temps reel ? Vendeurs ou Acheteurs // ============================================================ var sellDominant = this.recentSellTotal > this.recentBuyTotal; var buyDominant = this.recentBuyTotal > this.recentSellTotal; // --- SELL BLOCKAGE : vendeurs gagnent --- if (sellDominant) { if (!this.sellBlocked) { this.sellBlocked = true; this.sellRetraceDone = false; this.sellDangerPrice = 0; this.sellDangerOrders = 0; this.sellDangerPts = 0; this.sellPotOrders = 0; this.sellZoneVol = 0; } this.sellBlockPrice = swingHigh; this.sellPotPrice = this._roundPrice(swingHigh - retraceTargetPts, tickSize); } else { this.sellBlocked = false; this.sellPotPrice = 0; this.sellBlockPrice = 0; this.sellRetraceDone = false; this.sellDangerPrice = 0; this.sellDangerOrders = 0; this.sellDangerPts = 0; this.sellPotOrders = 0; this.sellZoneVol = 0; } if (this.sellBlocked && this.sellPotPrice > 0) { this.sellRetraceDone = (low <= this.sellPotPrice); } if (this.sellBlocked) { var sellNetOpposing = barBuyVol - barSellVol; if (sellNetOpposing > 0) { var ptsDown = this.sellBlockPrice - close; if (ptsDown > 0 && ptsDown < retraceTargetPts) { this.sellDangerOrders += sellNetOpposing; this.sellDangerPrice = close; this.sellDangerPts = Math.round(ptsDown); } } var sellNetZone = barSellVol - barBuyVol; if (sellNetZone > 0) this.sellZoneVol += sellNetZone; } if (!isCurrentBar) this._prevSwingHigh = swingHigh; // --- BUY BLOCKAGE : acheteurs gagnent --- if (buyDominant) { if (!this.buyBlocked) { this.buyBlocked = true; this.buyRetraceDone = false; this.buyDangerPrice = 0; this.buyDangerOrders = 0; this.buyDangerPts = 0; this.buyPotOrders = 0; this.buyZoneVol = 0; } this.buyBlockPrice = swingLow; this.buyPotPrice = this._roundPrice(swingLow + retraceTargetPts, tickSize); } else { this.buyBlocked = false; this.buyPotPrice = 0; this.buyBlockPrice = 0; this.buyRetraceDone = false; this.buyDangerPrice = 0; this.buyDangerOrders = 0; this.buyDangerPts = 0; this.buyPotOrders = 0; this.buyZoneVol = 0; } if (this.buyBlocked && this.buyPotPrice > 0) { this.buyRetraceDone = (high >= this.buyPotPrice); } if (this.buyBlocked) { var buyNetOpposing = barSellVol - barBuyVol; if (buyNetOpposing > 0) { var ptsUp = close - this.buyBlockPrice; if (ptsUp > 0 && ptsUp < retraceTargetPts) { this.buyDangerOrders += buyNetOpposing; this.buyDangerPrice = close; this.buyDangerPts = Math.round(ptsUp); } } var buyNetZone = barBuyVol - barSellVol; if (buyNetZone > 0) this.buyZoneVol += buyNetZone; } if (!isCurrentBar) this._prevSwingLow = swingLow; this._lastSellEntry = sellCalc; this._lastBuyEntry = buyCalc; // ============================================================ // DRAWING // ============================================================ var drawZones = false; if (typeof d.isLast === "function") { drawZones = d.isLast(); } else { if (this._prevDraw === 0 || Math.abs(close - this._prevDraw) >= tickSize) { drawZones = true; this._prevDraw = close; } } var items = []; var showBuyZone = minVolume <= 0 || this.totalBuyVol >= minVolume; var showSellZone = minVolume <= 0 || this.totalSellVol >= minVolume; var xStart = index > 200 ? index - 200 : 0; var xEnd = index + 200; if (drawZones) { // ===== ZONE VENDEUR (HAUT) ===== if (showSellZone) { items.push({ tag: "Shapes", key: "sz_" + index, primitives: [{ tag: "Rectangle", position: { x: du(xStart), y: du(sellDisplay + halfH) }, size: { width: du(xEnd - xStart), height: op(du(sellDisplay - halfH), '-', du(sellDisplay + halfH)) } }], fillStyle: { color: sellColor, opacity: this.sellBlocked ? zoneOpacity + 20 : zoneOpacity }, lineStyle: { lineWidth: this.sellBlocked ? 3 : 2, color: sellColor } }); var sellText = "VENDEUR " + sellDisplay.toFixed(precision) + " | " + this.recentSellTotal + " r\u00E9cents (" + retracePts + " pts)"; if (this.sellBlocked) { sellText += " \u25BC VENTE DOMINE"; if (this.sellRetraceDone) sellText += " RETRACE OK"; } items.push({ tag: "Text", key: "st_" + index, point: { x: du(index), y: du(sellDisplay) }, text: sellText, style: { fontSize: fontSize, fontWeight: "bold", fill: "#FFFFFF" }, textAlignment: "centerMiddle" }); } // ===== CALCULS POINTS TEMPS REEL ===== var sellRetraceLive = Math.round(swingHigh - close); var buyRetraceLive = Math.round(close - swingLow); var recentNetSell = Math.max(0, this.recentSellTotal - this.recentBuyTotal); var recentNetBuy = Math.max(0, this.recentBuyTotal - this.recentSellTotal); var sellNetDiff = Math.abs(this.totalSellVol - this.totalBuyVol); var sellForceOk = this.totalSellVol > this.totalBuyVol; var buyNetDiff = Math.abs(this.totalBuyVol - this.totalSellVol); var buyForceOk = this.totalBuyVol > this.totalSellVol; // ===== ZONE VENTE NET (micro retrace) ===== var netSellCount = recentNetSell; var netBuyCount = recentNetBuy; var zoneVenteY = this._roundPrice(close + microRetracePts / 2, tickSize); var zoneAchatY = this._roundPrice(close - microRetracePts / 2, tickSize); var zvLineH = tickSize * 2; if (showSellZone && zoneVenteY > 0) { items.push({ tag: "Shapes", key: "zvl_" + index, primitives: [{ tag: "Rectangle", position: { x: du(xStart), y: du(zoneVenteY + zvLineH) }, size: { width: du(xEnd - xStart), height: op(du(zoneVenteY - zvLineH), '-', du(zoneVenteY + zvLineH)) } }], fillStyle: { color: "#FFA000", opacity: 60 }, lineStyle: { lineWidth: 2, color: "#FFD600" } }); items.push({ tag: "Text", key: "zvt_" + index, point: { x: du(index), y: du(zoneVenteY) }, text: "zone vente " + zoneVenteY.toFixed(precision) + " | " + netSellCount + " net | (" + microRetracePts + ") pts", style: { fontSize: fontSize, fontWeight: "bold", fill: "#FFD600" }, textAlignment: "centerMiddle" }); } // ===== ZONE ACHAT NET (micro retrace) ===== if (showBuyZone && zoneAchatY > 0) { items.push({ tag: "Shapes", key: "zal_" + index, primitives: [{ tag: "Rectangle", position: { x: du(xStart), y: du(zoneAchatY + zvLineH) }, size: { width: du(xEnd - xStart), height: op(du(zoneAchatY - zvLineH), '-', du(zoneAchatY + zvLineH)) } }], fillStyle: { color: "#00838F", opacity: 60 }, lineStyle: { lineWidth: 2, color: "#00BCD4" } }); items.push({ tag: "Text", key: "zat_" + index, point: { x: du(index), y: du(zoneAchatY) }, text: "zone achat " + zoneAchatY.toFixed(precision) + " | " + netBuyCount + " net | (" + microRetracePts + ") pts", style: { fontSize: fontSize, fontWeight: "bold", fill: "#00BCD4" }, textAlignment: "centerMiddle" }); } // ===== POTENTIEL VENTE (TOUJOURS VISIBLE) ===== if (showSellZone) { var sellPotDisplay = this.sellBlocked && this.sellPotPrice > 0 ? this.sellPotPrice : this._roundPrice(swingHigh - retraceTargetPts, tickSize); if (sellPotDisplay > 0) { items.push({ tag: "Shapes", key: "spz_" + index, primitives: [{ tag: "Rectangle", position: { x: du(xStart), y: du(sellPotDisplay + potHalfH) }, size: { width: du(xEnd - xStart), height: op(du(sellPotDisplay - potHalfH), '-', du(sellPotDisplay + potHalfH)) } }], fillStyle: { color: sellColor, opacity: this.sellBlocked ? zoneOpacity : zoneOpacity - 10 }, lineStyle: { lineWidth: 1, color: sellColor } }); var sellPotText = sellPotDisplay.toFixed(precision) + " | retracement -" + retraceTargetPts + " pts" + " | " + this.recentSellTotal + " ordres r\u00E9cents (" + recentNetSell + " VENTE net)" + " | " + (sellForceOk ? sellNetDiff + " ordres VENTE net total" : sellNetDiff + " ordres ACHAT contraires en exc\u00E8s"); items.push({ tag: "Text", key: "spt_" + index, point: { x: du(index), y: du(sellPotDisplay) }, text: sellPotText, style: { fontSize: fontSize - 1, fontWeight: "bold", fill: "#FFFFFF" }, textAlignment: "centerMiddle" }); } } // ===== SIGNAL DANGER VENDEUR ===== if (this.sellBlocked && this.sellDangerPrice > 0 && this.sellDangerOrders > 0) { items.push({ tag: "Text", key: "sdw_" + index, point: { x: du(index), y: du(this.sellDangerPrice + 2 * tickSize) }, text: "\u26A0\uFE0F " + this.sellDangerPrice.toFixed(precision) + " | " + this.sellDangerOrders + " ordres ACHAT contraires" + " | retrace " + this.sellDangerPts + "/" + retraceTargetPts + " pts", style: { fontSize: fontSize, fontWeight: "bold", fill: "#FFD700" }, textAlignment: "centerMiddle" }); } // ===== RECTANGLE CONTRE VOUS VENDEUR (pts contraires > pts retrace) ===== if (showSellZone && buyRetraceLive > sellRetraceLive && sellRetraceLive < retraceTargetPts) { var sellContrePts = buyRetraceLive - sellRetraceLive; var sellContreY = close; var scH = 2 * tickSize; items.push({ tag: "Shapes", key: "scr_" + index, primitives: [{ tag: "Rectangle", position: { x: du(index - 15), y: du(sellContreY + scH) }, size: { width: du(30), height: op(du(sellContreY - scH), '-', du(sellContreY + scH)) } }], fillStyle: { color: "#FF6F00", opacity: 70 }, lineStyle: { lineWidth: 2, color: "#FFD600" } }); items.push({ tag: "Text", key: "sct_" + index, point: { x: du(index), y: du(sellContreY) }, text: "\u26A0\uFE0F " + sellContrePts + " pts contre | " + buyRetraceLive + " ACHAT vs " + sellRetraceLive + " VENTE", style: { fontSize: fontSize - 1, fontWeight: "bold", fill: "#FFD700" }, textAlignment: "centerMiddle" }); } // ===== RECTANGLE CONTRE VOUS ACHETEUR (pts contraires > pts retrace) ===== if (showBuyZone && sellRetraceLive > buyRetraceLive && buyRetraceLive < retraceTargetPts) { var buyContrePts = sellRetraceLive - buyRetraceLive; var buyContreY = close; var bcH = 2 * tickSize; items.push({ tag: "Shapes", key: "bcr_" + index, primitives: [{ tag: "Rectangle", position: { x: du(index - 15), y: du(buyContreY + bcH) }, size: { width: du(30), height: op(du(buyContreY - bcH), '-', du(buyContreY + bcH)) } }], fillStyle: { color: "#FF6F00", opacity: 70 }, lineStyle: { lineWidth: 2, color: "#FFD600" } }); items.push({ tag: "Text", key: "bct_" + index, point: { x: du(index), y: du(buyContreY) }, text: "\u26A0\uFE0F " + buyContrePts + " pts contre | " + sellRetraceLive + " VENTE vs " + buyRetraceLive + " ACHAT", style: { fontSize: fontSize - 1, fontWeight: "bold", fill: "#FFD700" }, textAlignment: "centerMiddle" }); } // ===== SIGNAL DANGER ACHETEUR ===== if (this.buyBlocked && this.buyDangerPrice > 0 && this.buyDangerOrders > 0) { items.push({ tag: "Text", key: "bdw_" + index, point: { x: du(index), y: du(this.buyDangerPrice - 2 * tickSize) }, text: "\u26A0\uFE0F " + this.buyDangerPrice.toFixed(precision) + " | " + this.buyDangerOrders + " ordres VENTE contraires" + " | retrace " + this.buyDangerPts + "/" + retraceTargetPts + " pts", style: { fontSize: fontSize, fontWeight: "bold", fill: "#FFD700" }, textAlignment: "centerMiddle" }); } // ===== POTENTIEL ACHAT (TOUJOURS VISIBLE) ===== if (showBuyZone) { var buyPotDisplay = this.buyBlocked && this.buyPotPrice > 0 ? this.buyPotPrice : this._roundPrice(swingLow + retraceTargetPts, tickSize); if (buyPotDisplay > 0) { items.push({ tag: "Shapes", key: "bpz_" + index, primitives: [{ tag: "Rectangle", position: { x: du(xStart), y: du(buyPotDisplay + potHalfH) }, size: { width: du(xEnd - xStart), height: op(du(buyPotDisplay - potHalfH), '-', du(buyPotDisplay + potHalfH)) } }], fillStyle: { color: buyColor, opacity: this.buyBlocked ? zoneOpacity : zoneOpacity - 10 }, lineStyle: { lineWidth: 1, color: buyColor } }); var buyPotText = buyPotDisplay.toFixed(precision) + " | retracement +" + retraceTargetPts + " pts" + " | " + this.recentBuyTotal + " ordres r\u00E9cents (" + recentNetBuy + " ACHAT net)" + " | " + (buyForceOk ? buyNetDiff + " ordres ACHAT net total" : buyNetDiff + " ordres VENTE contraires en exc\u00E8s"); items.push({ tag: "Text", key: "bpt_" + index, point: { x: du(index), y: du(buyPotDisplay) }, text: buyPotText, style: { fontSize: fontSize - 1, fontWeight: "bold", fill: "#FFFFFF" }, textAlignment: "centerMiddle" }); } } // ===== ZONE ACHETEUR (BAS) ===== if (showBuyZone) { items.push({ tag: "Shapes", key: "bz_" + index, primitives: [{ tag: "Rectangle", position: { x: du(xStart), y: du(buyDisplay + halfH) }, size: { width: du(xEnd - xStart), height: op(du(buyDisplay - halfH), '-', du(buyDisplay + halfH)) } }], fillStyle: { color: buyColor, opacity: this.buyBlocked ? zoneOpacity + 20 : zoneOpacity }, lineStyle: { lineWidth: this.buyBlocked ? 3 : 2, color: buyColor } }); var buyText = "ACHETEUR " + buyDisplay.toFixed(precision) + " | " + this.recentBuyTotal + " r\u00E9cents (" + retracePts + " pts)"; if (this.buyBlocked) { buyText += " \u25B2 ACHAT DOMINE"; if (this.buyRetraceDone) buyText += " RETRACE OK"; } items.push({ tag: "Text", key: "bt_" + index, point: { x: du(index), y: du(buyDisplay) }, text: buyText, style: { fontSize: fontSize, fontWeight: "bold", fill: "#FFFFFF" }, textAlignment: "centerMiddle" }); } } var hasItems = items.length > 0; return { graphics: hasItems && { items: items }, buySignal: 0, sellSignal: 0 }; } catch (e) { return this._safeReturn(); } } _safeReturn() { return { buySignal: 0, sellSignal: 0 }; } _roundPrice(price, tickSize) { if (!tickSize || tickSize <= 0 || isNaN(price)) return price || 0; return Math.round(price / tickSize) * tickSize; } _getPrecision(tickSize) { if (!tickSize) return 2; var str = tickSize.toString(); var dot = str.indexOf("."); return dot === -1 ? 0 : str.length - dot - 1; } _hexToRgb(hex, opacity) { var r = parseInt(hex.slice(1, 3), 16) / 255; var g = parseInt(hex.slice(3, 5), 16) / 255; var b = parseInt(hex.slice(5, 7), 16) / 255; var a = (opacity !== undefined ? opacity : 100) / 100; return { r: r * a, g: g * a, b: b * a }; } } module.exports = { name: "retracementZones", description: "Zones Liquidite Vendeur / Acheteur", calculator: RetracementZones, inputType: meta.InputType.BARS, overlay: true, tags: [predef.tags.Channels], params: { lookbackBars: predef.paramSpecs.number(50, 5, 10), retracePct: predef.paramSpecs.number(20, 1, 5), minTrendPoints: predef.paramSpecs.number(50, 5, 10), zoneHeight: predef.paramSpecs.number(10, 1, 3), retraceTargetPts: predef.paramSpecs.number(50, 1, 5), blockSensitivity: predef.paramSpecs.number(5, 1, 1), potZoneHeight: predef.paramSpecs.number(4, 1, 1), minVolume: predef.paramSpecs.number(0, 1, 100000), buyColor: predef.paramSpecs.color("#26A69A"), sellColor: predef.paramSpecs.color("#EF5350"), zoneOpacity: predef.paramSpecs.number(30, 5, 10), fontSize: predef.paramSpecs.number(11, 1, 8), microRetracePts: predef.paramSpecs.number(10, 1, 1) }, plots: { buySignal: { title: "Signal Achat" }, sellSignal: { title: "Signal Vente" } }, schemeStyles: { dark: { buySignal: { color: "#26A69A" }, sellSignal: { color: "#EF5350" } } } };