Last active
September 8, 2026 06:45
-
-
Save waiyanwh/e38705c4709d4b7cb9701c39688b163d to your computer and use it in GitHub Desktop.
YanMoneyOrderflow.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const predef = require("./tools/predef"); | |
| const { px, du, op, min, max } = require('./tools/graphics'); | |
| function labelParam(spec, label) { | |
| return Object.assign({}, spec, { label }); | |
| } | |
| function profileLabelThreshold(props) { | |
| if (props.SessionFilter === 'new_york_rth') return 300; | |
| if (props.SessionFilter === 'new_york_overnight') return 150; | |
| return props.TextLabelThreshold; | |
| } | |
| function selectProfileLabelRows(candidates, threshold) { | |
| const selected = []; | |
| candidates | |
| .filter(({ delta }) => Math.abs(delta) >= threshold) | |
| .sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta)) | |
| .forEach((candidate) => { | |
| const overlaps = selected.some((existing) => { | |
| const minSpacing = ((candidate.rowHeightPrice || 0) + (existing.rowHeightPrice || 0)) * 0.55; | |
| return Math.abs(candidate.yPrice - existing.yPrice) < minSpacing; | |
| }); | |
| if (!overlaps) { | |
| selected.push(Object.assign({}, candidate, { | |
| strong: selected.length < 3 | |
| })); | |
| } | |
| }); | |
| return selected.sort((a, b) => a.yPrice - b.yPrice); | |
| } | |
| function addProfileLabelGutter(container, candidates, props, keyPrefix) { | |
| if (!props.EnableDeltaLabels) return; | |
| const visibleLabels = selectProfileLabelRows(candidates, profileLabelThreshold(props)); | |
| if (visibleLabels.length === 0) return; | |
| const fontSize = props.ProfileLabelFontSize; | |
| const gutterWidth = Math.max(48, Math.min(55, props.ProfileLabelGutterWidth || 52)); | |
| const gutterGap = 4; | |
| const gutterRightX = px(-(props.barOffsetPx + props.scaleFactorPx + gutterGap)); | |
| const gutterLeftX = px(-(props.barOffsetPx + props.scaleFactorPx + gutterGap + gutterWidth)); | |
| const background = { | |
| tag: 'Shapes', | |
| key: `${keyPrefix}_gutter_bg`, | |
| primitives: [], | |
| fillStyle: { | |
| color: props.ProfileLabelGutterColor, | |
| opacity: Math.max(0, Math.min(100, props.ProfileLabelGutterOpacity || 55)) | |
| } | |
| }; | |
| visibleLabels.forEach((label) => { | |
| const rowHeight = label.rowHeightPrice || 0.01; | |
| const labelFontSize = label.strong ? fontSize + 1 : fontSize; | |
| background.primitives.push({ | |
| tag: 'Rectangle', | |
| position: { | |
| x: gutterLeftX, | |
| y: op(du(label.yPrice), '-', du(rowHeight / 2)) | |
| }, | |
| size: { | |
| width: px(gutterWidth), | |
| height: du(rowHeight) | |
| } | |
| }); | |
| container.children.push({ | |
| tag: 'Text', | |
| key: `${keyPrefix}_${label.key}`, | |
| text: `${label.delta}`, | |
| point: { | |
| x: gutterRightX, | |
| y: du(label.yPrice) | |
| }, | |
| style: { | |
| fontSize: labelFontSize, | |
| fill: label.strong ? props.StrongProfileLabelColor : props.FontColor, | |
| fontWeight: label.strong ? '600' : '500' | |
| }, | |
| textAlignment: 'rightMiddle' | |
| }); | |
| }); | |
| container.children.unshift(background); | |
| } | |
| // ── SESSION DEFINITIONS ────────────────────────────────────────────────────── | |
| // Hardcoded session windows, defined in each market's *native local time*. | |
| // These are resolved to actual UTC windows per-bar (see resolveSessionUTC), | |
| // so they stay correct regardless of: | |
| // - the chart's display timezone (e.g. Dubai time) | |
| // - daylight saving transitions in the session's home timezone | |
| // | |
| // utcOffsetStd / utcOffsetDst: hours to ADD to local time to get UTC during | |
| // standard time / daylight time respectively (e.g. New York is UTC-5 standard, | |
| // UTC-4 daylight, so offsets here are -5 and -4 — added to local to reach UTC | |
| // means: UTC = local - offset, so we store offset as hours *behind* UTC as a | |
| // positive "hoursBehindUTC" value to keep the math unambiguous below). | |
| const SESSIONS = { | |
| none: null, | |
| // New York / US equity & futures RTH: 9:30 AM - 4:00 PM ET | |
| new_york_rth: { | |
| label: 'New York RTH (9:30-16:00 ET)', | |
| openHour: 9, openMinute: 30, | |
| closeHour: 16, closeMinute: 0, | |
| dst: 'us', | |
| }, | |
| // CME futures overnight / Globex session, ET-based: 6:00 PM - 9:30 AM ET | |
| new_york_overnight: { | |
| label: 'New York Overnight (18:00-09:30 ET)', | |
| openHour: 18, openMinute: 0, | |
| closeHour: 9, closeMinute: 30, | |
| dst: 'us', | |
| overnight: true, // closes "the next day" relative to open | |
| }, | |
| // Full Globex session: 6:00 PM - 5:00 PM ET (next day), i.e. nearly 23h | |
| new_york_globex: { | |
| label: 'New York Full Session (18:00-17:00 ET)', | |
| openHour: 18, openMinute: 0, | |
| closeHour: 17, closeMinute: 0, | |
| dst: 'us', | |
| overnight: true, | |
| }, | |
| // London session: 8:00 AM - 4:30 PM London time (LSE cash hours) | |
| london: { | |
| label: 'London (08:00-16:30 UK)', | |
| openHour: 8, openMinute: 0, | |
| closeHour: 16, closeMinute: 30, | |
| dst: 'uk', | |
| }, | |
| // Tokyo session: 9:00 AM - 3:00 PM JST (no DST in Japan) | |
| tokyo: { | |
| label: 'Tokyo (09:00-15:00 JST)', | |
| openHour: 9, openMinute: 0, | |
| closeHour: 15, closeMinute: 0, | |
| dst: 'none', | |
| fixedOffsetHours: 9, // JST = UTC+9 year-round | |
| }, | |
| // Sydney session: 10:00 AM - 4:00 PM AEST/AEDT | |
| sydney: { | |
| label: 'Sydney (10:00-16:00 AET)', | |
| openHour: 10, openMinute: 0, | |
| closeHour: 16, closeMinute: 0, | |
| dst: 'au', | |
| }, | |
| // Frankfurt/Xetra: 9:00 AM - 5:30 PM CET/CEST | |
| frankfurt: { | |
| label: 'Frankfurt (09:00-17:30 CET)', | |
| openHour: 9, openMinute: 0, | |
| closeHour: 17, closeMinute: 30, | |
| dst: 'eu', | |
| }, | |
| }; | |
| // Returns hours-behind-UTC for a given DST rule and a UTC instant (used only | |
| // to figure out which side of a DST transition we're on, via UTC month — this | |
| // is an approximation that ignores the exact transition day/hour, see note | |
| // below). | |
| function hoursBehindUTC(dstRule, fixedOffsetHours, utcDate) { | |
| if (dstRule === 'none') return -fixedOffsetHours; // negative => ahead of UTC | |
| const month = utcDate.getUTCMonth(); // 0=Jan..11=Dec | |
| const date = utcDate.getUTCDate(); | |
| // Rough DST windows (good enough for trading-session bucketing; exact | |
| // transition hour is not critical since sessions don't open/close at | |
| // exactly 2am on transition weekends in practice). | |
| if (dstRule === 'us') { | |
| // DST (EDT, UTC-4) roughly mid-March through early November. | |
| const isDst = (month > 2 && month < 10) || | |
| (month === 2 && date >= 8) || // ~2nd Sun March | |
| (month === 10 && date < 1); // never true, kept for clarity | |
| return isDst ? 4 : 5; // hours behind UTC | |
| } | |
| if (dstRule === 'uk' || dstRule === 'eu') { | |
| // BST/CEST roughly late-March through late-October. | |
| const isDst = (month > 2 && month < 9) || | |
| (month === 2 && date >= 25) || // ~last Sun March | |
| (month === 9 && date < 26); // ~last Sun Oct | |
| // UK is UTC+0 std / UTC+1 dst -> "behind UTC" is negative (ahead) | |
| // EU(Frankfurt) is UTC+1 std / UTC+2 dst. | |
| if (dstRule === 'uk') return isDst ? -1 : 0; | |
| return isDst ? -2 : -1; | |
| } | |
| if (dstRule === 'au') { | |
| // Sydney DST is opposite hemisphere: roughly Oct through early April. | |
| const isDst = (month > 9) || (month < 2) || (month === 2 && date < 1) || | |
| (month === 9 && date >= 1); | |
| return isDst ? -11 : -10; | |
| } | |
| return -fixedOffsetHours; | |
| } | |
| // Given a session config and the bar's UTC date, returns { startMs, endMs } | |
| // for the session window covering (or most recently preceding) that bar. | |
| function resolveSessionUTC(session, utcDate) { | |
| const behind = session.dst === 'none' | |
| ? -session.fixedOffsetHours | |
| : hoursBehindUTC(session.dst, session.fixedOffsetHours || 0, utcDate); | |
| // Local wall-clock date matching this UTC instant (for picking which | |
| // calendar day's open/close to anchor to). | |
| const localMs = utcDate.getTime() - behind * 3600000; | |
| const local = new Date(localMs); | |
| const sessionOpenLocal = new Date(Date.UTC( | |
| local.getUTCFullYear(), local.getUTCMonth(), local.getUTCDate(), | |
| session.openHour, session.openMinute | |
| )); | |
| let sessionCloseLocal = new Date(Date.UTC( | |
| local.getUTCFullYear(), local.getUTCMonth(), local.getUTCDate(), | |
| session.closeHour, session.closeMinute | |
| )); | |
| if (session.overnight || sessionCloseLocal.getTime() <= sessionOpenLocal.getTime()) { | |
| sessionCloseLocal = new Date(sessionCloseLocal.getTime() + 86400000); | |
| } | |
| // Convert those local wall-clock instants to true UTC ms. | |
| const openUTC = sessionOpenLocal.getTime() + behind * 3600000; | |
| const closeUTC = sessionCloseLocal.getTime() + behind * 3600000; | |
| return { startMs: openUTC, endMs: closeUTC }; | |
| } | |
| // Returns true if the bar's actual UTC timestamp falls within the configured | |
| // session window. If the bar is before today's session open, checks against | |
| // yesterday's window too (handles bars just after midnight UTC for sessions | |
| // that span a UTC day boundary). | |
| function isInSession(sessionKey, barUtcMs) { | |
| const session = SESSIONS[sessionKey]; | |
| if (!session) return true; // 'none' => no filtering | |
| const utcDate = new Date(barUtcMs); | |
| const todayWindow = resolveSessionUTC(session, utcDate); | |
| if (barUtcMs >= todayWindow.startMs && barUtcMs < todayWindow.endMs) return true; | |
| // Check the window anchored a day earlier too, in case barUtcMs sits in | |
| // the early-UTC tail of a session that opened "yesterday" local-date-wise. | |
| const yesterdayUtcDate = new Date(barUtcMs - 86400000); | |
| const yesterdayWindow = resolveSessionUTC(session, yesterdayUtcDate); | |
| if (barUtcMs >= yesterdayWindow.startMs && barUtcMs < yesterdayWindow.endMs) return true; | |
| return false; | |
| } | |
| // Returns a string key identifying which "trading day" a bar belongs to, for | |
| // the purpose of resetting the delta/volume accumulator daily. When a session | |
| // filter is active, the key is anchored to that session's own window (so the | |
| // accumulator resets exactly when a new session window begins — e.g. a new | |
| // "New York RTH" day, not a UTC calendar day, which would split/rejoin the | |
| // session incorrectly). When no session filter is set, falls back to a plain | |
| // UTC calendar-day key. | |
| function dayKeyFor(sessionKey, barUtcMs) { | |
| const session = SESSIONS[sessionKey]; | |
| if (!session) { | |
| const d = new Date(barUtcMs); | |
| return `${d.getUTCFullYear()}-${d.getUTCMonth()}-${d.getUTCDate()}`; | |
| } | |
| const window = resolveSessionUTC(session, new Date(barUtcMs)); | |
| // Key off the session's own open instant — every bar inside the same | |
| // session window resolves to the same openUTC ms, so this is stable | |
| // for the whole window and changes exactly when a new window starts. | |
| return `${sessionKey}_${window.startMs}`; | |
| } | |
| class BigTradeDeltaAbsorption { | |
| init() { | |
| // --- Big Trade Bubbles / Price Axis Delta state --- | |
| this.mDelta = 0; | |
| this.maxDelta = 0; | |
| this.byPrice = {}; | |
| this.currentDayKey = null; | |
| // --- Absorption / Divergence state --- | |
| this.defaultTickSize = 0.01; // Fallback tick size | |
| this.validationState = { | |
| consecutiveErrors: 0, | |
| maxConsecutiveErrors: 5, | |
| }; | |
| console.log('BigTradeDeltaAbsorption (RewiredSanjay): Initialization started'); | |
| } | |
| map(d, i, history) { | |
| // ── DEFENSIVE VALIDATION (shared by both features) ────────────────── | |
| // Ported from the absorption indicator. If contractInfo/OHLC/volume | |
| // data is missing or malformed for this bar, skip the whole bar | |
| // rather than letting either feature draw on top of bad data. | |
| if (!this.contractInfo || typeof this.contractInfo.tickSize !== 'number' || this.contractInfo.tickSize <= 0) { | |
| console.warn('BigTradeDeltaAbsorption: Missing or invalid contractInfo.tickSize for bar ' + d.index() + ', using fallback'); | |
| this.contractInfo = this.contractInfo || {}; | |
| this.contractInfo.tickSize = this.contractInfo.tickSize || this.defaultTickSize; | |
| } | |
| const hasValidOHLC = ( | |
| typeof d.open() === 'number' && !isNaN(d.open()) && | |
| typeof d.close() === 'number' && !isNaN(d.close()) && | |
| typeof d.high() === 'number' && !isNaN(d.high()) && | |
| typeof d.low() === 'number' && !isNaN(d.low()) | |
| ); | |
| if (!hasValidOHLC) { | |
| console.warn('BigTradeDeltaAbsorption: Invalid OHLC data for bar ' + d.index() + ' - skipping'); | |
| this.validationState.consecutiveErrors++; | |
| return {}; | |
| } | |
| const hasValidVolumeData = ( | |
| typeof d.offerVolume() === 'number' && !isNaN(d.offerVolume()) && | |
| typeof d.bidVolume() === 'number' && !isNaN(d.bidVolume()) | |
| ); | |
| if (!hasValidVolumeData) { | |
| console.warn('BigTradeDeltaAbsorption: Invalid volume data for bar ' + d.index() + ' - skipping'); | |
| this.validationState.consecutiveErrors++; | |
| return {}; | |
| } | |
| this.validationState.consecutiveErrors = 0; | |
| // ── SESSION FILTER (delta/volume section only) ────────────────────── | |
| // Uses the bar's true UTC timestamp, independent of the chart's | |
| // display timezone. Bubbles are intentionally NOT gated by this — | |
| // they run on every bar all day. Only the price-axis delta/volume | |
| // accumulation below is restricted to the selected session window. | |
| const barUtcMs = d.date ? d.date.getTime() : (typeof d.timestamp === 'function' ? d.timestamp() : null); | |
| const inSession = this.props.SessionFilter === 'none' | |
| || barUtcMs == null | |
| || isInSession(this.props.SessionFilter, barUtcMs); | |
| const volCircles = { | |
| tag: "Container", key: 'circs', global: false, children: [], | |
| }; | |
| const bubbleTextContainer = { | |
| tag: 'Container', key: 'bubbleText', children: [], | |
| }; | |
| const combinedBars = { | |
| tag: 'Container', key: 'combinedBars', | |
| origin: { cs: 'grid', h: 'right', v: 'top' }, | |
| global: true, children: [], | |
| conditions: { scaleRangeY: { max: 3 } } | |
| }; | |
| const combinedText = { | |
| tag: 'Container', key: 'combinedText', | |
| origin: { cs: 'grid', h: 'right', v: 'top' }, | |
| global: true, children: [], | |
| conditions: { scaleRangeY: { max: 3, min: 2 / this.props.priceLevelsPerBar } } | |
| }; | |
| const textContainer = { | |
| tag: 'Container', key: 'PADV_textcontainer', | |
| origin: { cs: 'grid', h: 'right', v: 'top' }, | |
| global: true, children: [], | |
| conditions: { scaleRangeY: { min: 3 } } | |
| }; | |
| const barsContainer = { | |
| tag: 'Container', key: 'PADV_barscontainer', | |
| global: true, origin: { cs: 'grid', h: 'right', v: 'top' }, | |
| children: [], conditions: { scaleRangeY: { min: 3 } } | |
| }; | |
| // ── BIG TRADE BUBBLES ───────────────────────────────────────────────── | |
| if (this.props.EnableBubbles) { | |
| const profiles = d.profile(); | |
| const combinedProfs = {}; | |
| const modifiedSize = this.contractInfo.tickSize * this.props.priceLevelsPerBar; | |
| let last, barInit; | |
| profiles | |
| .sort((a, b) => a.price - b.price) | |
| .forEach(({ price, vol, bidVol, askVol }) => { | |
| if (!barInit) barInit = price; | |
| if (last && last >= barInit + modifiedSize) barInit = price; | |
| if (last && last <= barInit + modifiedSize) { | |
| let item = combinedProfs[barInit.toFixed(7).toString()]; | |
| if (!item) { | |
| item = { vol: 0, askVol: 0, bidVol: 0, price: barInit }; | |
| combinedProfs[barInit.toFixed(7).toString()] = item; | |
| } | |
| item.askVol += askVol; | |
| item.bidVol += bidVol; | |
| item.vol += vol; | |
| } | |
| last = price; | |
| }); | |
| Object.values(combinedProfs).forEach(({ bidVol, askVol, price }) => { | |
| const delta = -(bidVol - askVol); | |
| const xOffset = 0.02; | |
| const fontSize = this.props.BubbleLabelFontSize; | |
| this.mdelta = Math.abs(delta); | |
| this.maxDelta = Math.max(this.mdelta, this.maxDelta); | |
| const volThreshold = this.props.RthVolThreshold; | |
| const ctrPt = { | |
| x: op(du(d.index()), '-', du(xOffset)), | |
| y: du(price + modifiedSize / 2) | |
| }; | |
| const minR = 8; | |
| const maxR = this.props.MaxBubbleSize; | |
| const maxI = this.maxDelta + (this.maxDelta / 2.15); | |
| const cValue = delta; | |
| const radiusScale = Math.min( | |
| ((Math.abs(cValue) - volThreshold) / (maxI - volThreshold)) * (maxR - minR) + minR, | |
| maxR | |
| ); | |
| if (Math.abs(cValue) > volThreshold) { | |
| volCircles.children.push({ | |
| tag: "Shapes", | |
| key: `bar${d.index()}${price}`, | |
| primitives: [{ tag: 'Circle', radius: radiusScale, center: ctrPt }], | |
| fillStyle: { | |
| color: cValue > 0 ? this.props.positiveDeltaColor : this.props.negativeDeltaColor, | |
| opacity: this.props.BubbleOpacity | |
| } | |
| }); | |
| bubbleTextContainer.children.push({ | |
| tag: 'Text', | |
| key: `${d.index()}${price}`, | |
| text: this.props.EnableBubbleLabels ? `${cValue}` : undefined, | |
| point: ctrPt, | |
| style: { | |
| fontSize, | |
| fill: this.props.BubbleLabelColor | |
| }, | |
| textAlignment: 'centerMiddle' | |
| }); | |
| } | |
| }); | |
| } | |
| // ── PRICE AXIS DELTA & VOLUME ───────────────────────────────────────── | |
| // Only accumulates/draws while the bar is inside the selected session | |
| // window. Bars outside the window are skipped here entirely (no effect | |
| // on bubbles above, which always run). | |
| // | |
| // RestrictToCurrentDay resets the accumulator whenever the bar's day | |
| // (or session window, if a SessionFilter is active) changes, so the | |
| // profile only reflects the current/most-recent day rather than every | |
| // day ever loaded on the chart. | |
| if (this.props.EnableDeltaVolume && inSession) { | |
| if (this.props.RestrictToCurrentDay && barUtcMs != null) { | |
| const dayKey = dayKeyFor(this.props.SessionFilter, barUtcMs); | |
| if (dayKey !== this.currentDayKey) { | |
| this.byPrice = {}; | |
| this.currentDayKey = dayKey; | |
| } | |
| } | |
| d.profile().forEach((profile) => { | |
| const { price, bidVol, askVol, vol } = profile; | |
| const priceKey = price.toFixed(4).toString(); | |
| if (this.byPrice[priceKey]) { | |
| this.byPrice[priceKey].askVol += askVol; | |
| this.byPrice[priceKey].bidVol += bidVol; | |
| this.byPrice[priceKey].vol += vol; | |
| } else { | |
| this.byPrice[priceKey] = { price, askVol, bidVol, vol }; | |
| } | |
| }); | |
| if (d.isLast()) { | |
| const keys = Object.keys(this.byPrice); | |
| if (keys.length > 0) { | |
| const highPrice = keys.map(k => parseFloat(k)).reduce((a, b) => Math.max(a, b), 0); | |
| const lowPrice = keys.map(k => parseFloat(k)).reduce((a, b) => Math.min(a, b), Infinity); | |
| const range = highPrice - lowPrice; | |
| const greatestDelta = keys | |
| .map(k => this.byPrice[k]) | |
| .map(({ bidVol, askVol }) => Math.abs(bidVol - askVol)) | |
| .reduce((a, b) => Math.max(a, b), 0); | |
| const greatestVol = keys | |
| .map(k => this.byPrice[k]) | |
| .map(({ vol }) => vol) | |
| .reduce((a, b) => Math.max(a, b), 0); | |
| const numBars = range / this.contractInfo.tickSize; | |
| if (this.props.priceLevelsPerBar > 0) { | |
| const combinedByPrice = {}; | |
| const modSize = this.contractInfo.tickSize * this.props.priceLevelsPerBar; | |
| let lastK, barInitK; | |
| keys | |
| .sort((a, b) => parseFloat(a) - parseFloat(b)) | |
| .forEach(k => { | |
| let float = parseFloat(k); | |
| if (!barInitK) barInitK = float; | |
| if (lastK && lastK >= barInitK + modSize) barInitK = float; | |
| if (lastK && lastK <= barInitK + modSize) { | |
| let item = combinedByPrice[barInitK.toFixed(4).toString()]; | |
| if (!item) { | |
| item = { vol: 0, askVol: 0, bidVol: 0, price: barInitK }; | |
| combinedByPrice[barInitK.toFixed(4).toString()] = item; | |
| } | |
| const src = this.byPrice[k]; | |
| item.askVol += src.askVol; | |
| item.bidVol += src.bidVol; | |
| item.vol += src.vol; | |
| } | |
| lastK = float; | |
| }); | |
| const greatestCombinedDelta = Object.values(combinedByPrice) | |
| .map(({ bidVol, askVol }) => Math.abs(bidVol - askVol)) | |
| .reduce((a, b) => Math.max(a, b), 0); | |
| const greatestCombinedVol = Object.values(combinedByPrice) | |
| .map(({ vol }) => vol) | |
| .reduce((a, b) => Math.max(a, b), 0); | |
| const combinedNumBars = range / modSize; | |
| let lastPush, initPush; | |
| const combinedLabelCandidates = []; | |
| Object.values(combinedByPrice).forEach(({ price, askVol, bidVol, vol }) => { | |
| if (!initPush) initPush = price; | |
| if (lastPush && lastPush > initPush + modSize) initPush = price; | |
| if (lastPush && lastPush <= initPush + modSize) { | |
| const delta = -(bidVol - askVol); | |
| const barHeight = du(range / combinedNumBars); | |
| const deltaBarWidth = min(px(-2), px(-this.props.scaleFactorPx * Math.abs(delta / greatestCombinedDelta))); | |
| const volBarWidth = max(px(2), px(this.props.volScaleFactorPx * (vol / greatestCombinedVol))); | |
| const labelY = price - modSize / 2; | |
| combinedBars.children.push({ | |
| tag: 'Shapes', key: 'cbar_' + price, | |
| primitives: [{ | |
| tag: 'Rectangle', | |
| position: { x: px(-(this.props.barOffsetPx)), y: op(du(price), '-', du(modSize)) }, | |
| size: { height: barHeight, width: deltaBarWidth } | |
| }], | |
| fillStyle: { color: delta > 0 ? this.props.positiveColor : delta < 0 ? this.props.negativeColor : '#999' } | |
| }); | |
| combinedLabelCandidates.push({ | |
| key: `ctxt_${price}`, | |
| delta, | |
| yPrice: labelY, | |
| rowHeightPrice: modSize | |
| }); | |
| if (this.props.EnableVolumeBars) { | |
| combinedBars.children.push({ | |
| tag: 'Shapes', key: 'cvol_' + price, | |
| primitives: [{ | |
| tag: 'Rectangle', | |
| position: { x: px(-(this.props.barOffsetPx)), y: op(du(price), '-', du(modSize)) }, | |
| size: { height: barHeight, width: volBarWidth } | |
| }], | |
| fillStyle: { color: this.props.VolumeBarColor, opacity: this.props.VolumeBarOpacity } | |
| }); | |
| } | |
| } | |
| lastPush = price; | |
| }); | |
| addProfileLabelGutter(combinedText, combinedLabelCandidates, this.props, 'combined_profile_label'); | |
| } | |
| const labelCandidates = []; | |
| keys.map(k => this.byPrice[k]).forEach(({ price, askVol, bidVol, vol }) => { | |
| const delta = -(bidVol - askVol); | |
| const barHeight = op(du(range / numBars), '-', px(2)); | |
| const deltaBarWidth = min(px(-2), px(-this.props.scaleFactorPx * Math.abs(delta / greatestDelta))); | |
| const volBarWidth = max(px(2), px(this.props.volScaleFactorPx * (vol / greatestVol))); | |
| barsContainer.children.push({ | |
| tag: 'Shapes', key: 'bar_' + price, | |
| primitives: [{ | |
| tag: 'Rectangle', | |
| position: { | |
| x: px(-(this.props.barOffsetPx)), | |
| y: op(du(price), '-', du(this.contractInfo.tickSize / 2)) | |
| }, | |
| size: { height: barHeight, width: deltaBarWidth } | |
| }], | |
| fillStyle: { color: delta > 0 ? this.props.positiveColor : delta < 0 ? this.props.negativeColor : '#999' } | |
| }); | |
| labelCandidates.push({ | |
| key: `txt_${price}`, | |
| delta, | |
| yPrice: price, | |
| rowHeightPrice: this.contractInfo.tickSize | |
| }); | |
| if (this.props.EnableVolumeBars) { | |
| barsContainer.children.push({ | |
| tag: 'Shapes', key: 'vol_' + price, | |
| primitives: [{ | |
| tag: 'Rectangle', | |
| position: { | |
| x: px(-(this.props.barOffsetPx)), | |
| y: op(du(price), '-', du(this.contractInfo.tickSize / 2)) | |
| }, | |
| size: { height: barHeight, width: volBarWidth } | |
| }], | |
| fillStyle: { color: this.props.VolumeBarColor, opacity: this.props.VolumeBarOpacity } | |
| }); | |
| } | |
| }); | |
| addProfileLabelGutter(textContainer, labelCandidates, this.props, 'profile_label'); | |
| } | |
| } | |
| } | |
| // ── ABSORPTION / DIVERGENCE ────────────────────────────────────────── | |
| // Flags bars where price closes in one direction while order-flow | |
| // delta (ask volume - bid volume) moves the opposite way. | |
| const offerVol = d.offerVolume(); | |
| const bidVol = d.bidVolume(); | |
| const deltaValue = offerVol - bidVol; | |
| const deltaText = deltaValue.toFixed(0); | |
| const upCandleCheck = d.close() > d.open(); | |
| const downCandleCheck = d.close() < d.open(); | |
| // Bullish absorption: price closed up, but delta was negative (selling absorbed). | |
| // Bearish absorption: price closed down, but delta was positive (buying absorbed). | |
| const bullDiv = deltaValue < 0 && upCandleCheck; | |
| const bearDiv = deltaValue > 0 && downCandleCheck; | |
| const deltaDisplayMode = this.props.displayDelta; | |
| const isShowAll = deltaDisplayMode === 'showAll'; | |
| const isShowOnlyDivergence = deltaDisplayMode === 'showOnlyDivergence'; | |
| const isNone = deltaDisplayMode === 'none'; | |
| let bullishDivergence = null; | |
| let bearishDivergence = null; | |
| if (!isNone) { | |
| if (bullDiv) bullishDivergence = true; | |
| if (bearDiv) bearishDivergence = true; | |
| } | |
| // Divergence icon dot positions | |
| let bullishDivergencePosition = null; | |
| let bearishDivergencePosition = null; | |
| if (this.props.showDivergenceIcon) { | |
| if (bullDiv) { | |
| bullishDivergencePosition = d.low() - (this.contractInfo.tickSize * this.props.divergenceIconSpacing); | |
| } | |
| if (bearDiv) { | |
| bearishDivergencePosition = d.high() + (this.contractInfo.tickSize * this.props.divergenceIconSpacing); | |
| } | |
| } | |
| const absXOffset = 0.02; | |
| const absFontSize = this.props.deltaFontSize; | |
| const modifiedSizeAbove = this.contractInfo.tickSize * this.props.deltaTextSpacing; | |
| const modifiedSizeBelow = this.contractInfo.tickSize * (this.props.deltaTextSpacing + 1); | |
| const hasDivergence = bullishDivergence !== null || bearishDivergence !== null; | |
| const filterMinDelta = this.props.filterMinDelta || 0; | |
| const meetsMinDeltaFilter = Math.abs(deltaValue) >= filterMinDelta; | |
| const shouldShowDeltaText = !isNone && meetsMinDeltaFilter && (isShowAll || (isShowOnlyDivergence && hasDivergence)); | |
| const absorptionTextContainer = { | |
| tag: "Container", | |
| key: "absorptionTextContainer", | |
| children: [], | |
| conditions: {}, | |
| }; | |
| if (shouldShowDeltaText) { | |
| let labelAnchor; | |
| let textColor; | |
| if (hasDivergence) { | |
| labelAnchor = bullishDivergence ? d.low() - modifiedSizeBelow : d.high() + modifiedSizeAbove; | |
| textColor = bullishDivergence ? this.props.sellingAbsorptionText : this.props.buyingAbsorptionText; | |
| } else { | |
| labelAnchor = upCandleCheck ? d.low() - modifiedSizeBelow : d.high() + modifiedSizeAbove; | |
| textColor = this.props.neutralDeltaText; | |
| } | |
| const absTextPt = { | |
| x: op(du(d.index()), '-', du(absXOffset)), | |
| y: du(labelAnchor), | |
| }; | |
| absorptionTextContainer.children.push({ | |
| tag: "Text", | |
| key: `absorptionTextKey${d.index()}`, | |
| text: deltaText, | |
| point: absTextPt, | |
| style: { fontSize: absFontSize, fill: textColor }, | |
| textAlignment: "centerMiddle", | |
| }); | |
| } | |
| const items = [ | |
| volCircles, | |
| bubbleTextContainer, | |
| barsContainer, | |
| textContainer, | |
| combinedBars, | |
| combinedText, | |
| ]; | |
| if (shouldShowDeltaText) items.push(absorptionTextContainer); | |
| return { | |
| graphics: { items }, | |
| bullishDivergence: bullishDivergencePosition, | |
| bearishDivergence: bearishDivergencePosition, | |
| }; | |
| } | |
| } | |
| // Base line style, extended below with dark-mode colors for the divergence | |
| // dots. (The original absorption indicator accidentally declared | |
| // `schemeStyles` twice in one object literal, which silently discarded the | |
| // solidLine default — this merges both correctly.) | |
| const baseScheme = predef.styles.solidLine("#000") || {}; | |
| const mergedSchemeStyles = Object.assign({}, baseScheme, { | |
| dark: Object.assign({}, baseScheme.dark, { | |
| bullishDivergence: { color: "#38BDF8" }, | |
| bearishDivergence: { color: "#F59E0B" } | |
| }), | |
| light: Object.assign({}, baseScheme.light, { | |
| bullishDivergence: { color: "#38BDF8" }, | |
| bearishDivergence: { color: "#F59E0B" } | |
| }) | |
| }); | |
| module.exports = { | |
| inputType: 'bars', | |
| name: "Yan Orderflow", | |
| description: "Big Trade Delta + Session Profile + Absorption/Divergence", | |
| calculator: BigTradeDeltaAbsorption, | |
| params: { | |
| // ── SESSION FILTER ──────────────────────────────────────────────────── | |
| divider0: predef.paramSpecs.enum({ s: '━━━━━━━━━━ SESSION FILTER ━━━━━━━━━━' }, 's'), | |
| SessionFilter: predef.paramSpecs.enum({ | |
| none: 'All sessions (no filter)', | |
| new_york_rth: 'New York RTH (9:30-16:00 ET)', | |
| new_york_overnight: 'New York Overnight (18:00-09:30 ET)', | |
| new_york_globex: 'New York Full/Globex (18:00-17:00 ET)', | |
| london: 'London (08:00-16:30 UK)', | |
| frankfurt: 'Frankfurt (09:00-17:30 CET)', | |
| tokyo: 'Tokyo (09:00-15:00 JST)', | |
| sydney: 'Sydney (10:00-16:00 AET)', | |
| }, 'none'), | |
| // ── BIG TRADE BUBBLES ───────────────────────────────────────────────── | |
| divider1: predef.paramSpecs.enum({ s: '━━━━━━━━━━ BIG TRADE BUBBLES ━━━━━━━━━━' }, 's'), | |
| EnableBubbles: labelParam(predef.paramSpecs.bool(true), 'Enable Big Trade Bubbles'), | |
| positiveDeltaColor: labelParam(predef.paramSpecs.color('#22C55E'), 'Positive Delta Color'), | |
| negativeDeltaColor: labelParam(predef.paramSpecs.color('#EF4444'), 'Negative Delta Color'), | |
| RthVolThreshold: labelParam(predef.paramSpecs.number(155, 1, 0), 'Bubble Threshold'), | |
| BubbleOpacity: labelParam(predef.paramSpecs.number(55, 1, 0), 'Bubble Opacity'), | |
| MaxBubbleSize: labelParam(predef.paramSpecs.number(42, 1, 0), 'Maximum Bubble Size'), | |
| EnableBubbleLabels: labelParam(predef.paramSpecs.bool(true), 'Show Bubble Labels'), | |
| BubbleLabelFontSize: labelParam(predef.paramSpecs.number(11, 1, 6), 'Bubble Label Font Size'), | |
| BubbleLabelColor: labelParam(predef.paramSpecs.color('#E5E7EB'), 'Bubble Label Color'), | |
| priceLevelsPerBar: labelParam(predef.paramSpecs.number(6, 1, 1), 'Price Levels Per Bubble'), | |
| // ── PRICE AXIS DELTA & VOLUME ───────────────────────────────────────── | |
| divider2: predef.paramSpecs.enum({ s: '━━━━━━━━━━ PRICE AXIS DELTA ━━━━━━━━━━' }, 's'), | |
| EnableDeltaVolume: labelParam(predef.paramSpecs.bool(true), 'Enable Price Axis Delta'), | |
| RestrictToCurrentDay: labelParam(predef.paramSpecs.bool(true), 'Restrict Profile To Current Day'), | |
| positiveColor: labelParam(predef.paramSpecs.color('#22C55E'), 'Positive Delta Color'), | |
| negativeColor: labelParam(predef.paramSpecs.color('#EF4444'), 'Negative Delta Color'), | |
| scaleFactorPx: labelParam(predef.paramSpecs.number(60, 10, 10), 'Profile Scale'), | |
| volScaleFactorPx: labelParam(predef.paramSpecs.number(60, 10, 10), 'Volume Bar Scale'), | |
| barOffsetPx: labelParam(predef.paramSpecs.number(60, 1, 0), 'Profile Offset'), | |
| EnableDeltaLabels: labelParam(predef.paramSpecs.bool(true), 'Show Delta Labels'), | |
| TextLabelThreshold: labelParam(predef.paramSpecs.number(100, 1, 0), 'Profile Label Threshold'), | |
| ProfileLabelFontSize: labelParam(predef.paramSpecs.number(10, 1, 6), 'Profile Label Font Size'), | |
| ProfileLabelGutterWidth: labelParam(predef.paramSpecs.number(52, 1, 48), 'Profile Label Gutter Width'), | |
| ProfileLabelGutterColor: labelParam(predef.paramSpecs.color('#000000'), 'Profile Label Gutter Color'), | |
| ProfileLabelGutterOpacity: labelParam(predef.paramSpecs.number(55, 1, 0), 'Profile Label Gutter Opacity'), | |
| FontColor: labelParam(predef.paramSpecs.color('#CBD5E1'), 'Profile Label Color'), | |
| StrongProfileLabelColor: labelParam(predef.paramSpecs.color('#FFFFFF'), 'Strong Profile Label Color'), | |
| // ── VOLUME BARS ─────────────────────────────────────────────────────── | |
| divider3: predef.paramSpecs.enum({ s: '━━━━━━━━━━ VOLUME BARS ━━━━━━━━━━' }, 's'), | |
| EnableVolumeBars: labelParam(predef.paramSpecs.bool(false), 'Show Volume Bars'), | |
| VolumeBarColor: labelParam(predef.paramSpecs.color('#64748B'), 'Volume Bar Color'), | |
| VolumeBarOpacity: labelParam(predef.paramSpecs.number(40, 1, 0), 'Volume Bar Opacity'), | |
| // ── ABSORPTION / DIVERGENCE ────────────────────────────────────────── | |
| divider4: predef.paramSpecs.enum({ s: '━━━━━━━━━━ ABSORPTION / DIVERGENCE ━━━━━━━━━━' }, 's'), | |
| sellingAbsorptionText: labelParam(predef.paramSpecs.color("#38BDF8"), 'Bullish Absorption Color'), | |
| buyingAbsorptionText: labelParam(predef.paramSpecs.color("#F59E0B"), 'Bearish Absorption Color'), | |
| neutralDeltaText: labelParam(predef.paramSpecs.color("#94A3B8"), 'Neutral Delta Color'), | |
| deltaTextSpacing: labelParam(predef.paramSpecs.number(30), 'Absorption Text Spacing'), | |
| displayDelta: labelParam(predef.paramSpecs.enum({ | |
| none: 'None', | |
| showOnlyDivergence: 'Show Only Delta Divergence', | |
| showAll: 'Show All Delta' | |
| }, 'showOnlyDivergence'), 'Absorption Display Mode'), | |
| filterMinDelta: labelParam(predef.paramSpecs.number(50), 'Minimum Absorption Delta'), | |
| showDivergenceIcon: labelParam(predef.paramSpecs.bool(true), 'Show Divergence Icon'), | |
| divergenceIconSpacing: labelParam(predef.paramSpecs.number(15), 'Divergence Icon Spacing'), | |
| deltaFontSize: labelParam(predef.paramSpecs.number(14), 'Absorption Font Size'), | |
| }, | |
| tags: ["YanOrderflow"], | |
| schemeStyles: mergedSchemeStyles, | |
| requirements: { volumeProfiles: true }, | |
| plotter: [ | |
| predef.plotters.dots('bearishDivergence'), | |
| predef.plotters.dots('bullishDivergence') | |
| ], | |
| plots: { | |
| bullishDivergence: { title: 'Bullish Absorption Icon', displayOnly: true }, | |
| bearishDivergence: { title: 'Bearish Absorption Icon', displayOnly: true } | |
| }, | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment