- 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

@ -0,0 +1,16 @@
import pygments.styles
bulb = pygments.styles.get_style_by_name('lightbulb')
sas = pygments.styles.get_style_by_name('sas')
class DarkStyle(bulb):
background_color = '#1e2124ff'
class LightStyle(sas):
background_color = '#efeff4ff'

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,478 @@
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,
}
this.container = document.getElementById('wrapper')
this.commandFunctions = []
this.chart = LightweightCharts.createChart(this.div, {
width: this.container.clientWidth * innerWidth,
height: this.container.clientHeight * 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.display = 'flex'
this.wrapper.style.float = position
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 < 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(this.container.clientWidth * this.scale.width, (this.container.clientHeight * 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
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: this.color,
lineWidth: width,
lineStyle: style,
axisLabelVisible: axisLabelVisible,
title: text,
}
this.line = this.chart.series.createPriceLine(this.priceLine)
this.chart.horizontal_lines.push(this)
}
toJSON() {
// Exclude the chart attribute from serialization
const {chart, line, ...serialized} = this;
return serialized;
}
updatePrice(price) {
this.chart.series.removePriceLine(this.line)
this.price = price
this.priceLine.price = this.price
this.line = this.chart.series.createPriceLine(this.priceLine)
}
updateLabel(text) {
this.chart.series.removePriceLine(this.line)
this.priceLine.title = text
this.line = this.chart.series.createPriceLine(this.priceLine)
}
updateColor(color) {
this.chart.series.removePriceLine(this.line)
this.color = color
this.priceLine.color = this.color
this.line = this.chart.series.createPriceLine(this.priceLine)
}
deleteLine() {
this.chart.series.removePriceLine(this.line)
this.chart.horizontal_lines.splice(this.chart.horizontal_lines.indexOf(this))
delete this
}
}
window.HorizontalLine = HorizontalLine
class Legend {
constructor(chart, ohlcEnabled, percentEnabled, linesEnabled,
color = 'rgb(191, 195, 203)', fontSize = '11', fontFamily = 'Monaco') {
this.div = document.createElement('div')
this.div.style.position = 'absolute'
this.div.style.zIndex = '3000'
this.div.style.pointerEvents = 'none'
this.div.style.top = '10px'
this.div.style.left = '10px'
this.div.style.display = 'flex'
this.div.style.flexDirection = 'column'
this.div.style.maxWidth = `${(chart.scale.width * 100) - 8}vw`
this.div.style.color = color
this.div.style.fontSize = fontSize + 'px'
this.div.style.fontFamily = fontFamily
this.candle = document.createElement('div')
this.div.appendChild(this.candle)
chart.div.appendChild(this.div)
this.color = color
this.linesEnabled = linesEnabled
this.makeLines(chart)
let legendItemFormat = (num, decimal) => num.toFixed(decimal).toString().padStart(8, ' ')
let shorthandFormat = (num) => {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString().padStart(8, ' ');
}
chart.chart.subscribeCrosshairMove((param) => {
if (param.time) {
let data = param.seriesData.get(chart.series);
let finalString = '<span style="line-height: 1.8;">'
if (data) {
this.candle.style.color = ''
let ohlc = `O ${legendItemFormat(data.open, chart.precision)}
| H ${legendItemFormat(data.high, chart.precision)}
| L ${legendItemFormat(data.low, chart.precision)}
| C ${legendItemFormat(data.close, chart.precision)} `
let percentMove = ((data.close - data.open) / data.open) * 100
let percent = `| ${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`
finalString += ohlcEnabled ? ohlc : ''
finalString += percentEnabled ? percent : ''
let volumeData = param.seriesData.get(chart.volumeSeries)
if (volumeData) finalString += ohlcEnabled ? `<br>V ${shorthandFormat(volumeData.value)}` : ''
}
this.candle.innerHTML = finalString + '</span>'
this.lines.forEach((line) => {
if (!param.seriesData.get(line.line.series)) return
let price = legendItemFormat(param.seriesData.get(line.line.series).value, line.line.precision)
line.div.innerHTML = `<span style="color: ${line.solid};">▨</span> ${line.line.name} : ${price}`
})
} else {
this.candle.style.color = 'transparent'
}
});
}
makeLines(chart) {
this.lines = []
if (this.linesEnabled) chart.lines.forEach(line => this.lines.push(this.makeLineRow(line)))
}
makeLineRow(line) {
let openEye = `
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 21.998437 12 C 21.998437 12 18.998437 18 12 18 C 5.001562 18 2.001562 12 2.001562 12 C 2.001562 12 5.001562 6 12 6 C 18.998437 6 21.998437 12 21.998437 12 Z M 21.998437 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 15 12 C 15 13.654687 13.654687 15 12 15 C 10.345312 15 9 13.654687 9 12 C 9 10.345312 10.345312 9 12 9 C 13.654687 9 15 10.345312 15 12 Z M 15 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>\`
`
let closedEye = `
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 20.001562 9 C 20.001562 9 19.678125 9.665625 18.998437 10.514062 M 12 14.001562 C 10.392187 14.001562 9.046875 13.589062 7.95 12.998437 M 12 14.001562 C 13.607812 14.001562 14.953125 13.589062 16.05 12.998437 M 12 14.001562 L 12 17.498437 M 3.998437 9 C 3.998437 9 4.354687 9.735937 5.104687 10.645312 M 7.95 12.998437 L 5.001562 15.998437 M 7.95 12.998437 C 6.689062 12.328125 5.751562 11.423437 5.104687 10.645312 M 16.05 12.998437 L 18.501562 15.998437 M 16.05 12.998437 C 17.38125 12.290625 18.351562 11.320312 18.998437 10.514062 M 5.104687 10.645312 L 2.001562 12 M 18.998437 10.514062 L 21.998437 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>
`
let row = document.createElement('div')
row.style.display = 'flex'
row.style.alignItems = 'center'
let div = document.createElement('div')
let toggle = document.createElement('div')
toggle.style.borderRadius = '4px'
toggle.style.marginLeft = '10px'
toggle.style.pointerEvents = 'auto'
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "22");
svg.setAttribute("height", "16");
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
group.innerHTML = openEye
let on = true
toggle.addEventListener('click', (event) => {
if (on) {
on = false
group.innerHTML = closedEye
line.series.applyOptions({
visible: false
})
} else {
on = true
line.series.applyOptions({
visible: true
})
group.innerHTML = openEye
}
})
toggle.addEventListener('mouseover', (event) => {
document.body.style.cursor = 'pointer'
toggle.style.backgroundColor = 'rgba(50, 50, 50, 0.5)'
})
toggle.addEventListener('mouseleave', (event) => {
document.body.style.cursor = 'default'
toggle.style.backgroundColor = 'transparent'
})
svg.appendChild(group)
toggle.appendChild(svg);
row.appendChild(div)
row.appendChild(toggle)
this.div.appendChild(row)
return {
div: div,
row: row,
toggle: toggle,
line: line,
solid: line.color.startsWith('rgba') ? line.color.replace(/[^,]+(?=\))/, '1') : line.color
}
}
}
window.Legend = Legend
}
function syncCrosshairs(childChart, parentChart) {
function crosshairHandler (e, thisChart, otherChart, otherHandler) {
thisChart.applyOptions({crosshair: { horzLine: {
visible: true,
labelVisible: true,
}}})
otherChart.applyOptions({crosshair: { horzLine: {
visible: false,
labelVisible: false,
}}})
otherChart.unsubscribeCrosshairMove(otherHandler)
if (e.time !== undefined) {
let xx = otherChart.timeScale().timeToCoordinate(e.time);
otherChart.setCrosshairXY(xx,300,true);
} else if (e.point !== undefined){
otherChart.setCrosshairXY(e.point.x,300,false);
}
otherChart.subscribeCrosshairMove(otherHandler)
}
let parent = 0
let child = 0
let parentCrosshairHandler = (e) => {
parent ++
if (parent < 10) return
child = 0
crosshairHandler(e, parentChart, childChart, childCrosshairHandler)
}
let childCrosshairHandler = (e) => {
child ++
if (child < 10) return
parent = 0
crosshairHandler(e, childChart, parentChart, parentCrosshairHandler)
}
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
childChart.subscribeCrosshairMove(childCrosshairHandler)
}
function stampToDate(stampOrBusiness) {
return new Date(stampOrBusiness*1000)
}
function dateToStamp(date) {
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 (stampToDate(endDate).getTime() < stampToDate(startDate).getTime()) {
reversed = true;
[startDate, endDate] = [endDate, startDate];
}
let startIndex
if (stampToDate(startDate).getTime() < stampToDate(chart.candleData[0].time).getTime()) {
startIndex = 0
}
else {
startIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(startDate).getTime())
}
if (startIndex === -1) {
return []
}
let endIndex
if (ray) {
endIndex = chart.candleData.length+1000
startValue = endValue
}
else {
endIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(endDate).getTime())
if (endIndex === -1) {
let barsBetween = (stampToDate(endDate)-stampToDate(chart.candleData[chart.candleData.length-1].time))/interval
endIndex = chart.candleData.length-1+barsBetween
}
}
let numBars = endIndex-startIndex
const rate_of_change = (endValue - startValue) / numBars;
const trendData = [];
let currentDate = null
let iPastData = 0
for (let i = 0; i <= numBars; i++) {
if (chart.candleData[startIndex+i]) {
currentDate = chart.candleData[startIndex+i].time
}
else {
iPastData ++
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;
trendData.push({ time: currentDate, value: currentValue });
}
return trendData;
}
if (!window.ContextMenu) {
class ContextMenu {
constructor() {
this.menu = document.createElement('div')
this.menu.style.position = 'absolute'
this.menu.style.zIndex = '10000'
this.menu.style.background = 'rgb(50, 50, 50)'
this.menu.style.color = '#ececed'
this.menu.style.display = 'none'
this.menu.style.borderRadius = '5px'
this.menu.style.padding = '3px 3px'
this.menu.style.fontSize = '13px'
this.menu.style.cursor = 'default'
document.body.appendChild(this.menu)
this.hoverItem = null
let closeMenu = (event) => {
if (!this.menu.contains(event.target)) {
this.menu.style.display = 'none';
this.listen(false)
}
}
this.onRightClick = (event) => {
event.preventDefault();
this.menu.style.left = event.clientX + 'px';
this.menu.style.top = event.clientY + 'px';
this.menu.style.display = 'block';
document.removeEventListener('click', closeMenu)
document.addEventListener('click', closeMenu)
}
}
listen(active) {
active ? document.addEventListener('contextmenu', this.onRightClick) : document.removeEventListener('contextmenu', this.onRightClick)
}
menuItem(text, action, hover=false) {
let item = document.createElement('span')
item.style.display = 'flex'
item.style.alignItems = 'center'
item.style.justifyContent = 'space-between'
item.style.padding = '2px 10px'
item.style.margin = '1px 0px'
item.style.borderRadius = '3px'
this.menu.appendChild(item)
let elem = document.createElement('span')
elem.innerText = text
item.appendChild(elem)
if (hover) {
let arrow = document.createElement('span')
arrow.innerText = ``
arrow.style.fontSize = '8px'
item.appendChild(arrow)
}
elem.addEventListener('mouseover', (event) => {
item.style.backgroundColor = 'rgba(0, 122, 255, 0.3)'
if (this.hoverItem && this.hoverItem.closeAction) this.hoverItem.closeAction()
this.hoverItem = {elem: elem, action: action, closeAction: hover}
})
elem.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent')
if (!hover) elem.addEventListener('click', (event) => {action(event); this.menu.style.display = 'none'})
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 = '3px 0px'
separator.style.backgroundColor = '#3C434C'
this.menu.appendChild(separator)
}
}
window.ContextMenu = ContextMenu
}
window.callbackFunction = () => undefined;

38761
docs/source/_static/ohlcv.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,140 @@
@font-face {
font-family: 'Maison Neue';
src: url('fonts/MaisonNeue-Demi.otf') format('opentype');
}
@font-face {
font-family: 'Maison Neue Italic';
src: url('fonts/MaisonNeue-MediumItalic.otf') format('opentype');
}
.splash-head {
position: relative;
display: flex;
flex-direction: column;
text-align: center;
align-self: center;
margin-bottom: 20px;
}
.splash-page-container {
display: flex;
flex-direction: column;
}
.theme-button {
position: absolute;
top: 70px;
right: -50px;
}
.top-nav {
margin: 0;
padding: 0;
align-self: center;
}
.top-nav ul {
list-style-type: none;
padding: 0;
display: flex;
justify-content: space-evenly;
background-color: var(--color-background-hover);
border-radius: 5px;
margin: 2rem 0 0;
}
.top-nav li {
display: inline;
}
.top-nav li a {
text-decoration: none;
border-radius: 3px;
color: var(--color-foreground-primary);
padding: 0.3rem 0.6rem;
display: flex;
align-items: center;
font-weight: 500;
}
.top-nav li a:hover {
background-color: var(--color-background-item)
}
#wrapper {
width: 500px;
height: 350px;
display: flex;
overflow: hidden;
border-radius: 10px;
border: 2px solid var(--color-foreground-muted);
box-sizing: border-box;
position: relative;
z-index: 1000;
}
#main-content {
margin: 30px 0;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
#curved-arrow {
transition: transform 0.1s;
transform: rotate(-42deg);
padding: 0 3vw;
width: 100px;
height: 100px;
}
@media (max-width: 968px) {
#curved-arrow {
transform: rotate(-70deg) scalex(-1);
}
}
@media (max-width: 550px) {
.splash-head h1 {
font-size: 30px;
}
#main-content {
flex-direction: column;
}
#curved-arrow {
padding: 4vw 0;
width: 60px;
height: 60px
}
}
@media (max-width: 450px) {
.splash-head h1 {
font-size: 22px;
}
.splash-head i {
font-size: 13px;
}
.top-nav a {
font-size: 12px;
}
.theme-button {
right: -20px;
}
#wrapper {
width: 300px;
height: 250px;
}
}

View File

@ -0,0 +1,688 @@
if (!window.ToolBox) {
class ToolBox {
constructor(chart) {
this.onTrendSelect = this.onTrendSelect.bind(this)
this.onHorzSelect = this.onHorzSelect.bind(this)
this.onRaySelect = this.onRaySelect.bind(this)
this.saveDrawings = this.saveDrawings.bind(this)
this.chart = chart
this.drawings = []
this.chart.cursor = 'default'
this.makingDrawing = false
this.interval = 24 * 60 * 60 * 1000
this.activeBackgroundColor = 'rgba(0, 122, 255, 0.7)'
this.activeIconColor = 'rgb(240, 240, 240)'
this.iconColor = 'lightgrey'
this.backgroundColor = 'transparent'
this.hoverColor = 'rgba(80, 86, 94, 0.7)'
this.clickBackgroundColor = 'rgba(90, 106, 104, 0.7)'
this.elem = this.makeToolBox()
this.subscribeHoverMove()
}
toJSON() {
// Exclude the chart attribute from serialization
const {chart, ...serialized} = this;
return serialized;
}
makeToolBox() {
let toolBoxElem = document.createElement('div')
toolBoxElem.style.position = 'absolute'
toolBoxElem.style.zIndex = '2000'
toolBoxElem.style.display = 'flex'
toolBoxElem.style.alignItems = 'center'
toolBoxElem.style.top = '25%'
toolBoxElem.style.borderRight = '2px solid #3C434C'
toolBoxElem.style.borderTop = '2px solid #3C434C'
toolBoxElem.style.borderBottom = '2px solid #3C434C'
toolBoxElem.style.borderTopRightRadius = '4px'
toolBoxElem.style.borderBottomRightRadius = '4px'
toolBoxElem.style.backgroundColor = 'rgba(25, 27, 30, 0.5)'
toolBoxElem.style.flexDirection = 'column'
this.chart.activeIcon = null
let trend = this.makeToolBoxElement(this.onTrendSelect, 'KeyT', `<rect x="3.84" y="13.67" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -5.9847 14.4482)" width="21.21" height="1.56"/><path d="M23,3.17L20.17,6L23,8.83L25.83,6L23,3.17z M23,7.41L21.59,6L23,4.59L24.41,6L23,7.41z"/><path d="M6,20.17L3.17,23L6,25.83L8.83,23L6,20.17z M6,24.41L4.59,23L6,21.59L7.41,23L6,24.41z"/>`)
let horz = this.makeToolBoxElement(this.onHorzSelect, 'KeyH', `<rect x="4" y="14" width="9" height="1"/><rect x="16" y="14" width="9" height="1"/><path d="M11.67,14.5l2.83,2.83l2.83-2.83l-2.83-2.83L11.67,14.5z M15.91,14.5l-1.41,1.41l-1.41-1.41l1.41-1.41L15.91,14.5z"/>`)
let ray = this.makeToolBoxElement(this.onRaySelect, 'KeyR', `<rect x="8" y="14" width="17" height="1"/><path d="M3.67,14.5l2.83,2.83l2.83-2.83L6.5,11.67L3.67,14.5z M7.91,14.5L6.5,15.91L5.09,14.5l1.41-1.41L7.91,14.5z"/>`)
//let testB = this.makeToolBoxElement(this.onTrendSelect, `<rect x="8" y="6" width="12" height="1"/><rect x="9" y="22" width="11" height="1"/><path d="M3.67,6.5L6.5,9.33L9.33,6.5L6.5,3.67L3.67,6.5z M7.91,6.5L6.5,7.91L5.09,6.5L6.5,5.09L7.91,6.5z"/><path d="M19.67,6.5l2.83,2.83l2.83-2.83L22.5,3.67L19.67,6.5z M23.91,6.5L22.5,7.91L21.09,6.5l1.41-1.41L23.91,6.5z"/><path d="M19.67,22.5l2.83,2.83l2.83-2.83l-2.83-2.83L19.67,22.5z M23.91,22.5l-1.41,1.41l-1.41-1.41l1.41-1.41L23.91,22.5z"/><path d="M3.67,22.5l2.83,2.83l2.83-2.83L6.5,19.67L3.67,22.5z M7.91,22.5L6.5,23.91L5.09,22.5l1.41-1.41L7.91,22.5z"/><rect x="22" y="9" width="1" height="11"/><rect x="6" y="9" width="1" height="11"/>`)
toolBoxElem.appendChild(trend)
toolBoxElem.appendChild(horz)
toolBoxElem.appendChild(ray)
//toolBoxElem.appendChild(testB)
this.chart.div.append(toolBoxElem)
let commandZHandler = (toDelete) => {
if (!toDelete) return
if ('price' in toDelete && toDelete.id !== 'toolBox') return commandZHandler(this.drawings[this.drawings.indexOf(toDelete) - 1])
this.deleteDrawing(toDelete)
}
this.chart.commandFunctions.push((event) => {
if ((event.metaKey || event.ctrlKey) && event.code === 'KeyZ') {
commandZHandler(this.drawings[this.drawings.length - 1])
return true
}
});
return toolBoxElem
}
makeToolBoxElement(action, keyCmd, paths) {
let icon = {
elem: document.createElement('div'),
action: action,
}
icon.elem.style.margin = '3px'
icon.elem.style.borderRadius = '4px'
icon.elem.style.display = 'flex'
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "29");
svg.setAttribute("height", "29");
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
group.innerHTML = paths
group.setAttribute("fill", this.iconColor)
svg.appendChild(group)
icon.elem.appendChild(svg);
icon.elem.addEventListener('mouseenter', () => {
icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.hoverColor
})
icon.elem.addEventListener('mouseleave', () => {
icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.backgroundColor
})
icon.elem.addEventListener('mousedown', () => {
icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : this.clickBackgroundColor
})
icon.elem.addEventListener('mouseup', () => {
icon.elem.style.backgroundColor = icon === this.chart.activeIcon ? this.activeBackgroundColor : 'transparent'
})
icon.elem.addEventListener('click', () => {
if (this.chart.activeIcon) {
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
group.setAttribute("fill", this.iconColor)
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
this.chart.activeIcon.action(false)
if (this.chart.activeIcon === icon) {
return this.chart.activeIcon = null
}
}
this.chart.activeIcon = icon
group.setAttribute("fill", this.activeIconColor)
icon.elem.style.backgroundColor = this.activeBackgroundColor
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
this.chart.activeIcon.action(true)
})
this.chart.commandFunctions.push((event) => {
if (event.altKey && event.code === keyCmd) {
event.preventDefault()
if (this.chart.activeIcon) {
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
group.setAttribute("fill", this.iconColor)
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
this.chart.activeIcon.action(false)
}
this.chart.activeIcon = icon
group.setAttribute("fill", this.activeIconColor)
icon.elem.style.backgroundColor = this.activeBackgroundColor
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
this.chart.activeIcon.action(true)
return true
}
})
return icon.elem
}
onTrendSelect(toggle, ray = false) {
let trendLine = {
line: null,
color: 'rgb(15, 139, 237)',
markers: null,
data: null,
from: null,
to: null,
ray: ray,
}
let firstTime = null
let firstPrice = null
let currentTime = null
if (!toggle) {
this.chart.chart.unsubscribeClick(this.clickHandler)
return
}
let crosshairHandlerTrend = (param) => {
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
if (!this.makingDrawing) return
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time
currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
if (!currentTime) {
let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime))
currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)))
}
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart, ray)
trendLine.from = [data[0].time, data[0].value]
trendLine.to = [data[data.length - 1].time, data[data.length-1].value]
trendLine.line.setData(data)
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
if (!ray) {
trendLine.markers = [
{time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
{time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
]
trendLine.line.setMarkers(trendLine.markers)
}
setTimeout(() => {
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
}, 10);
}
this.clickHandler = (param) => {
if (!this.makingDrawing) {
this.makingDrawing = true
trendLine.line = this.chart.chart.addLineSeries({
color: 'rgb(15, 139, 237)',
lineWidth: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
autoscaleInfoProvider: () => ({
priceRange: {
minValue: 1_000_000_000,
maxValue: 0,
},
}),
})
firstPrice = this.chart.series.coordinateToPrice(param.point.y)
firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : this.chart.candleData[this.chart.candleData.length - 1].time
this.chart.chart.applyOptions({handleScroll: false})
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
}
else {
this.chart.chart.applyOptions({handleScroll: true})
this.makingDrawing = false
trendLine.line.setMarkers([])
this.drawings.push(trendLine)
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
this.chart.chart.unsubscribeClick(this.clickHandler)
document.body.style.cursor = 'default'
this.chart.cursor = 'default'
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
this.chart.activeIcon = null
this.saveDrawings()
}
}
this.chart.chart.subscribeClick(this.clickHandler)
}
clickHandlerHorz = (param) => {
let price = this.chart.series.coordinateToPrice(param.point.y)
let lineStyle = LightweightCharts.LineStyle.Solid
let line = new HorizontalLine(this.chart, 'toolBox', price,'red', 2, lineStyle, true)
this.drawings.push(line)
this.chart.chart.unsubscribeClick(this.clickHandlerHorz)
document.body.style.cursor = 'default'
this.chart.cursor = 'default'
this.chart.activeIcon.elem.style.backgroundColor = this.backgroundColor
this.chart.activeIcon = null
this.saveDrawings()
}
onHorzSelect(toggle) {
!toggle ? this.chart.chart.unsubscribeClick(this.clickHandlerHorz) : this.chart.chart.subscribeClick(this.clickHandlerHorz)
}
onRaySelect(toggle) {
this.onTrendSelect(toggle, true)
}
subscribeHoverMove() {
let hoveringOver = null
let x, y
let colorPicker = new ColorPicker(this.saveDrawings)
let onClickDelete = () => this.deleteDrawing(contextMenu.drawing)
let onClickColor = (rect) => colorPicker.openMenu(rect, contextMenu.drawing)
let contextMenu = new ContextMenu()
contextMenu.menuItem('Color Picker', onClickColor, () =>{
document.removeEventListener('click', colorPicker.closeMenu)
colorPicker.container.style.display = 'none'
})
contextMenu.separator()
contextMenu.menuItem('Delete Drawing', onClickDelete)
let hoverOver = (param) => {
if (!param.point || this.makingDrawing) return
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
x = param.point.x
y = param.point.y
this.drawings.forEach((drawing) => {
let boundaryConditional
let horizontal = false
if ('price' in drawing) {
horizontal = true
let priceCoordinate = this.chart.series.priceToCoordinate(drawing.price)
boundaryConditional = Math.abs(priceCoordinate - param.point.y) < 6
} else {
let trendData = param.seriesData.get(drawing.line);
if (!trendData) return
let priceCoordinate = this.chart.series.priceToCoordinate(trendData.value)
let timeCoordinate = this.chart.chart.timeScale().timeToCoordinate(trendData.time)
boundaryConditional = Math.abs(priceCoordinate - param.point.y) < 6 && Math.abs(timeCoordinate - param.point.x) < 6
}
if (boundaryConditional) {
if (hoveringOver === drawing) return
if (!horizontal && !drawing.ray) drawing.line.setMarkers(drawing.markers)
document.body.style.cursor = 'pointer'
document.addEventListener('mousedown', checkForClick)
document.addEventListener('mouseup', checkForRelease)
hoveringOver = drawing
contextMenu.listen(true)
contextMenu.drawing = drawing
} else if (hoveringOver === drawing) {
if (!horizontal && !drawing.ray) drawing.line.setMarkers([])
document.body.style.cursor = this.chart.cursor
hoveringOver = null
contextMenu.listen(false)
if (!mouseDown) {
document.removeEventListener('mousedown', checkForClick)
document.removeEventListener('mouseup', checkForRelease)
}
}
})
this.chart.chart.subscribeCrosshairMove(hoverOver)
}
let originalIndex
let originalTime
let originalPrice
let mouseDown = false
let clickedEnd = false
let labelColor
let checkForClick = (event) => {
mouseDown = true
document.body.style.cursor = 'grabbing'
this.chart.chart.applyOptions({handleScroll: false})
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
labelColor = this.chart.chart.options().crosshair.horzLine.labelBackgroundColor
this.chart.chart.applyOptions({crosshair: {horzLine: {labelBackgroundColor: hoveringOver.color}}})
if ('price' in hoveringOver) {
originalPrice = hoveringOver.price
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.from[0]) - x) < 4 && !hoveringOver.ray) {
clickedEnd = 'first'
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.to[0]) - x) < 4 && !hoveringOver.ray) {
clickedEnd = 'last'
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
} else {
originalPrice = this.chart.series.coordinateToPrice(y)
originalTime = this.chart.chart.timeScale().coordinateToTime(x * this.chart.scale.width)
this.chart.chart.subscribeCrosshairMove(checkForDrag)
}
originalIndex = this.chart.chart.timeScale().coordinateToLogical(x)
document.removeEventListener('mousedown', checkForClick)
}
let checkForRelease = (event) => {
mouseDown = false
document.body.style.cursor = this.chart.cursor
this.chart.chart.applyOptions({handleScroll: true})
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
this.chart.chart.applyOptions({crosshair: {horzLine: {labelBackgroundColor: labelColor}}})
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
window.callbackFunction(`${hoveringOver.id}_~_${hoveringOver.price.toFixed(8)}`);
}
hoveringOver = null
document.removeEventListener('mousedown', checkForClick)
document.removeEventListener('mouseup', checkForRelease)
this.chart.chart.subscribeCrosshairMove(hoverOver)
this.saveDrawings()
}
let checkForDrag = (param) => {
if (!param.point) return
this.chart.chart.unsubscribeCrosshairMove(checkForDrag)
if (!mouseDown) return
let priceAtCursor = this.chart.series.coordinateToPrice(param.point.y)
let priceDiff = priceAtCursor - originalPrice
let barsToMove = param.logical - originalIndex
let startBarIndex = this.chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(hoveringOver.from[0]).getTime())
let endBarIndex = this.chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(hoveringOver.to[0]).getTime())
let startDate
let endBar
if (hoveringOver.ray) {
endBar = this.chart.candleData[startBarIndex + barsToMove]
startDate = hoveringOver.to[0]
} else {
startDate = this.chart.candleData[startBarIndex + barsToMove].time
endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove]
}
let endDate = endBar ? endBar.time : dateToStamp(new Date(stampToDate(hoveringOver.to[0]).getTime() + (barsToMove * this.interval)))
let startValue = hoveringOver.from[1] + priceDiff
let endValue = hoveringOver.to[1] + priceDiff
let data = calculateTrendLine(startDate, startValue, endDate, endValue, this.interval, this.chart, hoveringOver.ray)
let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
hoveringOver.from = [data[0].time, data[0].value]
hoveringOver.to = [data[data.length - 1].time, data[data.length - 1].value]
hoveringOver.line.setData(data)
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
if (!hoveringOver.ray) {
hoveringOver.markers = [
{time: startDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
{time: endDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
]
hoveringOver.line.setMarkers(hoveringOver.markers)
}
originalIndex = param.logical
originalPrice = priceAtCursor
this.chart.chart.subscribeCrosshairMove(checkForDrag)
}
let crosshairHandlerTrend = (param) => {
if (!param.point) return
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
if (!mouseDown) return
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
let currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
let [firstTime, firstPrice] = [null, null]
if (clickedEnd === 'last') {
firstTime = hoveringOver.from[0]
firstPrice = hoveringOver.from[1]
} else if (clickedEnd === 'first') {
firstTime = hoveringOver.to[0]
firstPrice = hoveringOver.to[1]
}
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time
if (!currentTime) {
let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime))
currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)))
}
let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart)
hoveringOver.line.setData(data)
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
hoveringOver.from = [data[0].time, data[0].value]
hoveringOver.to = [data[data.length - 1].time, data[data.length - 1].value]
hoveringOver.markers = [
{time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
{time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
]
hoveringOver.line.setMarkers(hoveringOver.markers)
setTimeout(() => {
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
}, 10);
}
let crosshairHandlerHorz = (param) => {
if (!param.point) return
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerHorz)
if (!mouseDown) return
hoveringOver.updatePrice(this.chart.series.coordinateToPrice(param.point.y))
setTimeout(() => {
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
}, 10)
}
this.chart.chart.subscribeCrosshairMove(hoverOver)
}
renderDrawings() {
this.drawings.forEach((item) => {
if ('price' in item) return
let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.interval) * this.interval))
let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.interval) * this.interval))
let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray)
item.from = [data[0].time, data[0].value]
item.to = [data[data.length - 1].time, data[data.length-1].value]
item.line.setData(data)
})
}
deleteDrawing(drawing) {
if ('price' in drawing) {
this.chart.series.removePriceLine(drawing.line)
}
else {
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
this.chart.chart.removeSeries(drawing.line);
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
}
this.drawings.splice(this.drawings.indexOf(drawing), 1)
this.saveDrawings()
}
clearDrawings() {
this.drawings.forEach((item) => {
if ('price' in item) this.chart.series.removePriceLine(item.line)
else this.chart.chart.removeSeries(item.line)
})
this.drawings = []
}
saveDrawings() {
let drawingsString = JSON.stringify(this.drawings, (key, value) => {
if (key === '' && Array.isArray(value)) {
return value.filter(item => !(item && typeof item === 'object' && 'priceLine' in item && item.id !== 'toolBox'));
} else if (key === 'line' || (value && typeof value === 'object' && 'priceLine' in value && value.id !== 'toolBox')) {
return undefined;
}
return value;
});
window.callbackFunction(`save_drawings${this.chart.id}_~_${drawingsString}`)
}
loadDrawings(drawings) {
this.drawings = drawings
this.chart.chart.applyOptions({handleScroll: false})
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
this.drawings.forEach((item) => {
let idx = this.drawings.indexOf(item)
if ('price' in item) {
this.drawings[idx] = new HorizontalLine(this.chart, 'toolBox', item.priceLine.price, item.priceLine.color, 2, item.priceLine.lineStyle, item.priceLine.axisLabelVisible)
}
else {
this.drawings[idx].line = this.chart.chart.addLineSeries({
lineWidth: 2,
color: this.drawings[idx].color,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
autoscaleInfoProvider: () => ({
priceRange: {
minValue: 1_000_000_000,
maxValue: 0,
},
}),
})
let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.interval) * this.interval))
let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.interval) * this.interval))
let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray)
item.from = [data[0].time, data[0].value]
item.to = [data[data.length - 1].time, data[data.length-1].value]
item.line.setData(data)
}
})
this.chart.chart.applyOptions({handleScroll: true})
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
}
}
window.ToolBox = ToolBox
}
if (!window.ColorPicker) {
class ColorPicker {
constructor(saveDrawings) {
this.saveDrawings = saveDrawings
this.container = document.createElement('div')
this.container.style.maxWidth = '170px'
this.container.style.backgroundColor = '#191B1E'
this.container.style.position = 'absolute'
this.container.style.zIndex = '10000'
this.container.style.display = 'none'
this.container.style.flexDirection = 'column'
this.container.style.alignItems = 'center'
this.container.style.border = '2px solid #3C434C'
this.container.style.borderRadius = '8px'
this.container.style.cursor = 'default'
let colorPicker = document.createElement('div')
colorPicker.style.margin = '10px'
colorPicker.style.display = 'flex'
colorPicker.style.flexWrap = 'wrap'
let colors = [
'#EBB0B0','#E9CEA1','#E5DF80','#ADEB97','#A3C3EA','#D8BDED',
'#E15F5D','#E1B45F','#E2D947','#4BE940','#639AE1','#D7A0E8',
'#E42C2A','#E49D30','#E7D827','#3CFF0A','#3275E4','#B06CE3',
'#F3000D','#EE9A14','#F1DA13','#2DFC0F','#1562EE','#BB00EF',
'#B50911','#E3860E','#D2BD11','#48DE0E','#1455B4','#6E009F',
'#7C1713','#B76B12','#8D7A13','#479C12','#165579','#51007E',
]
colors.forEach((color) => colorPicker.appendChild(this.makeColorBox(color)))
let separator = document.createElement('div')
separator.style.backgroundColor = '#3C434C'
separator.style.height = '1px'
separator.style.width = '130px'
let opacity = document.createElement('div')
opacity.style.margin = '10px'
let opacityText = document.createElement('div')
opacityText.style.color = 'lightgray'
opacityText.style.fontSize = '12px'
opacityText.innerText = 'Opacity'
let opacityValue = document.createElement('div')
opacityValue.style.color = 'lightgray'
opacityValue.style.fontSize = '12px'
let opacitySlider = document.createElement('input')
opacitySlider.type = 'range'
opacitySlider.value = this.opacity*100
opacityValue.innerText = opacitySlider.value+'%'
opacitySlider.oninput = () => {
opacityValue.innerText = opacitySlider.value+'%'
this.opacity = opacitySlider.value/100
this.updateColor()
}
opacity.appendChild(opacityText)
opacity.appendChild(opacitySlider)
opacity.appendChild(opacityValue)
this.container.appendChild(colorPicker)
this.container.appendChild(separator)
this.container.appendChild(opacity)
document.getElementById('wrapper').appendChild(this.container)
}
makeColorBox(color) {
let box = document.createElement('div')
box.style.width = '18px'
box.style.height = '18px'
box.style.borderRadius = '3px'
box.style.margin = '3px'
box.style.boxSizing = 'border-box'
box.style.backgroundColor = color
box.addEventListener('mouseover', (event) => box.style.border = '2px solid lightgray')
box.addEventListener('mouseout', (event) => box.style.border = 'none')
let rgbValues = this.extractRGB(color)
box.addEventListener('click', (event) => {
this.rgbValues = rgbValues
this.updateColor()
})
return box
}
extractRGB = (anyColor) => {
let dummyElem = document.createElement('div');
dummyElem.style.color = anyColor;
document.body.appendChild(dummyElem);
let computedColor = getComputedStyle(dummyElem).color;
document.body.removeChild(dummyElem);
let colorValues = computedColor.match(/\d+/g).map(Number);
let isRgba = computedColor.includes('rgba');
let opacity = isRgba ? parseFloat(computedColor.split(',')[3]) : 1
return [colorValues[0], colorValues[1], colorValues[2], opacity]
}
updateColor() {
let oColor = `rgba(${this.rgbValues[0]}, ${this.rgbValues[1]}, ${this.rgbValues[2]}, ${this.opacity})`
if ('price' in this.drawing) this.drawing.updateColor(oColor)
else {
this.drawing.color = oColor
this.drawing.line.applyOptions({color: oColor})
}
this.saveDrawings()
}
openMenu(rect, drawing) {
this.drawing = drawing
this.rgbValues = this.extractRGB(drawing.color)
this.opacity = parseFloat(this.rgbValues[3])
this.container.style.top = (rect.top-30)+'px'
this.container.style.left = rect.right+'px'
this.container.style.display = 'flex'
setTimeout(() => document.addEventListener('mousedown', (event) => {
if (!this.container.contains(event.target)) {
this.closeMenu()
}
}), 10)
}
closeMenu(event) {
document.removeEventListener('click', this.closeMenu)
this.container.style.display = 'none'
}
}
window.ColorPicker = ColorPicker
}

View File

@ -0,0 +1,301 @@
<!doctype html>
<html class="no-js" lang="en" data-content_root="">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="index" title="Index" href="genindex.html"/>
<link rel="search" title="Search" href="search.html"/>
<link rel="next" title="Topbar &amp; Events" href="events.html"/>
<link rel="prev" title="lightweight-charts-python" href="index.html"/>
<!-- Generated with Sphinx 7.2.3 and Furo 2023.08.19 -->
<title>Getting Started - lightweight-charts-python 1.0.16 documentation</title>
<link rel="stylesheet" type="text/css" href=_static/pygments.css?v=045299b1"/>
<link rel="stylesheet" type="text/css" href=_static/styles/furo.css?v=135e06be"/>
<link rel="stylesheet" type="text/css" href=_static/copybutton.css?v=76b2166b"/>
<link rel="stylesheet" type="text/css" href=_static/styles/furo-extensions.css?v=36a5483c"/>
<link rel="stylesheet" type="text/css" href=_static/splash.css?v=a3b9b0a1"/>
<style>
body {
--color-code-background: #ffffff;
--color-code-foreground: black;
display: flex;
justify-content: center;
align-items: flex-start;
}
@media not print {
body[data-theme="dark"] {
--color-code-background: #1d2331;
--color-code-foreground: #d4d2c8;
--color-background-primary: #121417;
--color-background-secondary: #181b1e;
}
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
--color-code-background: #1d2331;
--color-code-foreground: #d4d2c8;
--color-background-primary: #121417;
--color-background-secondary: #181b1e;
}
}
}
</style>
</head>
<body>
<script>
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
</script>
<div class="splash-page-container">
<div class="splash-head">
<div class="theme-button">
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"/>
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9"/>
<path d="M13 12h5"/>
<path d="M13 15h4"/>
<path d="M13 18h1"/>
<path d="M13 9h4"/>
<path d="M13 6h1"/>
</svg>
</symbol>
</svg>
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto">
<use href="#svg-sun-half"></use>
</svg>
<svg class="theme-icon-when-dark">
<use href="#svg-moon"></use>
</svg>
<svg class="theme-icon-when-light">
<use href="#svg-sun"></use>
</svg>
</button>
</div>
<h1 style="font-family: 'Maison Neue',sans-serif">Lightweight Charts Python</h1>
<b style="font-family: 'Maison Neue Italic',sans-serif">TradingView charts, wrapped for Python.</b>
<nav class="top-nav">
<ul>
<li><a href="tutorials/getting_started.html">Getting Started</a></li>
<li><a href="examples/events.html">Examples</a></li>
<li><a href="reference/index.html">Documentation</a></li>
</ul>
</nav>
</div>
<!-- <hr class="docutils"/>-->
<div class="highlight-text notranslate" style=" align-self: center;">
<div class="highlight" style="text-align: center; padding-right: 25px;">
<pre><span></span>pip install lightweight-charts</pre>
</div>
</div>
<script src="_static/pkg.js"></script>
<script src="_static/funcs.js"></script>
<script src="_static/callback.js"></script>
<script src="_static/toolbox.js"></script>
<div id="main-content">
<div class="highlight-python notranslate"
style="border: 2px solid var(--color-foreground-muted); border-radius: 8px; overflow: hidden;">
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">pandas</span> <span
class="k">as</span> <span class="nn">pd</span>
<span class="kn">from</span> <span class="nn">lightweight_charts</span> <span class="kn">import</span> <span class="n">Chart</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span
class="s1">'__main__'</span><span class="p">:</span>
<span class="n">chart</span> <span class="o">=</span> <span class="n">Chart</span><span class="p">(</span><span
class="n">toolbox</span><span class="o">=</span><span class="kc">True</span><span
class="p">)</span>
<span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span
class="p">(</span><span class="s1">'ohlcv.csv'</span><span class="p">)</span>
<span class="n">chart</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span
class="n">df</span><span class="p">)</span>
<span class="n">chart</span><span class="o">.</span><span class="n">show</span><span class="p">(</span><span
class="n">block</span><span class="o">=</span><span class="kc">True</span><span
class="p">)</span>
</pre>
</div>
</div>
<svg id="curved-arrow" fill="var(--color-foreground-primary)" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 415.262 415.261"
xml:space="preserve">
<g>
<path d="M414.937,374.984c-7.956-24.479-20.196-47.736-30.601-70.992c-1.224-3.06-6.12-3.06-7.956-1.224
c-10.403,11.016-22.031,22.032-28.764,35.496h-0.612c-74.664,5.508-146.88-58.141-198.288-104.652
c-59.364-53.244-113.22-118.116-134.64-195.84c-1.224-9.792-2.448-20.196-2.448-30.6c0-4.896-6.732-4.896-7.344,0
c0,1.836,0,3.672,0,5.508C1.836,12.68,0,14.516,0,17.576c0.612,6.732,2.448,13.464,3.672,20.196
C8.568,203.624,173.808,363.356,335.376,373.76c-5.508,9.792-10.403,20.195-16.523,29.988c-3.061,4.283,1.836,8.567,6.12,7.955
c30.6-4.283,58.14-18.972,86.292-29.987C413.712,381.104,416.16,378.656,414.937,374.984z M332.928,399.464
c3.673-7.956,6.12-15.912,10.404-23.868c1.225-3.061-0.612-5.508-2.448-6.12c0-1.836-1.224-3.061-3.06-3.672
c-146.268-24.48-264.996-124.236-309.06-259.489c28.764,53.244,72.828,99.756,116.28,138.924
c31.824,28.765,65.484,54.468,102.204,75.888c28.764,16.524,64.872,31.824,97.92,21.421l0,0c-1.836,4.896,5.508,7.344,7.956,3.672
c7.956-10.404,15.912-20.196,24.48-29.376c8.567,18.972,17.748,37.943,24.479,57.527
C379.44,382.94,356.796,393.956,332.928,399.464z"/>
</g>
</svg>
<div id="wrapper"></div>
<script>
window.addEventListener('DOMContentLoaded', () => {
let chart = new Chart(document.getElementById('wrapper'), 1, 1, 'left', true)
chart.makeCandlestickSeries()
let toolbox = new ToolBox(chart)
chart.chart.applyOptions({
grid: {
vertLines: {
visible: false,
},
horzLines: {
visible: false,
},
}
})
fetch("_static/ohlcv.json")
.then(response => response.json())
.then(data => {
chart.candleData = data.candleData
chart.volumeSeries.setData(data.volume)
chart.series.setData(chart.candleData)
chart.reSize()
})
document.querySelector('.theme-toggle').addEventListener('click', () => {
let theme = localStorage.getItem('theme')
let color = theme === 'dark' ? '#000' : theme === 'light' ? '#fff' : '#000'
chart.chart.applyOptions({
layout: {
textColor: color === '#000' ? '#fff' : '#000',
background: {
color: color,
type: LightweightCharts.ColorType.Solid,
},
}
})
})
})
</script>
</div>
<div style="transform: scale(0.7); align-self: center"><script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button"
data-slug="7wzcr2p9vxM" data-color="#FFDD00" data-emoji="" data-font="Cookie"
data-text="Support the library!" data-outline-color="#000000" data-font-color="#000000"
data-coffee-color="#ffffff"></script></div>
<footer>
<div class="bottom-of-page" style="margin: 0">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, louisnw
</div>
</div>
<div class="right-details">
<div class="icons">
<a class="muted-link " href="https://github.com/louisnw01/lightweight-charts-python"
aria-label="GitHub">
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
</a>
</div>
</div>
</div>
</footer>
</div>
<script src="_static/documentation_options.js?v=588f6264"></script>
<script src="_static/doctools.js?v=888ff710"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=32e29ea5"></script>
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
<script src="_static/copybutton.js?v=f281be69"></script>
<script defer="defer" src="https://unpkg.com/@popperjs/core@2"></script>
<script defer="defer" src="https://unpkg.com/tippy.js@6"></script>
<script defer="defer" src="_static/tippy/tutorials/getting_started.8daa58e2-f7c9-4b37-adde-0459512b1058.js"></script>
</body>
</html>

View File

@ -1,156 +0,0 @@
# Events
Events allow asynchronous and synchronous callbacks to be passed back into python.
___
## `chart.events`
`events.search` `->` `chart` | `string`: Fires upon searching. Searchbox will be automatically created.
`events.new_bar` `->` `chart`: Fires when a new candlestick is added to the chart.
`events.range_change` `->` `chart` | `bars_before` | `bars_after`: Fires when the range (visibleLogicalRange) changes.
Chart events can be subscribed to using: `chart.events.<name> += <callable>`
___
## How to use Events
Take a look at this minimal example:
```python
from lightweight_charts import Chart
def on_search(chart, string):
print(f'Search Text: "{string}" | Chart/SubChart ID: "{chart.id}"')
if __name__ == '__main__':
chart = Chart()
chart.events.search += on_search
chart.show(block=True)
```
Upon searching in a pane, the expected output would be akin to:
```
Search Text: "AAPL" | Chart/SubChart ID: "window.blyjagcr"
```
The ID shown above will change depending upon which pane was used to search, allowing for access to the object in question.
```{important}
* When using `show` rather than `show_async`, block should be set to `True` (`chart.show(block=True)`).
* Event callables can be either coroutines, methods, or functions.
```
___
## `TopBar`
The `TopBar` class represents the top bar shown on the chart:
![topbar](https://i.imgur.com/Qu2FW9Y.png)
This object is accessed from the `topbar` attribute of the chart object (`chart.topbar.<method>`).
Switchers, text boxes and buttons can be added to the top bar, and their instances can be accessed through the `topbar` dictionary. For example:
```python
chart.topbar.textbox('symbol', 'AAPL') # Declares a textbox displaying 'AAPL'.
print(chart.topbar['symbol'].value) # Prints the value within ('AAPL')
chart.topbar['symbol'].set('MSFT') # Sets the 'symbol' textbox to 'MSFT'
print(chart.topbar['symbol'].value) # Prints the value again ('MSFT')
```
Events can also be emitted from the topbar. For example:
```python
from lightweight_charts import Chart
def on_button_press(chart):
new_button_value = 'On' if chart.topbar['my_button'].value == 'Off' else 'Off'
chart.topbar['my_button'].set(new_button_value)
print(f'Turned something {new_button_value.lower()}.')
if __name__ == '__main__':
chart = Chart()
chart.topbar.button('my_button', 'Off', func=on_button_press)
chart.show(block=True)
```
___
### `switcher`
`name: str` | `options: tuple` | `default: str` | `func: callable`
* `name`: the name of the switcher which can be used to access it from the `topbar` dictionary.
* `options`: The options for each switcher item.
* `default`: The initial switcher option set.
___
### `textbox`
`name: str` | `initial_text: str`
* `name`: the name of the text box which can be used to access it from the `topbar` dictionary.
* `initial_text`: The text to show within the text box.
___
### `button`
`name: str` | `button_text: str` | `separator: bool` | `func: callable`
* `name`: the name of the text box to access it from the `topbar` dictionary.
* `button_text`: Text to show within the button.
* `separator`: places a separator line to the right of the button.
* `func`: The event handler which will be executed upon a button click.
___
## Callbacks Example:
```python
import pandas as pd
from lightweight_charts import Chart
def get_bar_data(symbol, timeframe):
if symbol not in ('AAPL', 'GOOGL', 'TSLA'):
print(f'No data for "{symbol}"')
return pd.DataFrame()
return pd.read_csv(f'../examples/6_callbacks/bar_data/{symbol}_{timeframe}.csv')
def on_search(chart, searched_string):
new_data = get_bar_data(searched_string, chart.topbar['timeframe'].value)
if new_data.empty:
return
chart.topbar['symbol'].set(searched_string)
chart.set(new_data)
def on_timeframe_selection(chart):
new_data = get_bar_data(chart.topbar['symbol'].value, chart.topbar['timeframe'].value)
if new_data.empty:
return
chart.set(new_data, True)
def on_horizontal_line_move(chart, line):
print(f'Horizontal line moved to: {line.price}')
if __name__ == '__main__':
chart = Chart(toolbox=True)
chart.legend(True)
chart.topbar.textbox('symbol', 'TSLA')
chart.topbar.switcher('timeframe', ('1min', '5min', '30min'), default='5min',
func=on_timeframe_selection)
df = get_bar_data('TSLA', '5min')
chart.set(df)
chart.horizontal_line(200, func=on_horizontal_line_move)
chart.show(block=True)
```

View File

@ -1,360 +0,0 @@
# Common Methods
The methods below can be used within all chart objects.
___
## `set`
`data: pd.DataFrame` `render_drawings: bool`
Sets the initial data for the chart. The data must be given as a DataFrame, with the columns:
`time | open | high | low | close | volume`
The `time` column can also be named `date` or be the index, and the `volume` column can be omitted if volume is not enabled. Column names are not case sensitive.
If `render_drawings` is `True`, any drawings made using the `toolbox` will be redrawn with the new data. This is designed to be used when switching to a different timeframe of the same symbol.
```{important}
the `time` column must have rows all of the same timezone and locale. This is particularly noticeable for data which crosses over daylight saving hours on data with intervals of less than 1 day. Errors are likely to be raised if they are not converted beforehand.
```
An empty `DataFrame` object or `None` can also be given to this method, which will erase all candle and volume data displayed on the chart.
___
## `update`
`series: pd.Series`
Updates the chart data from a `pd.Series` object. The bar should contain values with labels akin to `set`.
___
## `update_from_tick`
`series: pd.Series` | `cumulative_volume: bool`
Updates the chart from a tick. The series should use the labels:
`time | price | volume`
As before, the `time` can also be named `date`, and the `volume` can be omitted if volume is not enabled. The `time` column can also be the name of the Series object.
```{information}
The provided ticks do not need to be rounded to an interval (1 min, 5 min etc.), as the library handles this automatically.
```
If `cumulative_volume` is used, the volume data given will be added onto the latest bar of volume data.
___
## `create_line` (Line)
`name: str` | `color: str` | `style: LINE_STYLE`| `width: int` | `price_line: bool` | `price_label: bool` | `-> Line`
Creates and returns a `Line` object, representing a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to:
[`title`](#title), [`marker`](#marker), [`horizontal_line`](#horizontal-line) [`hide_data`](#hide-data), [`show_data`](#show-data) and[`price_line`](#price-line).
Its instance should only be accessed from this method.
### `set`
`data: pd.DataFrame`
Sets the data for the line.
When a name has not been set upon declaration, the columns should be named: `time | value` (Not case sensitive).
Otherwise, the method will use the column named after the string given in `name`. This name will also be used within the legend of the chart. For example:
```python
line = chart.create_line('SMA 50')
# DataFrame with columns: date | SMA 50
df = pd.read_csv('sma50.csv')
line.set(df)
```
### `update`
`series: pd.Series`
Updates the data for the line.
This should be given as a Series object, with labels akin to the `line.set()` function.
### `delete`
Irreversibly deletes the line.
___
## `lines`
`-> List[Line]`
Returns a list of all lines for the chart or subchart.
___
## `trend_line`
`start_time: str/datetime` | `start_value: float/int` | `end_time: str/datetime` | `end_value: float/int` | `color: str` | `width: int` | `-> Line`
Creates a trend line, drawn from the first point (`start_time`, `start_value`) to the last point (`end_time`, `end_value`).
___
## `ray_line`
`start_time: str/datetime` | `value: float/int` | `color: str` | `width: int` | `-> Line`
Creates a ray line, drawn from the first point (`start_time`, `value`) and onwards.
___
## `marker`
`time: datetime` | `position: 'above'/'below'/'inside'` | `shape: 'arrow_up'/'arrow_down'/'circle'/'square'` | `color: str` | `text: str` | `-> str`
Adds a marker to the chart, and returns its id.
If the `time` parameter is not given, the marker will be placed at the latest bar.
When using multiple markers, they should be placed in chronological order or display bugs may be present.
___
## `remove_marker`
`marker_id: str`
Removes the marker with the given id.
Usage:
```python
marker = chart.marker(text='hello_world')
chart.remove_marker(marker)
```
___
## `horizontal_line` (HorizontalLine)
`price: float/int` | `color: str` | `width: int` | `style: 'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'` | `text: str` | `axis_label_visible: bool` | `interactive: bool` | `-> HorizontalLine`
Places a horizontal line at the given price, and returns a `HorizontalLine` object, representing a `PriceLine` in Lightweight Charts.
If `interactive` is set to `True`, this horizontal line can be edited on the chart. Upon its movement a callback will also be emitted to an `on_horizontal_line_move` method, containing its ID and price. The toolbox should be enabled during its usage. It is designed to be used to update an order (limit, stop, etc.) directly on the chart.
### `update`
`price: float/int`
Updates the price of the horizontal line.
### `label`
`text: str`
Updates the label of the horizontal line.
### `delete`
Irreversibly deletes the horizontal line.
___
## `remove_horizontal_line`
`price: float/int`
Removes a horizontal line at the given price.
___
## `clear_markers`
Clears the markers displayed on the data.
___
## `clear_horizontal_lines`
Clears the horizontal lines displayed on the data.
___
## `precision`
`precision: int`
Sets the precision of the chart based on the given number of decimal places.
___
## `price_scale`
`mode: 'normal'/'logarithmic'/'percentage'/'index100'` | `align_labels: bool` | `border_visible: bool` | `border_color: str` | `text_color: str` | `entire_text_only: bool` | `ticks_visible: bool` | `scale_margin_top: float` | `scale_margin_bottom: float`
Price scale options for the chart.
___
## `time_scale`
`right_offset: int` | `min_bar_spacing: float` | `visible: bool` | `time_visible: bool` | `seconds_visible: bool` | `border_visible: bool` | `border_color: str`
Timescale options for the chart.
___
## `layout`
`background_color: str` | `text_color: str` | `font_size: int` | `font_family: str`
Global layout options for the chart.
___
## `grid`
`vert_enabled: bool` | `horz_enabled: bool` | `color: str` | `style: 'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'`
Grid options for the chart.
___
## `candle_style`
`up_color: str` | `down_color: str` | `wick_enabled: bool` | `border_enabled: bool` | `border_up_color: str` | `border_down_color: str` | `wick_up_color: str` | `wick_down_color: str`
Candle styling for each of the candle's parts (border, wick).
```{admonition} Color Formats
:class: note
Throughout the library, colors should be given as either:
* rgb: `rgb(100, 100, 100)`
* rgba: `rgba(100, 100, 100, 0.7)`
* hex: `#32a852`
```
___
## `volume_config`
`scale_margin_top: float` | `scale_margin_bottom: float` | `up_color: str` | `down_color: str`
Volume config options.
```{important}
The float values given to scale the margins must be greater than 0 and less than 1.
```
___
## `crosshair`
`mode` | `vert_visible: bool` | `vert_width: int` | `vert_color: str` | `vert_style: str` | `vert_label_background_color: str` | `horz_visible: bool` | `horz_width: int` | `horz_color: str` | `horz_style: str` | `horz_label_background_color: str`
Crosshair formatting for its vertical and horizontal axes.
`vert_style` and `horz_style` should be given as one of: `'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'`
___
## `watermark`
`text: str` | `font_size: int` | `color: str`
Overlays a watermark on top of the chart.
___
## `legend`
`visible: bool` | `ohlc: bool` | `percent: bool` | `lines: bool` | `color: str` | `font_size: int` | `font_family: str`
Configures the legend of the chart.
___
## `spinner`
`visible: bool`
Shows a loading spinner on the chart, which can be used to visualise the loading of large datasets, API calls, etc.
___
## `price_line`
`label_visible: bool` | `line_visible: bool` | `title: str`
Configures the visibility of the last value price line and its label.
___
## `fit`
Attempts to fit all data displayed on the chart within the viewport (`fitContent()`).
___
## `hide_data`
Hides the candles on the chart.
___
## `show_data`
Shows the hidden candles on the chart.
___
## `hotkey`
`modifier: 'ctrl'/'shift'/'alt'/'meta'` | `key: str/int/tuple` | `func: callable`
Adds a global hotkey to the chart window, which will execute the method or function given.
When using a number in `key`, it should be given as an integer. If multiple key commands are needed for the same function, you can pass a tuple to `key`. For example:
```python
def place_buy_order(key):
print(f'Buy {key} shares.')
def place_sell_order(key):
print(f'Sell all shares, because I pressed {key}.')
if __name__ == '__main__':
chart = Chart()
chart.hotkey('shift', (1, 2, 3), place_buy_order)
chart.hotkey('shift', 'X', place_sell_order)
chart.show(block=True)
```
___
## `create_table`
`width: int/float` | `height: int/float` | `headings: tuple[str]` | `widths: tuple[float]` | `alignments: tuple[str]` | `position: 'left'/'right'/'top'/'bottom'` | `draggable: bool` | `func: callable` | `-> Table`
Creates and returns a [`Table`](https://lightweight-charts-python.readthedocs.io/en/latest/tables.html) object.
___
## `create_subchart` (SubChart)
`position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/str` | `scale_candles_only: bool`|`toolbox: bool` | `-> SubChart`
Creates and returns a `SubChart` object, placing it adjacent to the previous `Chart` or `SubChart`. This allows for the use of multiple chart panels within the same `Chart` window. Its instance should only be accessed by using this method.
`position`: specifies how the Subchart will float.
`height` | `width`: Specifies the size of the Subchart, where `1` is the width/height of the window (100%)
`sync`: If given as `True`, the Subchart's timescale and crosshair will follow that of the declaring `Chart` or `SubChart`. If a `str` is passed, the `SubChart` will follow the panel with the given id. Chart ids can be accessed from the`chart.id` and `subchart.id` attributes.
```{important}
`width` and `height` should be given as a number between 0 and 1.
```
`SubCharts` are arranged horizontally from left to right. When the available space is no longer sufficient, the subsequent `SubChart` will be positioned on a new row, starting from the left side.
### Grid of 4 Example:
```python
import pandas as pd
from lightweight_charts import Chart
if __name__ == '__main__':
chart = Chart(inner_width=0.5, inner_height=0.5)
chart2 = chart.create_subchart(position='right', width=0.5, height=0.5)
chart3 = chart.create_subchart(position='left', width=0.5, height=0.5)
chart4 = chart.create_subchart(position='right', width=0.5, height=0.5)
chart.watermark('1')
chart2.watermark('2')
chart3.watermark('3')
chart4.watermark('4')
df = pd.read_csv('ohlcv.csv')
chart.set(df)
chart2.set(df)
chart3.set(df)
chart4.set(df)
chart.show(block=True)
```
### Synced Line Chart Example:
```python
import pandas as pd
from lightweight_charts import Chart
if __name__ == '__main__':
chart = Chart(inner_width=1, inner_height=0.8)
chart.time_scale(visible=False)
chart2 = chart.create_subchart(width=1, height=0.2, sync=True)
line = chart2.create_line()
df = pd.read_csv('ohlcv.csv')
df2 = pd.read_csv('rsi.csv')
chart.set(df)
line.set(df2)
chart.show(block=True)
```

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,74 @@
# Events
## Hotkey Example
```python
from lightweight_charts import Chart
def place_buy_order(key):
print(f'Buy {key} shares.')
def place_sell_order(key):
print(f'Sell all shares, because I pressed {key}.')
if __name__ == '__main__':
chart = Chart()
chart.hotkey('shift', (1, 2, 3), place_buy_order)
chart.hotkey('shift', 'X', place_sell_order)
chart.show(block=True)
```
___
## Topbar Example
```python
import pandas as pd
from lightweight_charts import Chart
def get_bar_data(symbol, timeframe):
if symbol not in ('AAPL', 'GOOGL', 'TSLA'):
print(f'No data for "{symbol}"')
return pd.DataFrame()
return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv')
def on_search(chart, searched_string):
new_data = get_bar_data(searched_string, chart.topbar['timeframe'].value)
if new_data.empty:
return
chart.topbar['symbol'].set(searched_string)
chart.set(new_data)
def on_timeframe_selection(chart):
new_data = get_bar_data(chart.topbar['symbol'].value, chart.topbar['timeframe'].value)
if new_data.empty:
return
chart.set(new_data, True)
def on_horizontal_line_move(chart, line):
print(f'Horizontal line moved to: {line.price}')
if __name__ == '__main__':
chart = Chart(toolbox=True)
chart.legend(True)
chart.events.search += on_search
chart.topbar.textbox('symbol', 'TSLA')
chart.topbar.switcher('timeframe', ('1min', '5min', '30min'), default='5min',
func=on_timeframe_selection)
df = get_bar_data('TSLA', '5min')
chart.set(df)
chart.horizontal_line(200, func=on_horizontal_line_move)
chart.show(block=True)
```

View File

@ -0,0 +1,98 @@
# Alternative GUI's
## PyQt5 / PySide6
```python
import pandas as pd
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from lightweight_charts.widgets import QtChart
app = QApplication([])
window = QMainWindow()
layout = QVBoxLayout()
widget = QWidget()
widget.setLayout(layout)
window.resize(800, 500)
layout.setContentsMargins(0, 0, 0, 0)
chart = QtChart(widget)
df = pd.read_csv('ohlcv.csv')
chart.set(df)
layout.addWidget(chart.get_webview())
window.setCentralWidget(widget)
window.show()
app.exec_()
```
___
## WxPython
```python
import wx
import pandas as pd
from lightweight_charts.widgets import WxChart
class MyFrame(wx.Frame):
def __init__(self):
super().__init__(None)
self.SetSize(1000, 500)
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
panel.SetSizer(sizer)
chart = WxChart(panel)
df = pd.read_csv('ohlcv.csv')
chart.set(df)
sizer.Add(chart.get_webview(), 1, wx.EXPAND | wx.ALL)
sizer.Layout()
self.Show()
if __name__ == '__main__':
app = wx.App()
frame = MyFrame()
app.MainLoop()
```
___
## Jupyter
```python
import pandas as pd
from lightweight_charts import JupyterChart
chart = JupyterChart()
df = pd.read_csv('ohlcv.csv')
chart.set(df)
chart.load()
```
___
## Streamlit
```python
import pandas as pd
from lightweight_charts.widgets import StreamlitChart
chart = StreamlitChart(width=900, height=600)
df = pd.read_csv('ohlcv.csv')
chart.set(df)
chart.load()
```

View File

@ -0,0 +1,50 @@
# Subcharts
## Grid of 4
```python
import pandas as pd
from lightweight_charts import Chart
if __name__ == '__main__':
chart = Chart(inner_width=0.5, inner_height=0.5)
chart2 = chart.create_subchart(position='right', width=0.5, height=0.5)
chart3 = chart.create_subchart(position='left', width=0.5, height=0.5)
chart4 = chart.create_subchart(position='right', width=0.5, height=0.5)
chart.watermark('1')
chart2.watermark('2')
chart3.watermark('3')
chart4.watermark('4')
df = pd.read_csv('ohlcv.csv')
chart.set(df)
chart2.set(df)
chart3.set(df)
chart4.set(df)
chart.show(block=True)
```
___
## Synced Line Chart
```python
import pandas as pd
from lightweight_charts import Chart
if __name__ == '__main__':
chart = Chart(inner_width=1, inner_height=0.8)
chart.time_scale(visible=False)
chart2 = chart.create_subchart(width=1, height=0.2, sync=True)
line = chart2.create_line()
df = pd.read_csv('ohlcv.csv')
df2 = pd.read_csv('rsi.csv')
chart.set(df)
line.set(df2)
chart.show(block=True)
```

View File

@ -0,0 +1,38 @@
# Table
```python
import pandas as pd
from lightweight_charts import Chart
def on_row_click(row):
row['PL'] = round(row['PL']+1, 2)
row.background_color('PL', 'green' if row['PL'] > 0 else 'red')
table.footer[1] = row['Ticker']
if __name__ == '__main__':
chart = Chart(width=1000, inner_width=0.7, inner_height=1)
subchart = chart.create_subchart(width=0.3, height=0.5)
df = pd.read_csv('ohlcv.csv')
chart.set(df)
subchart.set(df)
table = chart.create_table(width=0.3, height=0.2,
headings=('Ticker', 'Quantity', 'Status', '%', 'PL'),
widths=(0.2, 0.1, 0.2, 0.2, 0.3),
alignments=('center', 'center', 'right', 'right', 'right'),
position='left', func=on_row_click)
table.format('PL', f{table.VALUE}')
table.format('%', f'{table.VALUE} %')
table.new_row('SPY', 3, 'Submitted', 0, 0)
table.new_row('AMD', 1, 'Filled', 25.5, 105.24)
table.new_row('NVDA', 2, 'Filled', -0.5, -8.24)
table.footer(2)
table.footer[0] = 'Selected:'
chart.show(block=True)
```

View File

@ -0,0 +1,73 @@
# Toolbox with persistent drawings
To get started, create a file called `drawings.json`, which should contain:
```
{}
```
___
```python
import pandas as pd
from lightweight_charts import Chart
def get_bar_data(symbol, timeframe):
if symbol not in ('AAPL', 'GOOGL', 'TSLA'):
print(f'No data for "{symbol}"')
return pd.DataFrame()
return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv')
def on_search(chart, searched_string):
new_data = get_bar_data(searched_string, chart.topbar['timeframe'].value)
if new_data.empty:
return
chart.topbar['symbol'].set(searched_string)
chart.set(new_data)
# Load the drawings saved under the symbol.
chart.toolbox.load_drawings(searched_string)
def on_timeframe_selection(chart):
new_data = get_bar_data(chart.topbar['symbol'].value, chart.topbar['timeframe'].value)
if new_data.empty:
return
# The symbol has not changed, so we want to re-render the drawings.
chart.set(new_data, render_drawings=True)
if __name__ == '__main__':
chart = Chart(toolbox=True)
chart.legend(True)
chart.events.search += on_search
chart.topbar.textbox('symbol', 'TSLA')
chart.topbar.switcher(
'timeframe',
('1min', '5min', '30min'),
default='5min',
func=on_timeframe_selection
)
df = get_bar_data('TSLA', '5min')
chart.set(df)
# Imports the drawings saved in the JSON file.
chart.toolbox.import_drawings('drawings.json')
# Loads the drawings under the default symbol.
chart.toolbox.load_drawings(chart.topbar['symbol'].value)
# Saves drawings based on the symbol.
chart.toolbox.save_drawings_under(chart.topbar['symbol'])
chart.show(block=True)
# Exports the drawings to the JSON file upon close.
chart.toolbox.export_drawings('drawings.json')
```

View File

@ -0,0 +1,48 @@
# YFinance
```python
import datetime as dt
import yfinance as yf
from lightweight_charts import Chart
def get_bar_data(symbol, timeframe):
if timeframe in ('1m', '5m', '30m'):
days = 7 if timeframe == '1m' else 60
start_date = dt.datetime.now()-dt.timedelta(days=days)
else:
start_date = None
chart.spinner(True)
data = yf.download(symbol, start_date, interval=timeframe)
chart.spinner(False)
if data.empty:
return False
chart.set(data)
return True
def on_search(chart, searched_string):
if get_bar_data(searched_string, chart.topbar['timeframe'].value):
chart.topbar['symbol'].set(searched_string)
def on_timeframe_selection(chart):
get_bar_data(chart.topbar['symbol'].value, chart.topbar['timeframe'].value)
if __name__ == '__main__':
chart = Chart(toolbox=True, debug=True)
chart.legend(True)
chart.events.search += on_search
chart.topbar.textbox('symbol', 'n/a')
chart.topbar.switcher(
'timeframe',
('1m', '5m', '30m', '1d', '1wk'),
default='5m',
func=on_timeframe_selection
)
chart.show(block=True)
```

View File

@ -1,15 +1,35 @@
```{toctree}
:hidden:
:caption: TUTORIALS
tutorials/getting_started
tutorials/events
```
common_methods
charts
callbacks
toolbox
tables
```{toctree}
:hidden:
:caption: EXAMPLES
examples/table
examples/toolbox
examples/subchart
examples/yfinance
examples/events
examples/gui_examples
```
```{toctree}
:hidden:
:caption: DOCS
reference/index
polygon
Github Repository <https://github.com/louisnw01/lightweight-charts-python>
```
```{include} ../../README.md
```
```{include} ../../README.md
```

View File

@ -1,14 +1,11 @@
# Polygon.io
[Polygon.io's](https://polygon.io/?utm_source=affiliate&utm_campaign=pythonlwcharts) market data API is directly integrated within lightweight-charts-python, and is easy to use within the library.
___
## Requirements
To use data from Polygon, there are certain libraries (not listed as requirements) that must be installed:
* Static data requires the `requests` library.
* Live data requires the `websockets` library.
___
## `polygon`
`polygon` is a [Common Method](https://lightweight-charts-python.readthedocs.io/en/latest/common_methods.html), and can be accessed from within any chart type.
````{py:class} PolygonAPI
This class should be accessed from the `polygon` attribute, which is contained within all chart types.
`chart.polygon.<method>`
@ -21,14 +18,15 @@ The `stock`, `option`, `index`, `forex`, and `crypto` methods of `chart.polygon`
* `live`: When set to `True`, a websocket connection will be used to update the chart or subchart in real-time.
* These methods will also return a boolean representing whether the request was successful.
The `websockets` library is required when using live data.
```{important}
When using live data and the standard `show` method, the `block` parameter __must__ be set to `True` in order for the data to congregate on the chart (`chart.show(block=True)`).
If `show_async` is used with live data, `block` can be either value.
```
___
### Example:
For Example:
```python
from lightweight_charts import Chart
@ -43,67 +41,90 @@ if __name__ == '__main__':
)
chart.show(block=True)
```
___
### `api_key`
`key: str`
```{py:method} api_key(key: str)
Sets the API key for the chart. Subsequent `SubChart` objects will inherit the API key given to the parent chart.
```
___
### `stock`
`symbol: str` | `timeframe: str` | `start_date: str` | `end_date: str` | `limit: int` | `live: bool` | `-> bool`
```{py:method} stock(symbol: str, timeframe: str, start_date: str, end_date: str, limit: int, live: bool) -> bool
Requests and displays stock data pulled from Polygon.io.
`async_stock` can also be used.
```
___
### `option`
`symbol: str` | `timeframe: str` | `start_date: str` | `expiration` | `right: 'C' | 'P'` | `strike: int | float` | `end_date: str` | `limit: int` | `live: bool` | `-> bool`
```{py:method} option(symbol: str, timeframe: str, expiration: str, right: 'C' | 'P', strike: NUM, end_date: str, limit: int, live: bool) -> bool
Requests and displays option data pulled from Polygon.io.
A formatted option ticker (SPY251219C00650000) can also be given to the `symbol` parameter, allowing for `expiration`, `right`, and `strike` to be left blank.
`async_option` can also be used.
```
___
### `index`
`symbol: str` | `timeframe: str` | `start_date: str` | `end_date: str` | `limit: int` | `live: bool` | `-> bool`
```{py:method} index(symbol: str, timeframe: str, start_date: str, end_date: str, limit: int, live: bool) -> bool
Requests and displays index data pulled from Polygon.io.
`async_index` can also be used.
```
___
### `forex`
`fiat_pair: str` | `timeframe: str` | `start_date: str` | `end_date: str` | `limit: int` | `live: bool` | `-> bool`
```{py:method} forex(fiat_pair: str, timeframe: str, start_date: str, end_date: str, limit: int, live: bool) -> bool
Requests and displays a forex pair pulled from Polygon.io.
The two currencies should be separated by a '-' (`USD-CAD`, `GBP-JPY`, etc.).
`async_forex` can also be used.
```
___
### `crypto`
`crypto_pair: str` | `timeframe: str` | `start_date: str` | `end_date: str` | `limit: int` | `live: bool` | `-> bool`
```{py:method} crypto(crypto_pair: str, timeframe: str, start_date: str, end_date: str, limit: int, live: bool) -> bool
Requests and displays a crypto pair pulled from Polygon.io.
The two currencies should be separated by a '-' (`BTC-USD`, `ETH-BTC`, etc.).
`async_crypto` can also be used.
```
___
### `log`
`info: bool`
```{py:method} log(info: bool)
If `True`, informational log messages (connection, subscriptions etc.) will be displayed in the console.
Data errors will always be shown in the console.
```
````
___
## PolygonChart
`api_key: str` | `live: bool` | `num_bars: int`
````{py:class} PolygonChart(api_key: str, num_bars: int, limit: int, end_date: str, timeframe_options: tuple, security_options: tuple, live: bool)
The `PolygonChart` provides an easy and complete way to use the Polygon.io API within lightweight-charts-python.
This object requires the `requests` library for static data, and the `websockets` library for live data.
This object requires the `websockets` library for live data.
All data is requested within the chart window through searching and selectors.
@ -118,18 +139,45 @@ As well as the parameters from the [Chart](https://lightweight-charts-python.rea
* `live`: If True, the chart will update in real-time.
___
### Example:
For Example:
```python
from lightweight_charts import PolygonChart
if __name__ == '__main__':
chart = PolygonChart(api_key='<API-KEY>',
num_bars=200,
limit=5000,
live=True)
chart = PolygonChart(
api_key='<API-KEY>',
num_bars=200,
limit=5000,
live=True
)
chart.show(block=True)
```
![PolygonChart png](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/docs/source/polygonchart.png)
````
```{py:function} polygon.get_bar_data(ticker: str, timeframe: str, start_date: str, end_date: str, limit: int = 5_000) -> pd.DataFrame
Module function which returns a formatted Dataframe of the requested aggregate data.
`ticker` should be prefixed for the appropriate security type (eg. `I:NDX`)
```
```{py:function} polygon.subscribe(ticker: str, sec_type: SEC_TYPE, func: callable, args: tuple, precision=2)
:async:
Subscribes the given callable to live data from polygon. emitting a dictionary (and any given arguments) to the function.
```
```{py:function} polygon.unsubscribe(func: callable)
:async:
Unsubscribes the given function from live data.
The ticker will only be unsubscribed if there are no additional functions that are currently subscribed.
```

View File

@ -0,0 +1,329 @@
# `AbstractChart`
`````{py:class} AbstractChart(width, height)
Abstracted chart used to create child classes.
___
```{py:method} set(data: pd.DataFrame, render_drawings: bool = False)
Sets the initial data for the chart.
Columns should be named:
: `time | open | high | low | close | volume`
Time can be given in the index rather than a column, and volume can be omitted if volume is not used. Column names are not case sensitive.
If `render_drawings` is `True`, any drawings made using the `toolbox` will be redrawn with the new data. This is designed to be used when switching to a different timeframe of the same symbol.
`None` can also be given, which will erase all candle and volume data displayed on the chart.
```
___
```{py:method} update(series: pd.Series)
Updates the chart data from a bar.
Series labels should be akin to [`set`](#AbstractChart.set).
```
___
```{py:method} update_from_tick(series: pd.Series, cumulative_volume: bool = False)
Updates the chart from a tick.
Labels should be named:
: `time | price | volume`
As before, the `time` can also be named `date`, and the `volume` can be omitted if volume is not enabled. The `time` column can also be the name of the Series object.
The provided ticks do not need to be rounded to an interval (1 min, 5 min etc.), as the library handles this automatically.
If `cumulative_volume` is used, the volume data given will be added onto the latest bar of volume data.
```
___
```{py:method} create_line(name: str, color: COLOR, style: LINE_STYLE, width: int, price_line: bool, price_label: bool) -> Line
Creates and returns a Line object, representing a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to:
[`marker`](#marker), [`horizontal_line`](#AbstractChart.horizontal_line) [`hide_data`](#hide_data), [`show_data`](#show_data) and[`price_line`](#price_line).
Its instance should only be accessed from this method.
```
___
```{py:function} lines() -> List[Line]
Returns a list of all lines for the chart.
```
___
```{py:function} trend_line(start_time: str | datetime, start_value: NUM, end_time: str | datetime, end_value: NUM, color: COLOR, width: int) -> Line
Creates a trend line, drawn from the first point (`start_time`, `start_value`) to the last point (`end_time`, `end_value`).
```
___
```{py:function} ray_line(start_time: str | datetime, value: NUM, color: COLOR, width: int) -> Line
Creates a ray line, drawn from the first point (`start_time`, `value`) and onwards.
```
___
```{py:function} marker(time: datetime, position: MARKER_POSITION, shape: MARKER_SHAPE, color: COLOR, text: str) -> str
Adds a marker to the chart, and returns its id.
If the `time` parameter is not given, the marker will be placed at the latest bar.
When using multiple markers, they should be placed in chronological order or display bugs may be present.
```
___
```{py:function} remove_marker(marker_id: str)
Removes the marker with the given id.
```
___
```{py:function} horizontal_line(price: NUM, color: COLOR, width: int, style: LINE_STYLE, text: str, axis_label_visible: bool, func: callable= None) -> HorizontalLine
Places a horizontal line at the given price, and returns a [`HorizontalLine`] object.
If a `func` is given, the horizontal line can be edited on the chart. Upon its movement a callback will also be emitted to the callable given, containing the HorizontalLine object. The toolbox should be enabled during its usage. It is designed to be used to update an order (limit, stop, etc.) directly on the chart.
```
___
```{py:function} remove_horizontal_line(price: NUM)
Removes a horizontal line at the given price.
```
___
```{py:function} clear_markers()
Clears the markers displayed on the data.
```
___
```{py:function} clear_horizontal_lines
Clears the horizontal lines displayed on the data.
```
___
```{py:function} precision(precision: int)
Sets the precision of the chart based on the given number of decimal places.
```
___
```{py:function} price_scale(mode: PRICE_SCALE_MODE, align_labels: bool, border_visible: bool, border_color: COLOR, text_color: COLOR, entire_text_only: bool, ticks_visible: bool, scale_margin_top: float, scale_margin_bottom: float)
Price scale options for the chart.
```
___
```{py:function} time_scale(right_offset: int, min_bar_spacing: float, visible: bool, time_visible: bool, seconds_visible: bool, border_visible: bool, border_color: COLOR)
Timescale options for the chart.
```
___
```{py:function} layout(background_color: COLOR, text_color: COLOR, font_size: int, font_family: str)
Global layout options for the chart.
```
___
```{py:function} grid(vert_enabled: bool, horz_enabled: bool, color: COLOR, style: LINE_STYLE)
Grid options for the chart.
```
___
```{py:function} candle_style(up_color: COLOR, down_color: COLOR, wick_enabled: bool, border_enabled: bool, border_up_color: COLOR, border_down_color: COLOR, wick_up_color: COLOR, wick_down_color: COLOR)
Candle styling for each of the candle's parts (border, wick).
```
___
```{py:function} volume_config(scale_margin_top: float, scale_margin_bottom: float, up_color: COLOR, down_color: COLOR)
Volume config options.
```{important}
The float values given to scale the margins must be greater than 0 and less than 1.
```
___
```{py:function} crosshair(mode, vert_visible: bool, vert_width: int, vert_color: COLOR, vert_style: LINE_STYLE, vert_label_background_color: COLOR, horz_visible: bool, horz_width: int, horz_color: COLOR, horz_style: LINE_STYLE, horz_label_background_color: COLOR)
Crosshair formatting for its vertical and horizontal axes.
```
___
```{py:function} watermark(text: str, font_size: int, color: COLOR)
Overlays a watermark on top of the chart.
```
___
```{py:function} legend(visible: bool, ohlc: bool, percent: bool, lines: bool, color: COLOR, font_size: int, font_family: str)
Configures the legend of the chart.
```
___
```{py:function} spinner(visible: bool)
Shows a loading spinner on the chart, which can be used to visualise the loading of large datasets, API calls, etc.
```{important}
This method must be used in conjunction with the search event.
```
___
```{py:function} price_line(label_visible: bool, line_visible: bool, title: str)
Configures the visibility of the last value price line and its label.
```
___
```{py:function} fit()
Attempts to fit all data displayed on the chart within the viewport (`fitContent()`).
```
___
```{py:function} show_data()
Shows the hidden candles on the chart.
```
___
```{py:function} hide_data()
Hides the candles on the chart.
```
___
```{py:function} hotkey(modifier: 'ctrl' | 'alt' | 'shift' | 'meta', key: 'str' | 'int' | 'tuple', func: callable)
Adds a global hotkey to the chart window, which will execute the method or function given.
When using a number in `key`, it should be given as an integer. If multiple key commands are needed for the same function, a tuple can be passed to `key`.
```
___
```{py:function} create_table(width: NUM, height: NUM, headings: Tuple[str], widths: Tuple[float], alignments: Tuple[str], position: FLOAT, draggable: bool, func: callable) -> Table
Creates and returns a [`Table`](https://lightweight-charts-python.readthedocs.io/en/latest/tables.html) object.
```
___
````{py:function} create_subchart(position: FLOAT, width: float, height: float, sync: bool | str, scale_candles_only: bool, toolbox: bool) -> AbstractChart
Creates and returns a Chart object, placing it adjacent to the previous Chart. This allows for the use of multiple chart panels within the same window.
`position`
: specifies how the Subchart will float.
`height` | `width`
: Specifies the size of the Subchart, where `1` is the width/height of the window (100%)
`sync`
: If given as `True`, the Subchart's timescale and crosshair will follow that of the declaring Chart. If a `str` is passed, the Chart will follow the panel with the given id. Chart ids can be accessed from the `chart.id` attribute.
```{important}
`width` and `height` should be given as a number between 0 and 1.
```
Charts are arranged horizontally from left to right. When the available space is no longer sufficient, the subsequent Chart will be positioned on a new row, starting from the left side.
````
`````

View File

@ -1,11 +1,12 @@
# Charts
This page contains a reference to all chart objects that can be used within the library. They all have access to the common methods.
This page contains a reference to all chart objects that can be used within the library.
They inherit from [AbstractChart](#AbstractChart).
___
## Chart
`width: int` | `height: int` | `x: int` | `y: int` | `on_top: bool` | `maximize: bool` | `debug: bool` | `toolbox: bool`
`````{py:class} Chart(width: int, height: int, x: int, y: int, on_top: bool, maximize: bool, debug: bool, toolbox: bool, inner_width: float, inner_height: float, scale_candles_only: bool)
The main object used for the normal functionality of lightweight-charts-python, built on the pywebview library.
@ -14,31 +15,45 @@ The `Chart` object should be defined within an `if __name__ == '__main__'` block
```
___
### `show`
`block: bool`
```{py:method} show(block: bool)
Shows the chart window, blocking until the chart has loaded. If `block` is enabled, the method will block code execution until the window is closed.
```
___
### `hide`
```{py:method} hide()
Hides the chart window, which can be later shown by calling `chart.show()`.
```
___
### `exit`
```{py:method} exit()
Exits and destroys the chart window.
```
___
### `show_async`
`block: bool`
```{py:method} show_async(block: bool)
:async:
Show the chart asynchronously.
```
___
### `screenshot`
`-> bytes`
````{py:method} screenshot(block: bool) -> bytes
Takes a screenshot of the chart, and returns a bytes object containing the image. For example:
@ -57,10 +72,14 @@ if __name__ == '__main__':
```{important}
This method should be called after the chart window has loaded.
```
````
`````
___
## QtChart
`widget: QWidget`
````{py:class} QtChart(widget: QWidget)
The `QtChart` object allows the use of charts within a `QMainWindow` object, and has similar functionality to the `Chart` object for manipulating data, configuring and styling.
@ -69,141 +88,68 @@ Either the `PyQt5` or `PySide6` libraries will work with this chart.
Callbacks can be received through the Qt event loop.
___
### `get_webview`
`-> QWebEngineView`
```{py:method} get_webview() -> QWebEngineView
Returns the `QWebEngineView` object.
___
### Example:
```python
import pandas as pd
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from lightweight_charts.widgets import QtChart
app = QApplication([])
window = QMainWindow()
layout = QVBoxLayout()
widget = QWidget()
widget.setLayout(layout)
window.resize(800, 500)
layout.setContentsMargins(0, 0, 0, 0)
chart = QtChart(widget)
df = pd.read_csv('ohlcv.csv')
chart.set(df)
layout.addWidget(chart.get_webview())
window.setCentralWidget(widget)
window.show()
app.exec_()
```
````
___
## WxChart
`parent: wx.Panel`
````{py:class} WxChart(parent: WxPanel)
The WxChart object allows the use of charts within a `wx.Frame` object, and has similar functionality to the `Chart` object for manipulating data, configuring and styling.
Callbacks can be received through the Wx event loop.
___
### `get_webview`
`-> wx.html2.WebView`
```{py:method} get_webview() -> wx.html2.WebView
Returns a `wx.html2.WebView` object which can be used to for positioning and styling within wxPython.
___
### Example:
```python
import wx
import pandas as pd
from lightweight_charts.widgets import WxChart
class MyFrame(wx.Frame):
def __init__(self):
super().__init__(None)
self.SetSize(1000, 500)
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
panel.SetSizer(sizer)
chart = WxChart(panel)
df = pd.read_csv('ohlcv.csv')
chart.set(df)
sizer.Add(chart.get_webview(), 1, wx.EXPAND | wx.ALL)
sizer.Layout()
self.Show()
if __name__ == '__main__':
app = wx.App()
frame = MyFrame()
app.MainLoop()
```
````
___
## StreamlitChart
````{py:class} StreamlitChart
The `StreamlitChart` object allows the use of charts within a Streamlit app, and has similar functionality to the `Chart` object for manipulating data, configuring and styling.
This object only supports the displaying of **static** data, and should not be used with the `update_from_tick` or `update` methods. Every call to the chart object must occur **before** calling `load`.
___
### `load`
Loads the chart into the Streamlit app. This should be called after setting, styling, and configuring the chart, as no further calls to the `StreamlitChart` will be acknowledged.
___
### Example:
```python
import pandas as pd
from lightweight_charts.widgets import StreamlitChart
```{py:method} load()
chart = StreamlitChart(width=900, height=600)
Loads the chart into the Streamlit app. This should be called after setting, styling, and configuring the chart, as no further calls to the `StreamlitChart` will be acknowledged.
df = pd.read_csv('ohlcv.csv')
chart.set(df)
chart.load()
```
````
___
## JupyterChart
````{py:class} JupyterChart
The `JupyterChart` object allows the use of charts within a notebook, and has similar functionality to the `Chart` object for manipulating data, configuring and styling.
This object only supports the displaying of **static** data, and should not be used with the `update_from_tick` or `update` methods. Every call to the chart object must occur **before** calling `load`.
___
### `load`
```{py:method} load()
Renders the chart. This should be called after setting, styling, and configuring the chart, as no further calls to the `JupyterChart` will be acknowledged.
___
### Example:
```python
import pandas as pd
from lightweight_charts import JupyterChart
chart = JupyterChart()
df = pd.read_csv('ohlcv.csv')
chart.set(df)
chart.load()
```
````

View File

@ -0,0 +1,28 @@
# `Events`
````{py:class} AbstractChart.Events
The chart events class, accessed through `chart.events`
Events allow asynchronous and synchronous callbacks to be passed back into python.
Chart events can be subscribed to using: `chart.events.<name> += <callable>`
```{py:method} search -> (chart: Chart, string: str)
Fires upon searching. Searchbox will be automatically created.
```
```{py:method} new_bar -> (chart: Chart)
Fires when a new candlestick is added to the chart.
```
```{py:method} range_change -> (chart: Chart, bars_before: NUM, bars_after: NUM)
Fires when the range (visibleLogicalRange) changes.
```
````
Tutorial: [Topbar & Events](../tutorials/events.md)

View File

@ -0,0 +1,28 @@
# `HorizontalLine`
````{py:class} HorizontalLine(price: NUM, color: COLOR, width: int, style: LINE_STYLE, text: str, axis_label_visible: bool, func: callable= None)
The `HorizontalLine` object represents a `PriceLine` in Lightweight Charts.
Its instance should be accessed from the `horizontal_line` method.
```{py:method} update(price: NUM)
Updates the price of the horizontal line.
```
```{py:method} label(text: str)
Updates the label of the horizontal line.
```
```{py:method} delete()
Irreversibly deletes the horizontal line.
```
````

View File

@ -0,0 +1,23 @@
# Reference
```{toctree}
:hidden:
abstract_chart
line
horizontal_line
charts
events
topbar
toolbox
tables
```
1. [`AbstractChart`](#AbstractChart)
2. [`Line`](#Line)
3. [`HorizontalLine`](#HorizontalLine)
4. [Charts](#charts)
5. [`Events`](./events.md)
6. [`Toolbox`](#ToolBox)
7. [`Table`](#Table)

View File

@ -0,0 +1,44 @@
# `Line`
````{py:class} Line(name: str, color: COLOR, style: LINE_STYLE, width: int, price_line: bool, price_label: bool)
The `Line` object represents a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to:
[`marker`](#marker), [`horizontal_line`](#AbstractChart.horizontal_line) [`hide_data`](#hide_data), [`show_data`](#show_data) and [`price_line`](#price_line).
Its instance should only be accessed from [create_line](#AbstractChart.create_line).
___
```{py:function} set(data: pd.DataFrame)
Sets the data for the line.
When a name has not been set upon declaration, the columns should be named: `time | value` (Not case sensitive).
Otherwise, the method will use the column named after the string given in `name`. This name will also be used within the legend of the chart.
```
___
```{py:function} update(series: pd.Series)
Updates the data for the line.
This should be given as a Series object, with labels akin to the `line.set()` function.
```
___
```{py:function} line.delete()
Irreversibly deletes the line.
```
````

View File

@ -0,0 +1,125 @@
# `Table`
`````{py:class} Table(width: NUM, height: NUM, headings: Tuple[str], widths: Tuple[float], alignments: Tuple[str], position: FLOAT, draggable: bool, func: callable)
Tables are panes that can be used to gain further functionality from charts. They are intended to be used for watchlists, order management, or position management. It should be accessed from the `create_table` common method.
The `Table` and `Row` objects act as dictionaries, and can be manipulated as such.
`width`/`height`
: Either given as a percentage (a `float` between 0 and 1) or as an integer representing pixel size.
`widths`
: Given as a `float` between 0 and 1.
`position`
: Used as you would with [`create_subchart`](#AbstractChart.create_subchart), representing how the table will float within the window.
`draggable`
: If `True`, then the window can be dragged to any position within the window.
`func`
: If given, this will be called when a row is clicked, returning the `Row` object in question.
___
````{py:method} new_row(*values, id: int) -> Row
Creates a new row within the table, and returns a `Row` object.
if `id` is passed it should be unique to all other rows. Otherwise, the `id` will be randomly generated.
Rows can be passed a string (header) item or a tuple to set multiple headings:
```python
row['Symbol'] = 'AAPL'
row['Symbol', 'Action'] = 'AAPL', 'BUY'
```
````
___
```{py:method} clear()
Clears and deletes all table rows.
```
___
````{py:method} format(column: str, format_str: str)
Sets the format to be used for the given column. `Table.VALUE` should be used as a placeholder for the cell value. For example:
```python
table.format('Daily %', f'{table.VALUE} %')
table.format('PL', f'$ {table.VALUE}')
```
````
___
```{py:method} visible(visible: bool)
Sets the visibility of the Table.
```
`````
___
````{py:class} Row()
```{py:method} background_color(column: str, color: COLOR)
Sets the background color of the row cell.
```
___
```{py:method} text_color(column: str, color: COLOR)
Sets the foreground color of the row cell.
```
___
```{py:method} delete()
Deletes the row.
```
````
___
````{py:class} Footer
Tables can also have a footer containing a number of text boxes. To initialize this, call the `footer` attribute with the number of textboxes to be used:
```python
table.footer(3) # Footer will be displayed, with 3 text boxes.
```
To edit the textboxes, treat `footer` as a list:
```python
table.footer[0] = 'Text Box 1'
table.footer[1] = 'Text Box 2'
table.footer[2] = 'Text Box 3'
```
````

View File

@ -0,0 +1,62 @@
# `ToolBox`
`````{py:class} ToolBox
The Toolbox allows for trendlines, ray lines and horizontal lines to be drawn and edited directly on the chart.
It can be used within any Chart object, and is enabled by setting the `toolbox` parameter to `True` upon Chart declaration.
The following hotkeys can also be used when the Toolbox is enabled:
| Key Cmd | Action |
|--- |--- |
| `alt T` | Trendline |
| `alt H` | Horizontal Line |
| `alt R` | Ray Line |
| `⌘ Z` or `ctrl Z` | Undo |
Right-clicking on a drawing will open a context menu, allowing for color selection and deletion.
___
````{py:method} save_drawings_under(widget: Widget)
Saves drawings under a specific `topbar` text widget. For example:
```python
chart.toolbox.save_drawings_under(chart.topbar['symbol'])
```
````
___
```{py:method} load_drawings(tag: str)
Loads and displays drawings stored under the tag given.
```
___
```{py:method} import_drawings(file_path: str)
Imports the drawings stored at the JSON file given in `file_path`.
```
___
```{py:method} export_drawings(file_path: str)
Exports all currently saved drawings to the JSON file given in `file_path`.
```
`````

View File

@ -0,0 +1,54 @@
# `TopBar`
````{py:class} TopBar
The `TopBar` class represents the top bar shown on the chart:
![topbar](https://i.imgur.com/Qu2FW9Y.png)
This object is accessed from the `topbar` attribute of the chart object (`chart.topbar.<method>`).
Switchers, text boxes and buttons can be added to the top bar, and their instances can be accessed through the `topbar` dictionary. For example:
```python
chart.topbar.textbox('symbol', 'AAPL') # Declares a textbox displaying 'AAPL'.
print(chart.topbar['symbol'].value) # Prints the value within ('AAPL')
chart.topbar['symbol'].set('MSFT') # Sets the 'symbol' textbox to 'MSFT'
print(chart.topbar['symbol'].value) # Prints the value again ('MSFT')
```
___
```{py:method} switcher(name: str, options: tuple: default: str, func: callable)
* `name`: the name of the switcher which can be used to access it from the `topbar` dictionary.
* `options`: The options for each switcher item.
* `default`: The initial switcher option set.
```
___
```{py:method} textbox(name: str, initial_text: str)
* `name`: the name of the text box which can be used to access it from the `topbar` dictionary.
* `initial_text`: The text to show within the text box.
```
___
```{py:method} button(name: str, button_text: str, separator: bool, func: callable)
* `name`: the name of the text box to access it from the `topbar` dictionary.
* `button_text`: Text to show within the button.
* `separator`: places a separator line to the right of the button.
* `func`: The event handler which will be executed upon a button click.
```
````

View File

@ -0,0 +1,42 @@
:orphan:
# `Typing`
These classes serve as placeholders for type requirements.
```{py:class} NUM(Literal[float, int])
```
```{py:class} FLOAT(Literal['left', 'right', 'top', 'bottom'])
```
```{py:class} TIME(Union[datetime, pd.Timestamp, str])
```
```{py:class} COLOR(str)
Throughout the library, colors should be given as either rgb (`rgb(100, 100, 100)`), rgba(`rgba(100, 100, 100, 0.7)`), hex(`#32a852`) or a html literal(`blue`, `red` etc).
```
```{py:class} LINE_STYLE(Literal['solid', 'dotted', 'dashed', 'large_dashed', 'sparse_dotted'])
```
```{py:class} MARKER_POSITION(Literal['above', 'below', 'inside'])
```
```{py:class} MARKER_SHAPE(Literal['arrow_up', 'arrow_down', 'circle', 'square'])
```
```{py:class} CROSSHAIR_MODE(Literal['normal', 'magnet'])
```
```{py:class} PRICE_SCALE_MODE(Literal['normal', 'logarithmic', 'percentage', 'index100'])
```

View File

@ -1,121 +0,0 @@
# Table
`width: int/float` | `height: int/float` | `headings: tuple[str]` | `widths: tuple[float]` | `alignments: tuple[str]` | `position: 'left'/'right'/'top'/'bottom'` | `draggable: bool` | `func: callable`
Tables are panes that can be used to gain further functionality from charts. They are intended to be used for watchlists, order management, or position management. It should be accessed from the `create_table` common method.
The `Table` and `Row` objects act as dictionaries, and can be manipulated as such.
`width`/`height`: Either given as a percentage (a `float` between 0 and 1) or as an integer representing pixel size.
`widths`: Given as a `float` between 0 and 1.
`position`: Used as you would when creating a `SubChart`, representing how the table will float within the window.
`draggable`: If `True`, then the window can be dragged to any position within the window.
`func`: If given this will be called when a row is clicked, returning the `Row` object in question.
___
## `new_row` (Row)
`*values` | `id: int` | `-> Row`
Creates a new row within the table, and returns a `Row` object.
if `id` is passed it should be unique to all other rows. Otherwise, the `id` will be randomly generated.
Rows can be passed a string (header) item or a tuple to set multiple headings:
```python
row['Symbol'] = 'AAPL'
row['Symbol', 'Action'] = 'AAPL', 'BUY'
```
### `background_color`
`column: str` | `color: str`
Sets the background color of the row cell.
### `text_color`
`column: str` | `color: str`
Sets the foreground color of the row cell.
### `delete`
Deletes the row.
___
## `clear`
Clears and deletes all table rows.
___
## `format`
`column: str` | `format_str: str`
Sets the format to be used for the given column. `table.VALUE` should be used as a placeholder for the cell value. For example:
```python
table.format('Daily %', f'{table.VALUE} %')
table.format('PL', f'$ {table.VALUE}')
```
___
## `visible`
`visible: bool`
Sets the visibility of the Table.
___
## Footer
Tables can also have a footer containing a number of text boxes. To initialize this, call the `footer` attribute with the number of textboxes to be used:
```python
table.footer(3) # Footer will be displayed, with 3 text boxes.
```
To edit the textboxes, treat `footer` as a list:
```python
table.footer[0] = 'Text Box 1'
table.footer[1] = 'Text Box 2'
table.footer[2] = 'Text Box 3'
```
___
## Example:
```python
import pandas as pd
from lightweight_charts import Chart
def on_row_click(row):
row['PL'] = round(row['PL']+1, 2)
row.background_color('PL', 'green' if row['PL'] > 0 else 'red')
table.footer[1] = row['Ticker']
if __name__ == '__main__':
chart = Chart(width=1000, inner_width=0.7, inner_height=1)
subchart = chart.create_subchart(width=0.3, height=0.5)
df = pd.read_csv('ohlcv.csv')
chart.set(df)
subchart.set(df)
table = chart.create_table(width=0.3, height=0.2,
headings=('Ticker', 'Quantity', 'Status', '%', 'PL'),
widths=(0.2, 0.1, 0.2, 0.2, 0.3),
alignments=('center', 'center', 'right', 'right', 'right'),
position='left', func=on_row_click)
table.format('PL', f{table.VALUE}')
table.format('%', f'{table.VALUE} %')
table.new_row('SPY', 3, 'Submitted', 0, 0)
table.new_row('AMD', 1, 'Filled', 25.5, 105.24)
table.new_row('NVDA', 2, 'Filled', -0.5, -8.24)
table.footer(2)
table.footer[0] = 'Selected:'
chart.show(block=True)
```

View File

@ -1,98 +0,0 @@
# Toolbox
The Toolbox allows for trendlines, ray lines and horizontal lines to be drawn and edited directly on the chart.
It can be used within any Chart object, and is enabled by setting the `toolbox` parameter to `True` upon Chart declaration.
The following hotkeys can also be used when the Toolbox is enabled:
* Alt+T: Trendline
* Alt+H: Horizontal Line
* Alt+R: Ray Line
* Meta+Z or Ctrl+Z: Undo
Right-clicking on a drawing will open a context menu, allowing for color selection and deletion.
___
## `save_drawings_under`
`widget: Widget`
Saves drawings under a specific `topbar` text widget. For example:
```python
chart.toolbox.save_drawings_under(chart.topbar['symbol'])
```
___
## `load_drawings`
`tag: str`
Loads and displays drawings stored under the tag given.
___
## `import_drawings`
`file_path: str`
Imports the drawings stored at the JSON file given in `file_path`.
___
## `export_drawings`
`file_path: str`
Exports all currently saved drawings to the JSON file given in `file_path`.
___
## Example:
To get started, create a file called `drawings.json`, which should only contain `{}`.
```python
import pandas as pd
from lightweight_charts import Chart
def get_bar_data(symbol, timeframe):
if symbol not in ('AAPL', 'GOOGL', 'TSLA'):
print(f'No data for "{symbol}"')
return pd.DataFrame()
return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv')
def on_search(chart, searched_string):
new_data = get_bar_data(searched_string, chart.topbar['timeframe'].value)
if new_data.empty:
return
chart.topbar['symbol'].set(searched_string)
chart.set(new_data)
chart.toolbox.load_drawings(searched_string) # Loads the drawings saved under the symbol.
def on_timeframe_selection(chart):
new_data = get_bar_data(chart.topbar['symbol'].value, chart.topbar['timeframe'].value)
if new_data.empty:
return
chart.set(new_data, render_drawings=True) # The symbol has not changed, so we want to re-render the drawings.
if __name__ == '__main__':
chart = Chart(toolbox=True)
chart.legend(True)
chart.events.search += on_search
chart.topbar.textbox('symbol', 'TSLA')
chart.topbar.switcher('timeframe', ('1min', '5min', '30min'), default='5min', func=on_timeframe_selection)
df = get_bar_data('TSLA', '5min')
chart.set(df)
chart.toolbox.import_drawings('drawings.json') # Imports the drawings saved in the JSON file.
chart.toolbox.load_drawings(chart.topbar['symbol'].value) # Loads the drawings under the default symbol.
chart.toolbox.save_drawings_under(chart.topbar['symbol']) # Saves drawings based on the symbol.
chart.show(block=True)
chart.toolbox.export_drawings('drawings.json') # Exports the drawings to the JSON file upon close.
```

View File

@ -0,0 +1,160 @@
# Topbar & Events
This section gives an overview of how events are handled across the library.
## How to use events
Take a look at this minimal example, which uses the [`search`](#AbstractChart.Events) event:
```python
from lightweight_charts import Chart
def on_search(chart, string):
print(f'Search Text: "{string}" | Chart/SubChart ID: "{chart.id}"')
if __name__ == '__main__':
chart = Chart()
# Subscribe the function above to search event
chart.events.search += on_search
chart.show(block=True)
```
Upon searching in a pane, the expected output would be akin to:
```
Search Text: "AAPL" | Chart/SubChart ID: "window.blyjagcr"
```
The ID shown above will change depending upon which pane was used to search, allowing for access to the object in question.
```{important}
* When using `show` rather than `show_async`, block should be set to `True` (`chart.show(block=True)`).
* Event callables can be either coroutines, methods, or functions.
```
___
## Topbar events
Events can also be emitted from the topbar:
```python
from lightweight_charts import Chart
def on_button_press(chart):
new_button_value = 'On' if chart.topbar['my_button'].value == 'Off' else 'Off'
chart.topbar['my_button'].set(new_button_value)
print(f'Turned something {new_button_value.lower()}.')
if __name__ == '__main__':
chart = Chart()
chart.topbar.button('my_button', 'Off', func=on_button_press)
chart.show(block=True)
```
In this example, we are passing `on_button_press` to the `func` parameter.
When the button is pressed, the function will be emitted the `chart` object as with the previous example, allowing access to the topbar dictionary.
The `switcher` is typically used for timeframe selection:
```python
from lightweight_charts import Chart
def on_timeframe_selection(chart):
print(f'Getting data with a {chart.topbar["my_switcher"].value} timeframe.')
if __name__ == '__main__':
chart = Chart()
chart.topbar.switcher(
name='my_switcher',
options=('1min', '5min', '30min'),
default='5min',
func=on_timeframe_selection)
chart.show(block=True)
```
___
## Async clock
There are many use cases where we will need to run our own code whilst the GUI loop continues to listen for events. Let's demonstrate this by using the `textbox` widget to display a clock:
```python
import asyncio
from datetime import datetime
from lightweight_charts import Chart
async def update_clock(chart):
while chart.is_alive:
await asyncio.sleep(1-(datetime.now().microsecond/1_000_000))
chart.topbar['clock'].set(datetime.now().strftime('%H:%M:%S'))
async def main():
chart = Chart()
chart.topbar.textbox('clock')
await asyncio.gather(chart.show_async(block=True), update_clock(chart))
if __name__ == '__main__':
asyncio.run(main())
```
This is how the library is intended to be used with live data (option #2 [described here]()).
___
## Live data, topbar & events
Now we can create an asyncio program which updates chart data whilst allowing the GUI loop to continue processing events, based the [Live data](live_chart.md) example:
```python
import asyncio
import pandas as pd
from lightweight_charts import Chart
async def data_loop(chart):
ticks = pd.read_csv('ticks.csv')
for i, tick in ticks.iterrows():
if not chart.is_alive:
return
chart.update_from_tick(ticks.iloc[i])
await asyncio.sleep(0.03)
i += 1
def on_new_bar(chart):
print('New bar event!')
def on_timeframe_selection(chart):
print(f'Selected timeframe of {chart.topbar["timeframe"].value}')
async def main():
chart = Chart()
chart.events.new_bar += on_new_bar
chart.topbar.switcher('timeframe', ('1min', '5min'), func=on_timeframe_selection)
df = pd.read_csv('ohlc.csv')
chart.set(df)
await asyncio.gather(chart.show_async(block=True), data_loop(chart))
if __name__ == '__main__':
asyncio.run(main())
```

View File

@ -0,0 +1,85 @@
# Getting Started
## Installation
To install the library, use pip:
```text
pip install lightweight-charts
```
Pywebview's installation can differ depending on OS. Please refer to their [documentation](https://pywebview.flowrl.com/guide/installation.html#installation).
___
## A simple static chart
```python
import pandas as pd
from lightweight_charts import Chart
```
Download this
[`ohlcv.csv`](../../../examples/1_setting_data/ohlcv.csv)
file for this tutorial.
In this example, we are reading a csv file using pandas:
```text
date open high low close volume
0 2010-06-29 1.2667 1.6667 1.1693 1.5927 277519500.0
1 2010-06-30 1.6713 2.0280 1.5533 1.5887 253039500.0
2 2010-07-01 1.6627 1.7280 1.3513 1.4640 121461000.0
3 2010-07-02 1.4700 1.5500 1.2473 1.2800 75871500.0
4..
```
..which can be used as data for the `Chart` object:
```python
if __name__ == '__main__':
chart = Chart()
df = pd.read_csv('ohlcv.csv')
chart.set(df)
chart.show(block=True)
```
The `block` parameter is set to `True` in this case, as we do not want the program to exit.
```{warning}
Due to the library's use of multiprocessing, instantiations of `Chart` should be encapsulated within an `if __name__ == '__main__'` block.
```
## Adding a line
Now lets add a moving average to the chart using the following function:
```python
def calculate_sma(df, period: int = 50):
return pd.DataFrame({
'time': df['date'],
f'SMA {period}': df['close'].rolling(window=period).mean()
}).dropna()
```
`calculate_sma` derives the data column from `f'SMA {period}'`, which we will use as the name of our line:
```python
if __name__ == '__main__':
chart = Chart()
line = chart.create_line(name='SMA 50')
df = pd.read_csv('ohlcv.csv')
sma_df = calculate_sma(df, period=50)
chart.set(df)
line.set(sma_df)
chart.show(block=True)
```

View File

@ -1,4 +1,4 @@
from .abstract import LWC
from .abstract import AbstractChart, Window
from .chart import Chart
from .widgets import JupyterChart
from .polygon import PolygonChart

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,42 @@
import asyncio
import multiprocessing as mp
from base64 import b64decode
import webview
from lightweight_charts.abstract import LWC
chart = None
num_charts = 0
from lightweight_charts import abstract
from .util import parse_event_message
class CallbackAPI:
def __init__(self, emit_queue, return_queue):
self.emit_q, self.return_q = emit_queue, return_queue
def __init__(self, emit_queue):
self.emit_q = emit_queue
def callback(self, message: str):
name, args = message.split('_~_')
self.return_q.put(*args) if name == 'return' else self.emit_q.put((name, args.split(';;;')))
self.emit_q.put(message)
class PyWV:
def __init__(self, q, start: mp.Event, exit, loaded, html, width, height, x, y, on_top, maximize, debug, emit_queue, return_queue):
if maximize:
width, height = webview.screens[0].width, webview.screens[0].height
def __init__(self, q, start_ev, exit_ev, loaded, emit_queue, return_queue, html, debug,
width, height, x, y, on_top, maximize):
self.queue = q
self.exit = exit
self.callback_api = CallbackAPI(emit_queue, return_queue)
self.return_queue = return_queue
self.exit = exit_ev
self.callback_api = CallbackAPI(emit_queue)
self.loaded: list = loaded
self.html = html
self.windows = []
self.create_window(html, on_top, width, height, x, y)
self.create_window(width, height, x, y, on_top, maximize)
start.wait()
start_ev.wait()
webview.start(debug=debug)
self.exit.set()
def create_window(self, html, on_top, width, height, x, y):
def create_window(self, width, height, x, y, on_top, maximize):
if maximize:
width, height = webview.screens[0].width, webview.screens[0].height
self.windows.append(webview.create_window(
'', html=html, on_top=on_top, js_api=self.callback_api,
'', html=self.html, on_top=on_top, js_api=self.callback_api,
width=width, height=height, x=x, y=y, background_color='#000000'))
self.windows[-1].events.loaded += lambda: self.loop(self.loaded[len(self.windows)-1])
@ -47,57 +47,62 @@ class PyWV:
if i == 'create_window':
self.create_window(*arg)
elif arg in ('show', 'hide'):
getattr(self.windows[i], arg)()
getattr(self.windows[i], arg)()
elif arg == 'exit':
self.exit.set()
else:
try:
self.windows[i].evaluate_js(arg)
if '_~_~RETURN~_~_' in arg:
self.return_queue.put(self.windows[i].evaluate_js(arg[14:]))
else:
self.windows[i].evaluate_js(arg)
except KeyError:
return
class Chart(LWC):
class Chart(abstract.AbstractChart):
MAX_WINDOWS = 10
_window_num = 0
_main_window_handlers = None
_exit, _start = (mp.Event() for _ in range(2))
_q, _emit_q, _return_q = (mp.Queue() for _ in range(3))
_loaded_list = [mp.Event() for _ in range(MAX_WINDOWS)]
def __init__(self, width: int = 800, height: int = 600, x: int = None, y: int = None,
on_top: bool = False, maximize: bool = False, debug: bool = False, toolbox: bool = False,
inner_width: float = 1.0, inner_height: float = 1.0, scale_candles_only: bool = False):
super().__init__(inner_width, inner_height, scale_candles_only, toolbox, 'pywebview.api.callback')
global chart, num_charts
self._i = Chart._window_num
self._loaded = Chart._loaded_list[self._i]
window = abstract.Window(lambda s: self._q.put((self._i, s)), 'pywebview.api.callback')
abstract.Window._return_q = Chart._return_q
Chart._window_num += 1
self.is_alive = True
if chart:
self._q, self._exit, self._start, self._process = chart._q, chart._exit, chart._start, chart._process
self._emit_q, self._return_q = mp.Queue(), mp.Queue()
for key, val in self._handlers.items():
chart._handlers[key] = val
self._handlers = chart._handlers
self._loaded = chart._loaded_list[num_charts]
self._q.put(('create_window', (self._html, on_top, width, height, x, y)))
else:
self._q, self._emit_q, self._return_q = (mp.Queue() for _ in range(3))
self._loaded_list = [mp.Event() for _ in range(10)]
self._loaded = self._loaded_list[0]
self._exit, self._start = (mp.Event() for _ in range(2))
self._process = mp.Process(target=PyWV, args=(self._q, self._start, self._exit, self._loaded_list, self._html,
width, height, x, y, on_top, maximize, debug,
self._emit_q, self._return_q), daemon=True)
if self._i == 0:
super().__init__(window, inner_width, inner_height, scale_candles_only, toolbox)
Chart._main_window_handlers = self.win.handlers
self._process = mp.Process(target=PyWV, args=(
self._q, self._start, self._exit, Chart._loaded_list,
self._emit_q, self._return_q, abstract.TEMPLATE, debug,
width, height, x, y, on_top, maximize,
), daemon=True)
self._process.start()
chart = self
self.i = num_charts
num_charts += 1
self._script_func = lambda s: self._q.put((self.i, s))
else:
window.handlers = Chart._main_window_handlers
super().__init__(window, inner_width, inner_height, scale_candles_only, toolbox)
self._q.put(('create_window', (abstract.TEMPLATE, on_top, width, height, x, y)))
def show(self, block: bool = False):
"""
Shows the chart window.\n
:param block: blocks execution until the chart is closed.
"""
if not self.loaded:
if not self.win.loaded:
self._start.set()
self._loaded.wait()
self._on_js_load()
self.win.on_js_load()
else:
self._q.put((self.i, 'show'))
self._q.put((self._i, 'show'))
if block:
asyncio.run(self.show_async(block=True))
@ -107,20 +112,19 @@ class Chart(LWC):
asyncio.create_task(self.show_async(block=True))
return
try:
from lightweight_charts import polygon
[asyncio.create_task(self.polygon.async_set(*args)) for args in polygon._set_on_load]
while 1:
while self._emit_q.empty() and not self._exit.is_set() and self.polygon._q.empty():
while self._emit_q.empty() and not self._exit.is_set():
await asyncio.sleep(0.05)
if self._exit.is_set():
self._exit.clear()
self.is_alive = False
return
elif not self._emit_q.empty():
name, args = self._emit_q.get()
func = self._handlers[name]
func, args = parse_event_message(self.win, self._emit_q.get())
await func(*args) if asyncio.iscoroutinefunction(func) else func(*args)
continue
value = self.polygon._q.get()
func, args = value[0], value[1:]
func(*args)
except KeyboardInterrupt:
return
@ -128,16 +132,26 @@ class Chart(LWC):
"""
Hides the chart window.\n
"""
self._q.put((self.i, 'hide'))
self._q.put((self._i, 'hide'))
def exit(self):
"""
Exits and destroys the chart window.\n
"""
global num_charts, chart
chart = None
num_charts = 0
self._q.put((self.i, 'exit'))
self._exit.wait()
self._q.put((self._i, 'exit'))
self._exit.wait() if self.win.loaded else None
self._process.terminate()
del self
Chart._main_window_handlers = None
Chart._window_num = 0
Chart._q = mp.Queue()
self.is_alive = False
def screenshot(self) -> bytes:
"""
Takes a screenshot. This method can only be used after the chart window is visible.
:return: a bytes object containing a screenshot of the chart.
"""
self.run_script(f'_~_~RETURN~_~_{self.id}.chart.takeScreenshot().toDataURL()')
serial_data = self.win._return_q.get()
return b64decode(serial_data.split(',')[1])

View File

@ -14,6 +14,9 @@ if (!window.TopBar) {
this.topBar.style.display = 'flex'
this.topBar.style.alignItems = 'center'
chart.wrapper.prepend(this.topBar)
chart.topBar = this.topBar
this.reSize = () => chart.reSize()
this.reSize()
}
makeSwitcher(items, activeItem, callbackName) {
let switcherElement = document.createElement('div');
@ -45,6 +48,7 @@ if (!window.TopBar) {
switcherElement.appendChild(itemEl);
return itemEl;
});
widget.intervalElements = intervalElements
let onItemClicked = (item)=> {
if (item === activeItem) return
@ -59,6 +63,7 @@ if (!window.TopBar) {
this.topBar.appendChild(switcherElement)
this.makeSeparator(this.topBar)
this.reSize()
return widget
}
makeTextBoxWidget(text) {
@ -69,9 +74,10 @@ if (!window.TopBar) {
textBox.innerText = text
this.topBar.append(textBox)
this.makeSeparator(this.topBar)
this.reSize()
return textBox
}
makeButton(defaultText, callbackName) {
makeButton(defaultText, callbackName, separator) {
let button = document.createElement('button')
button.style.border = 'none'
button.style.padding = '2px 5px'
@ -103,7 +109,9 @@ if (!window.TopBar) {
button.style.color = this.textColor
button.style.fontWeight = 'normal'
})
if (separator) this.makeSeparator()
this.topBar.appendChild(button)
this.reSize()
return widget
}
@ -159,11 +167,10 @@ function makeSearchBox(chart) {
chart.chart.subscribeCrosshairMove((param) => {
if (param.point) yPrice = param.point.y;
})
let selectedChart = false
chart.wrapper.addEventListener('mouseover', (event) => selectedChart = true)
chart.wrapper.addEventListener('mouseout', (event) => selectedChart = false)
window.selectedChart = chart
chart.wrapper.addEventListener('mouseover', (event) => window.selectedChart = chart)
chart.commandFunctions.push((event) => {
if (!selectedChart) return false
if (selectedChart !== chart) return false
if (searchWindow.style.display === 'none') {
if (/^[a-zA-Z0-9]$/.test(event.key)) {
searchWindow.style.display = 'flex';

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;

View File

@ -1,9 +1,8 @@
if (!window.Table) {
class Table {
constructor(width, height, headings, widths, alignments, position, draggable = false, chart) {
constructor(width, height, headings, widths, alignments, position, draggable = false) {
this.container = document.createElement('div')
this.callbackName = null
this.chart = chart
if (draggable) {
this.container.style.position = 'absolute'
@ -15,7 +14,7 @@ if (!window.Table) {
this.container.style.zIndex = '2000'
this.container.style.width = width <= 1 ? width * 100 + '%' : width + 'px'
this.container.style.minHeight = height <= 1 ? height * 100 + '%' : height + 'px'
this.container.style.height = height <= 1 ? height * 100 + '%' : height + 'px'
this.container.style.display = 'flex'
this.container.style.flexDirection = 'column'
this.container.style.justifyContent = 'space-between'
@ -52,7 +51,10 @@ if (!window.Table) {
th.style.border = '1px solid rgb(70, 70, 70)'
}
this.container.appendChild(this.table)
let overflowWrapper = document.createElement('div')
overflowWrapper.style.overflow = 'auto'
overflowWrapper.appendChild(this.table)
this.container.appendChild(overflowWrapper)
document.getElementById('wrapper').appendChild(this.container)
if (!draggable) return
@ -137,11 +139,6 @@ if (!window.Table) {
this.footer[i].style.textAlign = 'center'
}
}
toJSON() {
// Exclude the chart attribute from serialization
const {chart, ...serialized} = this;
return serialized;
}
}
window.Table = Table
}

View File

@ -174,7 +174,7 @@ if (!window.ToolBox) {
currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
if (!currentTime) {
let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime))
currentTime = dateToChartTime(new Date(chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval)
currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)))
}
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
@ -298,7 +298,6 @@ if (!window.ToolBox) {
if (boundaryConditional) {
if (hoveringOver === drawing) return
if (!horizontal && !drawing.ray) drawing.line.setMarkers(drawing.markers)
document.body.style.cursor = 'pointer'
document.addEventListener('mousedown', checkForClick)
@ -324,13 +323,17 @@ if (!window.ToolBox) {
let originalPrice
let mouseDown = false
let clickedEnd = false
let labelColor
let checkForClick = (event) => {
mouseDown = true
document.body.style.cursor = 'grabbing'
this.chart.chart.applyOptions({handleScroll: false})
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
labelColor = this.chart.chart.options().crosshair.horzLine.labelBackgroundColor
this.chart.chart.applyOptions({crosshair: {horzLine: {labelBackgroundColor: hoveringOver.color}}})
if ('price' in hoveringOver) {
originalPrice = hoveringOver.price
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
@ -353,6 +356,8 @@ if (!window.ToolBox) {
document.body.style.cursor = this.chart.cursor
this.chart.chart.applyOptions({handleScroll: true})
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
this.chart.chart.applyOptions({crosshair: {horzLine: {labelBackgroundColor: labelColor}}})
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
window.callbackFunction(`${hoveringOver.id}_~_${hoveringOver.price.toFixed(8)}`);
}
@ -372,8 +377,8 @@ if (!window.ToolBox) {
let priceDiff = priceAtCursor - originalPrice
let barsToMove = param.logical - originalIndex
let startBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.from[0]).getTime())
let endBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.to[0]).getTime())
let startBarIndex = this.chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(hoveringOver.from[0]).getTime())
let endBarIndex = this.chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(hoveringOver.to[0]).getTime())
let startDate
let endBar
@ -385,19 +390,18 @@ if (!window.ToolBox) {
endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove]
}
let endDate = endBar ? endBar.time : dateToChartTime(new Date(chartTimeToDate(hoveringOver.to[0]).getTime() + (barsToMove * this.interval)), this.interval)
let endDate = endBar ? endBar.time : dateToStamp(new Date(stampToDate(hoveringOver.to[0]).getTime() + (barsToMove * this.interval)))
let startValue = hoveringOver.from[1] + priceDiff
let endValue = hoveringOver.to[1] + priceDiff
let data = calculateTrendLine(startDate, startValue, endDate, endValue, this.interval, this.chart, hoveringOver.ray)
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
hoveringOver.from = [data[0].time, data[0].value]
hoveringOver.to = [data[data.length - 1].time, data[data.length - 1].value]
hoveringOver.line.setData(data)
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
if (!hoveringOver.ray) {
@ -435,7 +439,7 @@ if (!window.ToolBox) {
let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time
if (!currentTime) {
let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime))
currentTime = dateToChartTime(new Date(chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval)
currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)))
}
let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart)
hoveringOver.line.setData(data)
@ -472,8 +476,8 @@ if (!window.ToolBox) {
renderDrawings() {
this.drawings.forEach((item) => {
if ('price' in item) return
let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from[0]).getTime() / this.interval) * this.interval), this.interval)
let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to[0]).getTime() / this.interval) * this.interval), this.interval)
let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.interval) * this.interval))
let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.interval) * this.interval))
let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray)
item.from = [data[0].time, data[0].value]
item.to = [data[data.length - 1].time, data[data.length-1].value]
@ -537,8 +541,8 @@ if (!window.ToolBox) {
},
}),
})
let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from[0]).getTime() / this.interval) * this.interval), this.interval)
let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to[0]).getTime() / this.interval) * this.interval), this.interval)
let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.interval) * this.interval))
let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.interval) * this.interval))
let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray)
item.from = [data[0].time, data[0].value]
item.to = [data[data.length - 1].time, data[data.length-1].value]
@ -664,7 +668,7 @@ if (!window.ColorPicker) {
}
openMenu(rect, drawing) {
this.drawing = drawing
this.rgbValues = this.extractRGB('price' in drawing ? drawing.priceLine.color : drawing.color)
this.rgbValues = this.extractRGB(drawing.color)
this.opacity = parseFloat(this.rgbValues[3])
this.container.style.top = (rect.top-30)+'px'
this.container.style.left = rect.right+'px'

View File

@ -2,26 +2,43 @@ import asyncio
import logging
import datetime as dt
import re
import threading
import queue
import json
import ssl
import urllib.request
from typing import Literal, Union, List
import pandas as pd
from lightweight_charts import Chart
from .chart import Chart
try:
import requests
except ImportError:
requests = None
try:
import websockets
except ImportError:
websockets = None
SEC_TYPE = Literal['stocks', 'options', 'indices', 'forex', 'crypto']
def convert_timeframe(timeframe):
ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter('%(asctime)s | [polygon.io] %(levelname)s: %(message)s', datefmt='%H:%M:%S'))
ch.setLevel(logging.DEBUG)
_log = logging.getLogger('polygon')
_log.setLevel(logging.ERROR)
_log.addHandler(ch)
api_key = ''
_tickers = {}
_set_on_load = []
_lasts = {}
_ws = {'stocks': None, 'options': None, 'indices': None, 'crypto': None, 'forex': None}
_subscription_type = {
'stocks': ('Q', 'A'),
'options': ('Q', 'A'),
'indices': ('V', None),
'forex': ('C', 'CA'),
'crypto': ('XQ', 'XA'),
}
def _convert_timeframe(timeframe):
spans = {
'min': 'minute',
'H': 'hour',
@ -37,50 +54,211 @@ def convert_timeframe(timeframe):
return multiplier, timespan
def _get_sec_type(ticker):
if '/' in ticker:
return 'forex'
for prefix, security_type in zip(('O:', 'I:', 'C:', 'X:'), ('options', 'indices', 'forex', 'crypto')):
if ticker.startswith(prefix):
return security_type
else:
return 'stocks'
def _polygon_request(query_url):
query_url = 'https://api.polygon.io'+query_url
query_url += f'&apiKey={api_key}'
request = urllib.request.Request(query_url, headers={'User-Agent': 'lightweight_charts/1.0'})
with urllib.request.urlopen(request) as response:
if response.status != 200:
error = response.json()
_log.error(f'({response.status}) Request failed: {error["error"]}')
return
data = json.loads(response.read())
if 'results' not in data:
_log.error(f'No results for {query_url}')
return
return data['results']
def get_bar_data(ticker: str, timeframe: str, start_date: str, end_date: str, limit: int = 5_000):
end_date = dt.datetime.now().strftime('%Y-%m-%d') if end_date == 'now' else end_date
mult, span = _convert_timeframe(timeframe)
if '-' in ticker:
ticker = ticker.replace('-', '')
query_url = f"/v2/aggs/ticker/{ticker}/range/{mult}/{span}/{start_date}/{end_date}?limit={limit}"
results = _polygon_request(query_url)
if not results:
return None
df = pd.DataFrame(results)
df['t'] = pd.to_datetime(df['t'], unit='ms')
rename = {'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 't': 'time'}
if not ticker.startswith('I:'):
rename['v'] = 'volume'
return df[rename.keys()].rename(columns=rename)
async def async_get_bar_data(ticker: str, timeframe: str, start_date: str, end_date: str, limit: int = 5_000):
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, get_bar_data, ticker, timeframe, start_date, end_date, limit)
async def _send(sec_type: SEC_TYPE, action: str, params: str):
ws = _ws[sec_type]
while ws is None:
await asyncio.sleep(0.05)
ws = _ws[sec_type]
await ws.send(json.dumps({'action': action, 'params': params}))
async def subscribe(ticker: str, sec_type: SEC_TYPE, func, args, precision=2):
if not _ws[sec_type]:
asyncio.create_task(_websocket_connect(sec_type))
if sec_type in ('forex', 'crypto'):
key = ticker[ticker.index(':')+1:]
key = key.replace('-', '/') if sec_type == 'forex' else key
else:
key = ticker
if not _lasts.get(key):
_lasts[key] = {
'price': 0,
'funcs': [],
'precision': precision
}
if sec_type != 'indices':
_lasts[key]['volume'] = 0
data = _lasts[key]
quotes, aggs = _subscription_type[sec_type]
await _send(sec_type, 'subscribe', f'{quotes}.{ticker}')
await _send(sec_type, 'subscribe', f'{aggs}.{ticker}') if aggs else None
if func in data['funcs']:
return
data['funcs'].append((func, args))
async def unsubscribe(func):
for key, data in _lasts.items():
if val := next(((f, args) for f, args in data['funcs'] if f == func), None):
break
else:
return
data['funcs'].remove(val)
if data['funcs']:
return
sec_type = _get_sec_type(key)
quotes, aggs = _subscription_type[sec_type]
await _send(sec_type, 'unsubscribe', f'{quotes}.{key}')
await _send(sec_type, 'unsubscribe', f'{aggs}.{key}')
async def _websocket_connect(sec_type):
if websockets is None:
raise ImportError('The "websockets" library was not found, and must be installed to pull live data.')
ticker_key = {
'stocks': 'sym',
'options': 'sym',
'indices': 'T',
'forex': 'p',
'crypto': 'pair',
}[sec_type]
async with websockets.connect(f'wss://socket.polygon.io/{sec_type}') as ws:
_ws[sec_type] = ws
await _send(sec_type, 'auth', api_key)
while 1:
response = await ws.recv()
data_list: List[dict] = json.loads(response)
for i, data in enumerate(data_list):
if data['ev'] == 'status':
_log.info(f'{data["message"]}')
continue
_ticker_key = {
'stocks': 'sym',
'options': 'sym',
'indices': 'T',
'forex': 'p',
'crypto': 'pair',
}
await _handle_tick(data[ticker_key], data)
async def _handle_tick(ticker, data):
lasts = _lasts[ticker]
sec_type = _get_sec_type(ticker)
if data['ev'] in ('Q', 'V', 'C', 'XQ'):
if sec_type == 'forex':
data['bp'] = data.pop('b')
data['ap'] = data.pop('a')
price = (data['bp'] + data['ap']) / 2 if sec_type != 'indices' else data['val']
if abs(price - lasts['price']) < (1/(10**lasts['precision'])):
return
lasts['price'] = price
if sec_type != 'indices':
lasts['volume'] = 0
if 't' not in data:
lasts['time'] = pd.to_datetime(data.pop('s'), unit='ms')
else:
lasts['time'] = pd.to_datetime(data['t'], unit='ms')
elif data['ev'] in ('A', 'CA', 'XA'):
lasts['volume'] = data['v']
if not lasts.get('time'):
return
lasts['symbol'] = ticker
for func, args in lasts['funcs']:
func(pd.Series(lasts), *args)
class PolygonAPI:
"""
Offers direct access to Polygon API data within all Chart objects.
It is not designed to be initialized by the user, and should be utilised
through the `polygon` method of `LWC` (chart.polygon.<method>).
through the `polygon` method of `AbstractChart` (chart.polygon.<method>).
"""
_set_on_load = []
def __init__(self, chart):
ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter('%(asctime)s | [polygon.io] %(levelname)s: %(message)s', datefmt='%H:%M:%S'))
ch.setLevel(logging.DEBUG)
self._log = logging.getLogger('polygon')
self._log.setLevel(logging.ERROR)
self._log.addHandler(ch)
self.max_ticks_per_response = 20
self._chart = chart
self._lasts = {}
self._key = None
self._ws_q = queue.Queue()
self._q = queue.Queue()
self._lock = threading.Lock()
def set(self, *args):
if asyncio.get_event_loop().is_running():
asyncio.create_task(self.async_set(*args))
return True
else:
_set_on_load.append(args)
return False
self._using_live_data = False
self._using_live = {'stocks': False, 'options': False, 'indices': False, 'crypto': False, 'forex': False}
self._ws = {'stocks': None, 'options': None, 'indices': None, 'crypto': None, 'forex': None}
self._tickers = {}
async def async_set(self, sec_type: Literal['stocks', 'options', 'indices', 'forex', 'crypto'], ticker, timeframe,
start_date, end_date, limit, live):
await unsubscribe(self._chart.update_from_tick)
df = await async_get_bar_data(ticker, timeframe, start_date, end_date, limit)
def log(self, info: bool):
"""
Streams informational messages related to Polygon.io.
"""
self._log.setLevel(logging.INFO) if info else self._log.setLevel(logging.ERROR)
self._chart.set(df, render_drawings=_tickers.get(self._chart) == ticker)
_tickers[self._chart] = ticker
def api_key(self, key: str):
"""
Sets the API key to be used with Polygon.io.
"""
self._key = key
if not live:
return True
await subscribe(ticker, sec_type, self._chart.update_from_tick, (True,), self._chart.num_decimals)
return True
def stock(self, symbol: str, timeframe: str, start_date: str, end_date='now', limit: int = 5_000, live: bool = False):
def stock(
self, symbol: str, timeframe: str, start_date: str, end_date='now',
limit: int = 5_000, live: bool = False
) -> bool:
"""
Requests and displays stock data pulled from Polygon.io.\n
:param symbol: Ticker to request.
@ -90,13 +268,17 @@ class PolygonAPI:
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
:param live: If true, the data will be updated in real-time.
"""
return self._set(self._chart, 'stocks', symbol, timeframe, start_date, end_date, limit, live)
return self.set('stocks', symbol, timeframe, start_date, end_date, limit, live)
def option(self, symbol: str, timeframe: str, start_date: str, expiration: str = None, right: Literal['C', 'P'] = None, strike: Union[int, float] = None,
end_date: str = 'now', limit: int = 5_000, live: bool = False):
def option(
self, symbol: str, timeframe: str, start_date: str, expiration: str = None,
right: Literal['C', 'P'] = None, strike: Union[int, float] = None,
end_date: str = 'now', limit: int = 5_000, live: bool = False
) -> bool:
"""
Requests and displays option data pulled from Polygon.io.\n
:param symbol: The underlying ticker to request. A formatted option ticker can also be given instead of using the expiration, right, and strike parameters.
:param symbol: The underlying ticker to request.
A formatted option ticker can also be given instead of using the expiration, right, and strike parameters.
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
:param start_date: Start date of the data (YYYY-MM-DD).
:param expiration: Expiration of the option (YYYY-MM-DD).
@ -107,10 +289,14 @@ class PolygonAPI:
:param live: If true, the data will be updated in real-time.
"""
if any((expiration, right, strike)):
symbol = f'{symbol}{dt.datetime.strptime(expiration, "%Y-%m-%d").strftime("%y%m%d")}{right}{strike * 1000:08d}'
return self._set(self._chart, 'options', f'O:{symbol}', timeframe, start_date, end_date, limit, live)
expiration = dt.datetime.strptime(expiration, "%Y-%m-%d").strftime("%y%m%d")
symbol = f'{symbol}{expiration}{right}{strike * 1000:08d}'
return self.set('options', f'O:{symbol}', timeframe, start_date, end_date, limit, live)
def index(self, symbol, timeframe, start_date, end_date='now', limit: int = 5_000, live=False):
def index(
self, symbol: str, timeframe: str, start_date: str, end_date: str = 'now',
limit: int = 5_000, live: bool = False
) -> bool:
"""
Requests and displays index data pulled from Polygon.io.\n
:param symbol: Ticker to request.
@ -120,9 +306,12 @@ class PolygonAPI:
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
:param live: If true, the data will be updated in real-time.
"""
return self._set(self._chart, 'indices', f'I:{symbol}', timeframe, start_date, end_date, limit, live)
return self.set('indices', f'I:{symbol}', timeframe, start_date, end_date, limit, live)
def forex(self, fiat_pair, timeframe, start_date, end_date='now', limit: int = 5_000, live=False):
def forex(
self, fiat_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
limit: int = 5_000, live: bool = False
) -> bool:
"""
Requests and displays forex data pulled from Polygon.io.\n
:param fiat_pair: The fiat pair to request. (USD-CAD, GBP-JPY etc.)
@ -132,9 +321,12 @@ class PolygonAPI:
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
:param live: If true, the data will be updated in real-time.
"""
return self._set(self._chart, 'forex', f'C:{fiat_pair}', timeframe, start_date, end_date, limit, live)
return self.set('forex', f'C:{fiat_pair}', timeframe, start_date, end_date, limit, live)
def crypto(self, crypto_pair, timeframe, start_date, end_date='now', limit: int = 5_000, live=False):
def crypto(
self, crypto_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
limit: int = 5_000, live: bool = False
) -> bool:
"""
Requests and displays crypto data pulled from Polygon.io.\n
:param crypto_pair: The crypto pair to request. (BTC-USD, ETH-BTC etc.)
@ -144,174 +336,61 @@ class PolygonAPI:
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
:param live: If true, the data will be updated in real-time.
"""
return self._set(self._chart, 'crypto', f'X:{crypto_pair}', timeframe, start_date, end_date, limit, live)
return self.set('crypto', f'X:{crypto_pair}', timeframe, start_date, end_date, limit, live)
def _set(self, chart, sec_type, ticker, timeframe, start_date, end_date, limit, live):
if requests is None:
raise ImportError('The "requests" library was not found, and must be installed to use polygon.io.')
async def async_stock(
self, symbol: str, timeframe: str, start_date: str, end_date: str = 'now',
limit: int = 5_000, live: bool = False
) -> bool:
return await self.async_set('stocks', symbol, timeframe, start_date, end_date, limit, live)
self._ws_q.put(('_unsubscribe', chart))
end_date = dt.datetime.now().strftime('%Y-%m-%d') if end_date == 'now' else end_date
mult, span = convert_timeframe(timeframe)
async def async_option(
self, symbol: str, timeframe: str, start_date: str, expiration: str = None,
right: Literal['C', 'P'] = None, strike: Union[int, float] = None,
end_date: str = 'now', limit: int = 5_000, live: bool = False
) -> bool:
if any((expiration, right, strike)):
expiration = dt.datetime.strptime(expiration, "%Y-%m-%d").strftime("%y%m%d")
symbol = f'{symbol}{expiration}{right}{strike * 1000:08d}'
return await self.async_set('options', f'O:{symbol}', timeframe, start_date, end_date, limit, live)
query_url = f"https://api.polygon.io/v2/aggs/ticker/{ticker.replace('-', '')}/range/{mult}/{span}/{start_date}/{end_date}?limit={limit}&apiKey={self._key}"
response = requests.get(query_url, headers={'User-Agent': 'lightweight_charts/1.0'})
if response.status_code != 200:
error = response.json()
self._log.error(f'({response.status_code}) Request failed: {error["error"]}')
return
data = response.json()
if 'results' not in data:
self._log.error(f'No results for "{ticker}" ({sec_type})')
return
async def async_index(
self, symbol: str, timeframe: str, start_date: str, end_date: str = 'now',
limit: int = 5_000, live: bool = False
) -> bool:
return await self.async_set('indices', f'I:{symbol}', timeframe, start_date, end_date, limit, live)
df = pd.DataFrame(data['results'])
columns = ['t', 'o', 'h', 'l', 'c']
rename = {'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 't': 'time'}
if sec_type != 'indices':
rename['v'] = 'volume'
columns.append('v')
df = df[columns].rename(columns=rename)
df['time'] = pd.to_datetime(df['time'], unit='ms')
async def async_forex(
self, fiat_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
limit: int = 5_000, live: bool = False
) -> bool:
return await self.async_set('forex', f'C:{fiat_pair}', timeframe, start_date, end_date, limit, live)
chart.set(df, render_drawings=self._tickers.get(chart) == ticker)
self._tickers[chart] = ticker
async def async_crypto(
self, crypto_pair: str, timeframe: str, start_date: str, end_date: str = 'now',
limit: int = 5_000, live: bool = False
) -> bool:
return await self.async_set('crypto', f'X:{crypto_pair}', timeframe, start_date, end_date, limit, live)
if not live:
return True
if not self._using_live_data:
threading.Thread(target=asyncio.run, args=[self._thread_loop()], daemon=True).start()
self._using_live_data = True
with self._lock:
if not self._ws[sec_type]:
self._ws_q.put(('_websocket_connect', self._key, sec_type))
self._ws_q.put(('_subscribe', chart, ticker, sec_type))
return True
@staticmethod
def log(info: bool):
"""
Streams informational messages related to Polygon.io.
"""
_log.setLevel(logging.INFO) if info else _log.setLevel(logging.ERROR)
async def _thread_loop(self):
while 1:
while self._ws_q.empty():
await asyncio.sleep(0.05)
value = self._ws_q.get()
func, args = value[0], value[1:]
asyncio.create_task(getattr(self, func)(*args))
async def _subscribe(self, chart, ticker, sec_type):
key = ticker if ':' not in ticker else ticker.split(':')[1]
if not self._lasts.get(key):
self._lasts[key] = {
'ticker': ticker,
'sec_type': sec_type,
'sub_type': {
'stocks': ('Q', 'A'),
'options': ('Q', 'A'),
'indices': ('V', None),
'forex': ('C', 'CA'),
'crypto': ('XQ', 'XA'),
}[sec_type],
'price': chart._last_bar['close'],
'charts': [],
}
quotes, aggs = self._lasts[key]['sub_type']
await self._send(self._lasts[key]['sec_type'], 'subscribe', f'{quotes}.{ticker}')
await self._send(self._lasts[key]['sec_type'], 'subscribe', f'{aggs}.{ticker}') if aggs else None
if sec_type != 'indices':
self._lasts[key]['volume'] = chart._last_bar['volume']
if chart in self._lasts[key]['charts']:
return
self._lasts[key]['charts'].append(chart)
async def _unsubscribe(self, chart):
for data in self._lasts.values():
if chart in data['charts']:
break
else:
return
if chart in data['charts']:
data['charts'].remove(chart)
if data['charts']:
return
while self._q.qsize():
self._q.get() # Flush the queue
quotes, aggs = data['sub_type']
await self._send(data['sec_type'], 'unsubscribe', f'{quotes}.{data["ticker"]}')
await self._send(data['sec_type'], 'unsubscribe', f'{aggs}.{data["ticker"]}')
async def _send(self, sec_type, action, params):
while 1:
with self._lock:
ws = self._ws[sec_type]
if ws:
break
await asyncio.sleep(0.1)
await ws.send(json.dumps({'action': action, 'params': params}))
async def _handle_tick(self, sec_type, data):
data['ticker_key'] = {
'stocks': 'sym',
'options': 'sym',
'indices': 'T',
'forex': 'p',
'crypto': 'pair',
}[sec_type]
key = data[data['ticker_key']].replace('/', '-')
if ':' in key:
key = key[key.index(':')+1:]
data['t'] = pd.to_datetime(data.pop('s'), unit='ms') if 't' not in data else pd.to_datetime(data['t'], unit='ms')
if data['ev'] in ('Q', 'V', 'C', 'XQ'):
self._lasts[key]['time'] = data['t']
if sec_type == 'forex':
data['bp'] = data.pop('b')
data['ap'] = data.pop('a')
if sec_type == 'indices':
self._lasts[key]['price'] = data['val']
else:
self._lasts[key]['price'] = (data['bp']+data['ap'])/2
self._lasts[key]['volume'] = 0
elif data['ev'] in ('A', 'CA', 'XA'):
self._lasts[key]['volume'] = data['v']
if not self._lasts[key].get('time'):
return
for chart in self._lasts[key]['charts']:
self._q.put((chart.update_from_tick, pd.Series(self._lasts[key]), True))
async def _websocket_connect(self, api_key, sec_type):
if websockets is None:
raise ImportError('The "websockets" library was not found, and must be installed to pull live data.')
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
async with websockets.connect(f'wss://socket.polygon.io/{sec_type}', ssl=ssl_context) as ws:
with self._lock:
self._ws[sec_type] = ws
await self._send(sec_type, 'auth', api_key)
while 1:
response = await ws.recv()
data_list: List[dict] = json.loads(response)
for i, data in enumerate(data_list):
if data['ev'] == 'status':
self._log.info(f'{data["message"]}')
continue
elif data_list.index(data) < len(data_list)-self.max_ticks_per_response:
continue
await self._handle_tick(sec_type, data)
def _subchart(self, subchart):
return PolygonAPISubChart(self, subchart)
class PolygonAPISubChart(PolygonAPI):
def __init__(self, polygon, subchart):
super().__init__(subchart)
self._set = polygon._set
@staticmethod
def api_key(key: str):
"""
Sets the API key to be used with Polygon.io.
"""
global api_key
api_key = key
class PolygonChart(Chart):
"""
A prebuilt callback chart object allowing for a standalone and plug-and-play
A prebuilt callback chart object allowing for a standalone, plug-and-play
experience of Polygon.io's API.
Tickers, security types and timeframes are to be defined within the chart window.
@ -319,41 +398,44 @@ class PolygonChart(Chart):
If using the standard `show` method, the `block` parameter must be set to True.
When using `show_async`, either is acceptable.
"""
def __init__(self, api_key: str, live: bool = False, num_bars: int = 200, end_date: str = 'now', limit: int = 5_000,
timeframe_options: tuple = ('1min', '5min', '30min', 'D', 'W'),
security_options: tuple = ('Stock', 'Option', 'Index', 'Forex', 'Crypto'),
toolbox: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
on_top: bool = False, maximize: bool = False, debug: bool = False):
super().__init__(width=width, height=height, x=x, y=y, on_top=on_top, maximize=maximize, debug=debug, toolbox=toolbox)
self.chart = self
def __init__(
self, api_key: str, live: bool = False, num_bars: int = 200, end_date: str = 'now', limit: int = 5_000,
timeframe_options: tuple = ('1min', '5min', '30min', 'D', 'W'),
security_options: tuple = ('Stock', 'Option', 'Index', 'Forex', 'Crypto'),
toolbox: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
on_top: bool = False, maximize: bool = False, debug: bool = False
):
super().__init__(width, height, x, y, on_top, maximize, debug, toolbox)
self.num_bars = num_bars
self.end_date = end_date
self.limit = limit
self.live = live
self.polygon.api_key(api_key)
self.events.search += self.on_search
self.legend(True)
self.grid(False, False)
self.crosshair(vert_visible=False, horz_visible=False)
self.topbar.active_background_color = 'rgb(91, 98, 246)'
self.topbar.textbox('symbol')
self.topbar.switcher('timeframe', timeframe_options, func=self._on_timeframe_selection)
self.topbar.switcher('security', security_options, func=self._on_security_selection)
self.legend(True)
self.grid(False, False)
self.crosshair(vert_visible=False, horz_visible=False)
self.events.search += self.on_search
self.run_script(f'''
{self.id}.search.box.style.backgroundColor = 'rgba(91, 98, 246, 0.5)'
{self.id}.spinner.style.borderTop = '4px solid rgba(91, 98, 246, 0.8)'
{self.id}.search.window.style.display = "flex"
{self.id}.search.box.focus()
''')
def _polygon(self, symbol):
async def _polygon(self, symbol):
self.spinner(True)
self.set(pd.DataFrame(), True)
self.crosshair(vert_visible=False, horz_visible=False)
mult, span = convert_timeframe(self.topbar['timeframe'].value)
mult, span = _convert_timeframe(self.topbar['timeframe'].value)
delta = dt.timedelta(**{span + 's': int(mult)})
short_delta = (delta < dt.timedelta(days=7))
start_date = dt.datetime.now() if self.end_date == 'now' else dt.datetime.strptime(self.end_date, '%Y-%m-%d')
@ -365,7 +447,7 @@ class PolygonChart(Chart):
remaining_bars -= 1
epoch = dt.datetime.fromtimestamp(0)
start_date = epoch if start_date < epoch else start_date
success = getattr(self.polygon, self.topbar['security'].value.lower())(
success = await getattr(self.polygon, 'async_'+self.topbar['security'].value.lower())(
symbol,
timeframe=self.topbar['timeframe'].value,
start_date=start_date.strftime('%Y-%m-%d'),
@ -374,14 +456,14 @@ class PolygonChart(Chart):
live=self.live
)
self.spinner(False)
self.crosshair(vert_visible=True, horz_visible=True) if success else None
self.crosshair() if success else None
return success
async def on_search(self, chart, searched_string):
self.topbar['symbol'].set(searched_string if self._polygon(searched_string) else '')
chart.topbar['symbol'].set(searched_string if await self._polygon(searched_string) else '')
async def _on_timeframe_selection(self, chart):
self._polygon(self.topbar['symbol'].value) if self.topbar['symbol'].value else None
await self._polygon(chart.topbar['symbol'].value) if chart.topbar['symbol'].value else None
async def _on_security_selection(self, chart):
self.precision(5 if self.topbar['security'].value == 'Forex' else 2)
self.precision(5 if chart.topbar['security'].value == 'Forex' else 2)

View File

@ -1,25 +1,28 @@
import random
from typing import Union
from .util import jbool
from .util import jbool, Pane, NUM
class Footer:
def __init__(self, table): self._table = table
def __init__(self, table):
self._table = table
def __setitem__(self, key, value): self._table._run_script(f'{self._table.id}.footer[{key}].innerText = "{value}"')
def __setitem__(self, key, value):
self._table.run_script(f'{self._table.id}.footer[{key}].innerText = "{value}"')
def __call__(self, number_of_text_boxes): self._table._run_script(f'{self._table.id}.makeFooter({number_of_text_boxes})')
def __call__(self, number_of_text_boxes: int):
self._table.run_script(f'{self._table.id}.makeFooter({number_of_text_boxes})')
class Row(dict):
def __init__(self, table, id, items):
super().__init__()
self.run_script = table.run_script
self._table = table
self._run_script = table._run_script
self.id = id
self.meta = {}
self._run_script(f'''{self._table.id}.newRow({list(items.values())}, '{self.id}')''')
self.run_script(f'{self._table.id}.newRow({list(items.values())}, "{self.id}")')
for key, val in items.items():
self[key] = val
@ -29,8 +32,7 @@ class Row(dict):
original_value = value
if column in self._table._formatters:
value = self._table._formatters[column].replace(self._table.VALUE, str(value))
self._run_script(f'{self._table.id}.updateCell("{self.id}", "{column}", "{value}")')
self.run_script(f'{self._table.id}.updateCell("{self.id}", "{column}", "{value}")')
return super().__setitem__(column, original_value)
def background_color(self, column, color): self._style('backgroundColor', column, color)
@ -38,30 +40,29 @@ class Row(dict):
def text_color(self, column, color): self._style('textColor', column, color)
def _style(self, style, column, arg):
self._run_script(f"{self._table.id}.rows[{self.id}]['{column}'].style.{style} = '{arg}'")
self.run_script(f"{self._table.id}.rows[{self.id}]['{column}'].style.{style} = '{arg}'")
def delete(self):
self._run_script(f"{self._table.id}.deleteRow('{self.id}')")
self.run_script(f"{self._table.id}.deleteRow('{self.id}')")
self._table.pop(self.id)
class Table(dict):
class Table(Pane, dict):
VALUE = 'CELL__~__VALUE__~__PLACEHOLDER'
def __init__(self, chart, width, height, headings, widths=None, alignments=None, position='left', draggable=False, func=None):
super().__init__()
self._run_script = chart.run_script
self._chart = chart
self.headings = headings
def __init__(self, window, width: NUM, height: NUM, headings: tuple, widths: tuple = None, alignments: tuple = None, position='left', draggable: bool = False, func: callable = None):
dict.__init__(self)
Pane.__init__(self, window)
self._formatters = {}
self.headings = headings
self.is_shown = True
self.win.handlers[self.id] = lambda rId: func(self[rId])
headings = list(headings)
widths = list(widths) if widths else []
alignments = list(alignments) if alignments else []
self.id = chart._rand.generate()
chart._handlers[self.id] = lambda rId: func(self[rId])
self._run_script(f'''
{self.id} = new Table({width}, {height}, {list(headings)}, {list(widths) if widths else []}, {list(alignments) if alignments else []},
'{position}', {jbool(draggable)}, {chart.id})
''')
self._run_script(f'{self.id}.callbackName = "{self.id}"') if func else None
self.run_script(f'{self.id} = new Table({width}, {height}, {headings}, {widths}, {alignments}, "{position}", {jbool(draggable)})')
self.run_script(f'{self.id}.callbackName = "{self.id}"') if func else None
self.footer = Footer(self)
def new_row(self, *values, id=None) -> Row:
@ -69,7 +70,7 @@ class Table(dict):
self[row_id] = Row(self, row_id, {heading: item for heading, item in zip(self.headings, values)})
return self[row_id]
def clear(self): self._run_script(f"{self.id}.clearRows()"), super().clear()
def clear(self): self.run_script(f"{self.id}.clearRows()"), super().clear()
def get(self, __key: Union[int, str]) -> Row: return super().get(int(__key))
@ -79,7 +80,7 @@ class Table(dict):
def visible(self, visible: bool):
self.is_shown = visible
self._run_script(f"""
self.run_script(f"""
{self.id}.container.style.display = '{'block' if visible else 'none'}'
{self.id}.container.{'add' if visible else 'remove'}EventListener('mousedown', {self.id}.onMouseDown)
""")

View File

@ -0,0 +1,47 @@
import json
class ToolBox:
def __init__(self, chart):
from lightweight_charts.abstract import JS
self.run_script = chart.run_script
self.id = chart.id
self._save_under = None
self.drawings = {}
chart.win.handlers[f'save_drawings{self.id}'] = self._save_drawings
self.run_script(JS['toolbox'])
self.run_script(f'{self.id}.toolBox = new ToolBox({self.id})')
def save_drawings_under(self, widget: 'Widget'):
"""
Drawings made on charts will be saved under the widget given. eg `chart.toolbox.save_drawings_under(chart.topbar['symbol'])`.
"""
self._save_under = widget
def load_drawings(self, tag: str):
"""
Loads and displays the drawings on the chart stored under the tag given.
"""
if not self.drawings.get(tag):
return
self.run_script(f'if ("toolBox" in {self.id}) {self.id}.toolBox.loadDrawings({json.dumps(self.drawings[tag])})')
def import_drawings(self, file_path):
"""
Imports a list of drawings stored at the given file path.
"""
with open(file_path, 'r') as f:
json_data = json.load(f)
self.drawings = json_data
def export_drawings(self, file_path):
"""
Exports the current list of drawings to the given file path.
"""
with open(file_path, 'w+') as f:
json.dump(self.drawings, f, indent=4)
def _save_drawings(self, drawings):
if not self._save_under:
return
self.drawings[self._save_under.value] = json.loads(drawings)

View File

@ -0,0 +1,90 @@
import asyncio
from typing import Dict
from .util import jbool, Pane
class Widget(Pane):
def __init__(self, topbar, value, func=None):
super().__init__(topbar.win)
self.value = value
def wrapper(v):
self.value = v
func(topbar._chart)
async def async_wrapper(v):
self.value = v
await func(topbar._chart)
self.win.handlers[self.id] = async_wrapper if asyncio.iscoroutinefunction(func) else wrapper
class TextWidget(Widget):
def __init__(self, topbar, initial_text):
super().__init__(topbar, value=initial_text)
self.run_script(f'{self.id} = {topbar.id}.makeTextBoxWidget("{initial_text}")')
def set(self, string):
self.value = string
self.run_script(f'{self.id}.innerText = "{string}"')
class SwitcherWidget(Widget):
def __init__(self, topbar, options, default, func):
super().__init__(topbar, value=default, func=func)
self.run_script(f'{self.id} = {topbar.id}.makeSwitcher({list(options)}, "{default}", "{self.id}")')
class ButtonWidget(Widget):
def __init__(self, topbar, button, separator, func):
super().__init__(topbar, value=button, func=func)
self.run_script(f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)})')
def set(self, string):
self.value = string
self.run_script(f'{self.id}.elem.innerText = "{string}"')
class TopBar(Pane):
def __init__(self, chart):
super().__init__(chart.win)
self._chart = chart
self._widgets: Dict[str, Widget] = {}
self.click_bg_color = '#50565E'
self.hover_bg_color = '#3c434c'
self.active_bg_color = 'rgba(0, 122, 255, 0.7)'
self.active_text_color = '#ececed'
self.text_color = '#d8d9db'
self._created = False
def _create(self):
if self._created:
return
from lightweight_charts.abstract import JS
self._created = True
self.run_script(JS['callback'])
self.run_script(f'''
{self.id} = new TopBar( {self._chart.id}, '{self.hover_bg_color}', '{self.click_bg_color}',
'{self.active_bg_color}', '{self.text_color}', '{self.active_text_color}')
''')
def __getitem__(self, item):
if widget := self._widgets.get(item):
return widget
raise KeyError(f'Topbar widget "{item}" not found.')
def get(self, widget_name): return self._widgets.get(widget_name)
def switcher(self, name, options: tuple, default: str = None, func: callable = None):
self._create()
self._widgets[name] = SwitcherWidget(self, options, default if default else options[0], func)
def textbox(self, name: str, initial_text: str = ''):
self._create()
self._widgets[name] = TextWidget(self, initial_text)
def button(self, name, button_text: str, separator: bool = True, func: callable = None):
self._create()
self._widgets[name] = ButtonWidget(self, button_text, separator, func)

View File

@ -1,6 +1,18 @@
import asyncio
from datetime import datetime
from random import choices
from typing import Literal
from typing import Literal, Union
import pandas as pd
class Pane:
def __init__(self, window):
from lightweight_charts import Window
self.win: Window = window
self.run_script = window.run_script
if hasattr(self, 'id'):
return
self.id = Window._id_gen.generate()
class IDGen(list):
@ -14,6 +26,13 @@ class IDGen(list):
self.generate()
def parse_event_message(window, string):
name, args = string.split('_~_')
args = args.split(';;;')
func = window.handlers[name]
return func, args
def jbool(b: bool): return 'true' if b is True else 'false' if b is False else None
@ -27,6 +46,12 @@ CROSSHAIR_MODE = Literal['normal', 'magnet']
PRICE_SCALE_MODE = Literal['normal', 'logarithmic', 'percentage', 'index100']
TIME = Union[datetime, pd.Timestamp, str]
NUM = Union[float, int]
FLOAT = Literal['left', 'right', 'top', 'bottom']
def line_style(line: LINE_STYLE):
js = 'LightweightCharts.LineStyle.'
@ -65,6 +90,7 @@ class Emitter:
def _emit(self, *args):
self._callable(*args) if self._callable else None
class JSEmitter:
def __init__(self, chart, name, on_iadd, wrapper=None):
self._on_iadd = on_iadd
@ -78,7 +104,7 @@ class JSEmitter:
async def final_async_wrapper(*arg):
await other(self._chart, *arg) if not self._wrapper else await self._wrapper(other, self._chart, *arg)
self._chart._handlers[self._name] = final_async_wrapper if asyncio.iscoroutinefunction(other) else final_wrapper
self._chart.win.handlers[self._name] = final_async_wrapper if asyncio.iscoroutinefunction(other) else final_wrapper
self._on_iadd(other)
return self
@ -89,7 +115,7 @@ class Events:
from lightweight_charts.abstract import JS
self.search = JSEmitter(chart, f'search{chart.id}',
lambda o: chart.run_script(f'''
{JS['callback'] if not chart._callbacks_enabled else ''}
{JS['callback']}
makeSpinner({chart.id})
{chart.id}.search = makeSearchBox({chart.id})
''')

View File

@ -1,97 +1,85 @@
import asyncio
from .util import parse_event_message
from lightweight_charts import abstract
try:
import wx.html2
except ImportError:
wx = None
try:
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtCore import QObject, pyqtSlot
class Bridge(QObject):
def __init__(self, chart):
super().__init__()
self.chart = chart
@pyqtSlot(str)
def callback(self, message):
_widget_message(self.chart, message)
from PyQt5.QtCore import QObject, pyqtSlot as Slot
except ImportError:
try:
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtCore import QObject, Slot
class Bridge(QObject):
def __init__(self, chart):
super().__init__()
self.chart = chart
@Slot(str)
def callback(self, message):
_widget_message(self.chart, message)
except ImportError:
QWebEngineView = None
if QWebEngineView:
class Bridge(QObject):
def __init__(self, chart):
super().__init__()
self.chart = chart
@Slot(str)
def callback(self, message):
emit_callback(self.chart, message)
try:
from streamlit.components.v1 import html
except ImportError:
html = None
try:
from IPython.display import HTML, display
except ImportError:
HTML = None
from lightweight_charts.abstract import LWC, JS
def _widget_message(chart, string):
name, args = string.split('_~_')
args = args.split(';;;')
func = chart._handlers[name]
def emit_callback(window, string):
func, args = parse_event_message(window, string)
asyncio.create_task(func(*args)) if asyncio.iscoroutinefunction(func) else func(*args)
class WxChart(LWC):
class WxChart(abstract.AbstractChart):
def __init__(self, parent, inner_width: float = 1.0, inner_height: float = 1.0,
scale_candles_only: bool = False, toolbox: bool = False):
if wx is None:
raise ModuleNotFoundError('wx.html2 was not found, and must be installed to use WxChart.')
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
super().__init__(abstract.Window(self.webview.RunScript, 'window.wx_msg.postMessage.bind(window.wx_msg)'),
inner_width, inner_height, scale_candles_only, toolbox)
super().__init__(inner_width=inner_width, inner_height=inner_height,
scale_candles_only=scale_candles_only, toolbox=toolbox,
_js_api_code='window.wx_msg.postMessage.bind(window.wx_msg)')
self._script_func = self.webview.RunScript
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, lambda e: wx.CallLater(500, self._on_js_load))
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: _widget_message(self, e.GetString()))
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, lambda e: wx.CallLater(500, self.win.on_js_load))
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: emit_callback(self, e.GetString()))
self.webview.AddScriptMessageHandler('wx_msg')
self.webview.SetPage(self._html, '')
self.webview.AddUserScript(JS['toolbox']) if toolbox else None
self.webview.SetPage(abstract.TEMPLATE, '')
self.webview.AddUserScript(abstract.JS['toolbox']) if toolbox else None
def get_webview(self): return self.webview
class QtChart(LWC):
class QtChart(abstract.AbstractChart):
def __init__(self, widget=None, inner_width: float = 1.0, inner_height: float = 1.0,
scale_candles_only: bool = False, toolbox: bool = False):
if QWebEngineView is None:
raise ModuleNotFoundError('QWebEngineView was not found, and must be installed to use QtChart.')
self.webview = QWebEngineView(widget)
super().__init__(inner_width=inner_width, inner_height=inner_height,
scale_candles_only=scale_candles_only, toolbox=toolbox,
_js_api_code='window.pythonObject.callback')
self._script_func = self.webview.page().runJavaScript
super().__init__(abstract.Window(self.webview.page().runJavaScript, 'window.pythonObject.callback'),
inner_width, inner_height, scale_candles_only, toolbox)
self.web_channel = QWebChannel()
self.bridge = Bridge(self)
self.web_channel.registerObject('bridge', self.bridge)
self.webview.page().setWebChannel(self.web_channel)
self.webview.loadFinished.connect(self._on_js_load)
self.webview.loadFinished.connect(self.win.on_js_load)
self._html = f'''
{self._html[:85]}
{abstract.TEMPLATE[:85]}
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script>
var bridge = new QWebChannel(qt.webChannelTransport, function(channel) {{
@ -99,32 +87,33 @@ class QtChart(LWC):
window.pythonObject = pythonObject
}});
</script>
{self._html[85:]}
{abstract.TEMPLATE[85:]}
'''
self.webview.page().setHtml(self._html)
def get_webview(self): return self.webview
class StaticLWC(LWC):
class StaticLWC(abstract.AbstractChart):
def __init__(self, width=None, height=None, inner_width=1, inner_height=1,
scale_candles_only: bool = False, toolbox=False, autosize=True):
super().__init__(inner_width, inner_height, scale_candles_only=scale_candles_only, toolbox=toolbox, autosize=autosize)
self._html = abstract.TEMPLATE.replace('</script>\n</body>\n</html>', '')
super().__init__(abstract.Window(run_script=self.run_script), inner_width, inner_height,
scale_candles_only, toolbox, autosize)
self.width = width
self.height = height
self._html = self._html.replace('</script>\n</body>\n</html>', '')
def run_script(self, script, run_last=False):
if run_last:
self._final_scripts.append(script)
self.win.final_scripts.append(script)
else:
self._html += '\n' + script
def load(self):
if self.loaded:
if self.win.loaded:
return
self.loaded = True
for script in self._final_scripts:
self.win.loaded = True
for script in self.win.final_scripts:
self._html += '\n' + script
self._load()
@ -143,8 +132,7 @@ class StreamlitChart(StaticLWC):
class JupyterChart(StaticLWC):
def __init__(self, width: int = 800, height=350, inner_width=1, inner_height=1, scale_candles_only: bool = False, toolbox: bool = False):
super().__init__(width, height, inner_width, inner_height, scale_candles_only, toolbox, autosize=False)
self._position = ""
super().__init__(width, height, inner_width, inner_height, scale_candles_only, toolbox, False)
self.run_script(f'''
for (var i = 0; i < document.getElementsByClassName("tv-lightweight-charts").length; i++) {{

View File

@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup(
name='lightweight_charts',
version='1.0.16',
version='1.0.17',
packages=find_packages(),
python_requires='>=3.8',
install_requires=[