Skip to content

Instantly share code, notes, and snippets.

@waiyanwh
Last active September 8, 2026 06:45
Show Gist options
  • Select an option

  • Save waiyanwh/e38705c4709d4b7cb9701c39688b163d to your computer and use it in GitHub Desktop.

Select an option

Save waiyanwh/e38705c4709d4b7cb9701c39688b163d to your computer and use it in GitHub Desktop.
YanMoneyOrderflow.js
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