- Added async methods to polygon.
- The `requests` library is no longer required, with `urllib` being used instead.
- Added the `get_bar_data` function, which returns a dataframe of aggregate data from polygon.
- Opened up the `subscribe` and `unsubscribe` functions

Enhancements:
- Tables will now scroll when the rows exceed table height.

Bugs:
- Fixed a bug preventing async functions being used with horizontal line event.
- Fixed a bug causing the legend to show duplicate lines if the line was created after the legend.
- Fixed a bug causing the line hide icon to persist within the legend after deletion (#75)
- Fixed a bug causing the search box to be unfocused when the chart is loaded.
This commit is contained in:
louisnw
2023-08-27 00:20:05 +01:00
parent 34ce3f7199
commit f72baf95ba
49 changed files with 43156 additions and 1895 deletions

View File

@ -1,103 +1,113 @@
function makeChart(innerWidth, innerHeight, autoSize=true) {
let chart = {
markers: [],
horizontal_lines: [],
lines: [],
wrapper: document.createElement('div'),
div: document.createElement('div'),
scale: {
width: innerWidth,
height: innerHeight,
},
candleData: [],
commandFunctions: [],
precision: 2,
}
chart.chart = LightweightCharts.createChart(chart.div, {
width: window.innerWidth*innerWidth,
height: window.innerHeight*innerHeight,
layout: {
textColor: '#d1d4dc',
background: {
color:'#000000',
type: LightweightCharts.ColorType.Solid,
},
fontSize: 12
},
rightPriceScale: {
scaleMargins: {top: 0.3, bottom: 0.25},
},
timeScale: {timeVisible: true, secondsVisible: false},
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
vertLine: {
labelBackgroundColor: 'rgb(46, 46, 46)'
},
horzLine: {
labelBackgroundColor: 'rgb(55, 55, 55)'
if (!window.Chart) {
class Chart {
constructor(chartId, innerWidth, innerHeight, position, autoSize) {
this.makeCandlestickSeries = this.makeCandlestickSeries.bind(this)
this.reSize = this.reSize.bind(this)
this.id = chartId
this.lines = []
this.wrapper = document.createElement('div')
this.div = document.createElement('div')
this.scale = {
width: innerWidth,
height: innerHeight,
}
},
grid: {
vertLines: {color: 'rgba(29, 30, 38, 5)'},
horzLines: {color: 'rgba(29, 30, 58, 5)'},
},
handleScroll: {vertTouchDrag: true},
})
let up = 'rgba(39, 157, 130, 100)'
let down = 'rgba(200, 97, 100, 100)'
chart.series = chart.chart.addCandlestickSeries({
color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
})
chart.volumeSeries = chart.chart.addHistogramSeries({
color: '#26a69a',
priceFormat: {type: 'volume'},
priceScaleId: '',
})
chart.series.priceScale().applyOptions({
scaleMargins: {top: 0.2, bottom: 0.2},
});
chart.volumeSeries.priceScale().applyOptions({
scaleMargins: {top: 0.8, bottom: 0},
});
chart.wrapper.style.width = `${100*innerWidth}%`
chart.wrapper.style.height = `${100*innerHeight}%`
chart.wrapper.style.display = 'flex'
chart.wrapper.style.flexDirection = 'column'
chart.wrapper.style.position = 'relative'
this.commandFunctions = []
this.chart = LightweightCharts.createChart(this.div, {
width: window.innerWidth * innerWidth,
height: window.innerHeight * innerHeight,
layout: {
textColor: '#d1d4dc',
background: {
color: '#000000',
type: LightweightCharts.ColorType.Solid,
},
fontSize: 12
},
rightPriceScale: {
scaleMargins: {top: 0.3, bottom: 0.25},
},
timeScale: {timeVisible: true, secondsVisible: false},
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
vertLine: {
labelBackgroundColor: 'rgb(46, 46, 46)'
},
horzLine: {
labelBackgroundColor: 'rgb(55, 55, 55)'
}
},
grid: {
vertLines: {color: 'rgba(29, 30, 38, 5)'},
horzLines: {color: 'rgba(29, 30, 58, 5)'},
},
handleScroll: {vertTouchDrag: true},
})
this.wrapper.style.width = `${100 * innerWidth}%`
this.wrapper.style.height = `${100 * innerHeight}%`
this.wrapper.style.display = 'flex'
this.wrapper.style.flexDirection = 'column'
this.wrapper.style.position = 'relative'
this.wrapper.style.float = position
chart.div.style.position = 'relative'
chart.div.style.display = 'flex'
chart.wrapper.appendChild(chart.div)
document.getElementById('wrapper').append(chart.wrapper)
this.div.style.position = 'relative'
this.div.style.display = 'flex'
this.wrapper.appendChild(this.div)
document.getElementById('wrapper').append(this.wrapper)
document.addEventListener('keydown', (event) => {
for (let i=0; i<chart.commandFunctions.length; i++) {
if (chart.commandFunctions[i](event)) break
document.addEventListener('keydown', (event) => {
for (let i = 0; i < this.commandFunctions.length; i++) {
if (this.commandFunctions[i](event)) break
}
})
if (!autoSize) return
window.addEventListener('resize', () => this.reSize())
}
})
reSize() {
let topBarOffset = 'topBar' in this ? this.topBar.offsetHeight : 0
this.chart.resize(window.innerWidth * this.scale.width, (window.innerHeight * this.scale.height) - topBarOffset)
}
makeCandlestickSeries() {
this.markers = []
this.horizontal_lines = []
this.candleData = []
this.precision = 2
let up = 'rgba(39, 157, 130, 100)'
let down = 'rgba(200, 97, 100, 100)'
this.series = this.chart.addCandlestickSeries({
color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
})
this.volumeSeries = this.chart.addHistogramSeries({
color: '#26a69a',
priceFormat: {type: 'volume'},
priceScaleId: '',
})
this.series.priceScale().applyOptions({
scaleMargins: {top: 0.2, bottom: 0.2},
});
this.volumeSeries.priceScale().applyOptions({
scaleMargins: {top: 0.8, bottom: 0},
});
}
toJSON() {
// Exclude the chart attribute from serialization
const {chart, ...serialized} = this;
return serialized;
}
}
window.Chart = Chart
if (!autoSize) return chart
window.addEventListener('resize', () => reSize(chart))
return chart
}
function reSize(chart) {
let topBarOffset = 'topBar' in chart ? chart.topBar.offsetHeight : 0
chart.chart.resize(window.innerWidth*chart.scale.width, (window.innerHeight*chart.scale.height)-topBarOffset)
}
if (!window.HorizontalLine) {
class HorizontalLine {
constructor(chart, lineId, price, color, width, style, axisLabelVisible, text) {
this.updatePrice = this.updatePrice.bind(this)
this.deleteLine = this.deleteLine.bind(this)
this.chart = chart
this.price = price
this.color = color
this.id = lineId
this.priceLine = {
price: this.price,
color: color,
color: this.color,
lineWidth: width,
lineStyle: style,
axisLabelVisible: axisLabelVisible,
@ -128,7 +138,8 @@ if (!window.HorizontalLine) {
updateColor(color) {
this.chart.series.removePriceLine(this.line)
this.priceLine.color = color
this.color = color
this.priceLine.color = this.color
this.line = this.chart.series.createPriceLine(this.priceLine)
}
@ -210,11 +221,7 @@ if (!window.HorizontalLine) {
makeLines(chart) {
this.lines = []
if (this.linesEnabled) {
chart.lines.forEach((line) => {
this.lines.push(this.makeLineRow(line))
})
}
if (this.linesEnabled) chart.lines.forEach(line => this.lines.push(this.makeLineRow(line)))
}
makeLineRow(line) {
@ -322,39 +329,29 @@ function syncCrosshairs(childChart, parentChart) {
childChart.subscribeCrosshairMove(childCrosshairHandler)
}
function chartTimeToDate(stampOrBusiness) {
if (typeof stampOrBusiness === 'number') {
stampOrBusiness = new Date(stampOrBusiness*1000)
}
else if (typeof stampOrBusiness === 'string') {
let [year, month, day] = stampOrBusiness.split('-').map(Number)
stampOrBusiness = new Date(Date.UTC(year, month-1, day))
}
else {
stampOrBusiness = new Date(Date.UTC(stampOrBusiness.year, stampOrBusiness.month - 1, stampOrBusiness.day))
}
return stampOrBusiness
function stampToDate(stampOrBusiness) {
return new Date(stampOrBusiness*1000)
}
function dateToStamp(date) {
return Math.floor(date.getTime()/1000)
}
function dateToChartTime(date, interval) {
if (interval >= 24*60*60*1000) {
return {day: date.getUTCDate(), month: date.getUTCMonth()+1, year: date.getUTCFullYear()}
}
return Math.floor(date.getTime()/1000)
function lastBar(obj) {
return obj[obj.length-1]
}
function calculateTrendLine(startDate, startValue, endDate, endValue, interval, chart, ray=false) {
let reversed = false
if (chartTimeToDate(endDate).getTime() < chartTimeToDate(startDate).getTime()) {
if (stampToDate(endDate).getTime() < stampToDate(startDate).getTime()) {
reversed = true;
[startDate, endDate] = [endDate, startDate];
}
let startIndex
if (chartTimeToDate(startDate).getTime() < chartTimeToDate(chart.candleData[0].time).getTime()) {
if (stampToDate(startDate).getTime() < stampToDate(chart.candleData[0].time).getTime()) {
startIndex = 0
}
else {
startIndex = chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(startDate).getTime())
startIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(startDate).getTime())
}
if (startIndex === -1) {
@ -366,9 +363,9 @@ function calculateTrendLine(startDate, startValue, endDate, endValue, interval,
startValue = endValue
}
else {
endIndex = chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(endDate).getTime())
endIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(endDate).getTime())
if (endIndex === -1) {
let barsBetween = (chartTimeToDate(endDate)-chartTimeToDate(chart.candleData[chart.candleData.length-1].time))/interval
let barsBetween = (stampToDate(endDate)-stampToDate(chart.candleData[chart.candleData.length-1].time))/interval
endIndex = chart.candleData.length-1+barsBetween
}
}
@ -384,8 +381,7 @@ function calculateTrendLine(startDate, startValue, endDate, endValue, interval,
}
else {
iPastData ++
currentDate = dateToChartTime(new Date(chartTimeToDate(chart.candleData[chart.candleData.length-1].time).getTime()+(iPastData*interval)), interval)
currentDate = dateToStamp(new Date(stampToDate(chart.candleData[chart.candleData.length-1].time).getTime()+(iPastData*interval)))
}
const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i;
@ -431,22 +427,23 @@ if (!window.ContextMenu) {
active ? document.addEventListener('contextmenu', this.onRightClick) : document.removeEventListener('contextmenu', this.onRightClick)
}
menuItem(text, action, hover=false) {
let item = document.createElement('div')
let item = document.createElement('span')
item.style.display = 'flex'
item.style.alignItems = 'center'
item.style.justifyContent = 'space-between'
item.style.padding = '0px 10px'
item.style.margin = '3px 0px'
item.style.padding = '2px 10px'
item.style.margin = '1px 0px'
item.style.borderRadius = '3px'
this.menu.appendChild(item)
let elem = document.createElement('div')
let elem = document.createElement('span')
elem.innerText = text
item.appendChild(elem)
if (hover) {
let arrow = document.createElement('div')
arrow.innerHTML = `<svg width="15px" height="10px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.82054 20.7313C8.21107 21.1218 8.84423 21.1218 9.23476 20.7313L15.8792 14.0868C17.0505 12.9155 17.0508 11.0167 15.88 9.84497L9.3097 3.26958C8.91918 2.87905 8.28601 2.87905 7.89549 3.26958C7.50497 3.6601 7.50497 4.29327 7.89549 4.68379L14.4675 11.2558C14.8581 11.6464 14.8581 12.2795 14.4675 12.67L7.82054 19.317C7.43002 19.7076 7.43002 20.3407 7.82054 20.7313Z" fill="#fff"/></svg>`
let arrow = document.createElement('span')
arrow.innerText = ``
arrow.style.fontSize = '8px'
item.appendChild(arrow)
}
@ -457,13 +454,17 @@ if (!window.ContextMenu) {
})
elem.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent')
if (!hover) elem.addEventListener('click', (event) => {action(event); this.menu.style.display = 'none'})
else elem.addEventListener('mouseover', () => action(item.getBoundingClientRect()))
else {
let timeout
elem.addEventListener('mouseover', () => timeout = setTimeout(() => action(item.getBoundingClientRect()), 100))
elem.addEventListener('mouseout', () => clearTimeout(timeout))
}
}
separator() {
let separator = document.createElement('div')
separator.style.width = '90%'
separator.style.height = '1px'
separator.style.margin = '4px 0px'
separator.style.margin = '3px 0px'
separator.style.backgroundColor = '#3C434C'
this.menu.appendChild(separator)
}
@ -471,3 +472,5 @@ if (!window.ContextMenu) {
}
window.ContextMenu = ContextMenu
}
window.callbackFunction = () => undefined;