-
-
Save jtryan/1e5452d8b8fb2803eb3ae1e497517944 to your computer and use it in GitHub Desktop.
Price Axis Volume Delta indicator for the Tradovate platform.
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 { min, max, du, px, op } = require('./tools/graphics') | |
class PriceAxisDelta { | |
init() { | |
this.byPrice = {} | |
this.openDate = new Date() | |
if(this.props.startsYesterday) this.openDate.setTime(Date.now() - 1000*60*60*24) | |
this.openDate.setHours(this.props.marketOpenHours, this.props.marketOpenMinutes) | |
this.closeDate = new Date() | |
if(this.props.closesTomorrow) this.closeDate.setTime(Date.now() + 1000*60*60*24) | |
this.closeDate.setHours(this.props.marketCloseHours, this.props.marketCloseMinutes) | |
} | |
map(d) { | |
if(d.date < this.openDate) { | |
return {} | |
} | |
if(d.date > this.closeDate) { | |
return {} | |
} | |
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: 10/this.props.priceLevelsPerBar | |
} | |
} | |
} | |
const textContainer = { | |
tag: 'Container', | |
key: 'PADV_textcontainer', | |
origin: { | |
cs: 'grid', | |
h: 'right', | |
v: 'top' | |
}, | |
global: true, | |
children: [], | |
conditions: { | |
scaleRangeY: { | |
min: 12 | |
} | |
} | |
} | |
const barsContainer = { | |
tag: 'Container', | |
key: 'PADV_barscontainer', | |
global: true, | |
origin: { | |
cs: 'grid', | |
h: 'right', | |
v: 'top' | |
}, | |
children: [], | |
conditions: { | |
scaleRangeY: { | |
min: 3 | |
} | |
} | |
} | |
d.profile().forEach((profile, i, arr) => { | |
const { price, bidVol, askVol, vol } = profile | |
let priceKey = price.toFixed(4).toString() | |
const item = this.byPrice[priceKey] | |
if(item) { | |
item.askVol += askVol | |
item.bidVol += bidVol | |
item.vol += vol | |
} else { | |
this.byPrice[priceKey] = profile | |
} | |
}) | |
if(d.isLast()) { | |
const keys = Object.keys(this.byPrice) | |
const high = keys | |
.map(k => parseFloat(k, 10)) | |
.reduce((a, b) => Math.max(a, b), 0) | |
const low = keys | |
.map(k => parseFloat(k, 10)) | |
.reduce((a, b) => Math.min(a, b), Infinity) | |
const range = high - low | |
const greatestDelta = keys | |
.map(k => this.byPrice[k]) | |
.map(({bidVol, askVol}) => Math.abs(bidVol - askVol)) | |
.reduce((a, b) => Math.max(a, b), 0) | |
const numBars = range/this.contractInfo.tickSize | |
if(this.props.priceLevelsPerBar > 1) { | |
this.combinedByPrice = {} | |
const modifiedSize = this.contractInfo.tickSize * this.props.priceLevelsPerBar | |
let last, barInit | |
keys | |
.sort((a, b) => parseFloat(a, 10) - parseFloat(b, 10)) | |
.forEach(k => { | |
let float = parseFloat(k, 10) | |
if(!barInit) barInit = float | |
if(last && last >= barInit + modifiedSize) { | |
barInit = float | |
} | |
if(last && last <= barInit + modifiedSize) { | |
let item = this.combinedByPrice[barInit.toFixed(4).toString()] | |
if(!item) ( | |
item = { vol: 0, askVol: 0, bidVol: 0 }, | |
item.price = barInit, | |
this.combinedByPrice[barInit.toFixed(4).toString()] = item | |
) | |
const byPriceItem = this.byPrice[k] | |
item.askVol += byPriceItem.askVol | |
item.bidVol += byPriceItem.bidVol | |
item.vol += byPriceItem.vol | |
} | |
last = float | |
} | |
) | |
const greatestCombinedDelta = Object.keys(this.combinedByPrice) | |
.map(k => this.combinedByPrice[k]) | |
.map(({bidVol, askVol}) => Math.abs(bidVol - askVol)) | |
.reduce((a, b) => Math.max(a, b), 0) | |
const combinedNumBars = range/modifiedSize | |
let lastPush, initPush | |
Object.values(this.combinedByPrice).forEach(({price, askVol, bidVol}) => { | |
if(!initPush) initPush = price | |
if(lastPush && lastPush > initPush + modifiedSize) { | |
initPush = price | |
} | |
if(lastPush && lastPush <= initPush + modifiedSize) { | |
const delta = -(bidVol - askVol) | |
const textLength = delta.toString().length | |
const barWidth = min( | |
px(-2), | |
px(-this.props.scaleFactorPx * Math.abs(delta/greatestCombinedDelta)) | |
) | |
const barHeight = du(range/combinedNumBars) | |
const fontSize = 10 | |
const textPt = { | |
x: op( | |
px(-(fontSize)*(.667*textLength)), | |
'+', | |
barWidth | |
), | |
y: op(du(price),'-',du(modifiedSize/2)) | |
} | |
combinedBars.children.push( | |
{ | |
tag: 'Shapes', | |
key: 'volumeBars_' + price, | |
primitives: [ | |
{ | |
tag: 'Rectangle', | |
position: { | |
x: px(0), | |
y: op(du(price), '-', du(modifiedSize)) | |
}, | |
size: { | |
height: barHeight, | |
width: barWidth | |
} | |
} | |
], | |
fillStyle: { | |
color: | |
delta > 0 ? this.props.positiveColor | |
: delta < 0 ? this.props.negativeColor | |
: '#999', | |
} | |
} | |
) | |
combinedText.children.push( | |
{ | |
tag: 'Text', | |
key: 'text_' + price, | |
text: `${delta}`, | |
point: textPt, | |
style: { | |
fontSize, | |
fill: '#999' | |
}, | |
textAlignment: 'rightMiddle' | |
} | |
) | |
} | |
lastPush = price | |
}) | |
} | |
keys.map(k => this.byPrice[k]) | |
.forEach(({price, askVol, bidVol}) => { | |
const delta = -(bidVol - askVol) | |
const textLength = delta.toString().length | |
const barWidth = min( | |
px(-2), | |
px(-this.props.scaleFactorPx * Math.abs(delta/greatestDelta)) | |
) | |
const barHeight = op(du(range/numBars), '-', px(2)) | |
const fontSize = 10 | |
const textPt = { | |
x: op( | |
px(-(fontSize)*(.667*textLength)), | |
'+', | |
barWidth | |
), | |
y: du(price), | |
} | |
barsContainer.children.push( | |
{ | |
tag: 'Shapes', | |
key: 'volumeBars_' + price, | |
primitives: [ | |
{ | |
tag: 'Rectangle', | |
position: { | |
x: px(0), | |
y: op(du(price), '-', du(this.contractInfo.tickSize/2)) | |
}, | |
size: { | |
height: barHeight, | |
width: barWidth | |
} | |
} | |
], | |
fillStyle: { | |
color: | |
delta > 0 ? this.props.positiveColor | |
: delta < 0 ? this.props.negativeColor | |
: '#999', | |
} | |
} | |
) | |
textContainer.children.push( | |
{ | |
tag: 'Text', | |
key: 'text_' + price, | |
text: `${delta}`, | |
point: textPt, | |
style: { | |
fontSize, | |
fill: '#999' | |
}, | |
textAlignment: 'rightMiddle' | |
} | |
) | |
}) | |
} | |
return { | |
graphics: { | |
items: [ | |
barsContainer, | |
textContainer, | |
combinedBars, | |
combinedText | |
] | |
} | |
} | |
} | |
} | |
module.exports = { | |
inputType: 'bars', | |
name: "PriceAxisDeltaCandidate", | |
description: "Price Axis Volume Delta", | |
calculator: PriceAxisDelta, | |
params: { | |
marketOpenHours: predef.paramSpecs.number(17,1,1), | |
marketOpenMinutes: predef.paramSpecs.number(0,1,0), | |
marketCloseHours: predef.paramSpecs.number(18,1,1), | |
marketCloseMinutes: predef.paramSpecs.number(0,1,0), | |
positiveColor: predef.paramSpecs.color('#9d5'), | |
negativeColor: predef.paramSpecs.color('#d55'), | |
startsYesterday: predef.paramSpecs.bool(true), | |
closesTomorrow: predef.paramSpecs.bool(false), | |
scaleFactorPx: predef.paramSpecs.number(50, 10, 10), | |
priceLevelsPerBar: predef.paramSpecs.number(1,1,1) | |
}, | |
tags: [predef.tags.Volumes], | |
schemeStyles: predef.styles.solidLine("#000"), | |
requirements: { | |
volumeProfiles: true | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment