import {useChartOrderStore} from "@/orderbuild.js"; import {invokeCallbacks, prototype} from "@/common.js"; import {DataFeed, defaultSymbol, feelessTickerKey, getAllSymbols, lookupSymbol} from "@/charts/datafeed.js"; import {intervalToSeconds, SingletonCoroutine, toHuman, toPrecision} from "@/misc.js"; import {usePrefStore, useStore} from "@/store/store.js"; import {tvCustomThemes} from "../../theme.js"; export let widget = null export let chart = null export let crosshairPoint = null let symbolChangedCbs = [] // callbacks for TV's chart.onSymbolChanged() const s = useStore() const co = useChartOrderStore() const prefs = usePrefStore() export function addSymbolChangedCallback(cb) { symbolChangedCbs.push(cb) } export function removeSymbolChangedCallback(cb) { symbolChangedCbs = symbolChangedCbs.filter((i)=>i!==cb) } function symbolChanged(symbol) { const info = symbol===null ? (defaultSymbol===null?'default':defaultSymbol) : lookupSymbol(symbol.ticker) co.selectedSymbol = info console.log('setting prefs ticker', info.ticker) prefs.selectedTicker = info.ticker symbolChangedCbs.forEach((cb) => cb(info)) updateFeeDropdown() console.log('symbol changed', info) } export async function setSymbol(symbol, interval=null) { if (interval===null) interval = widget.symbolInterval().interval await new Promise(resolve => { if (co.selectedSymbol?.ticker !== symbol.ticker) widget.setSymbol(symbol.ticker, interval, ()=>setTimeout(resolve,0)) else resolve() }) } export async function setSymbolTicker(ticker) { const found = getAllSymbols()[ticker] if (!found) { console.error('No symbol for ticker', ticker) return } await setSymbol(found) } function changeInterval(interval) { co.intervalSecs = intervalToSeconds(interval) prefs.selectedTimeframe = interval DataFeed.intervalChanged(co.intervalSecs) } function dataLoaded() { const range = chartMeanRange() console.log('new mean range', range,) co.meanRange = range } /* TradingView event keystrings const subscribeEvents = [ 'toggle_sidebar', 'indicators_dialog', 'toggle_header', 'edit_object_dialog', 'chart_load_requested', 'chart_loaded', 'mouse_down', 'mouse_up', 'drawing', 'study', 'undo', 'redo', 'undo_redo_state_changed', 'reset_scales', 'compare_add', 'add_compare', 'load_study_template', 'onTick', 'onAutoSaveNeeded', 'onScreenshotReady', 'onMarkClick', 'onPlusClick', 'onTimescaleMarkClick', 'onSelectedLineToolChanged', 'layout_about_to_be_changed', 'layout_changed', 'activeChartChanged', 'series_event', 'study_event', 'drawing_event', 'study_properties_changed', 'series_properties_changed', 'panes_height_changed', 'panes_order_changed', ] */ let poolButtonTextElement = null function initFeeDropdown() { const button = widget.createButton() button.setAttribute('title', 'See Pool Info and Choose Fee'); button.addEventListener('click', function () { co.showPoolSelection = true }); button.id = 'pool-button' button.style.height = '34px'; button.style.display = 'flex'; button.style.alignItems = 'center'; button.addEventListener('mouseover', () => { button.style.backgroundColor = 'rgb(60,60,60)'; }); button.addEventListener('mouseout', () => { button.style.backgroundColor = ''; }); button.style.margin = '2px 0'; button.style.borderRadius = '4px'; button.classList.add('pool-button') let img = document.createElement('img'); img.src = '/arbitrum-logo.svg'; img.style.width = '1em'; img.style.marginRight = '0.2em'; button.appendChild(img); img = document.createElement('img'); img.src = '/uniswap-logo.svg'; img.style.height = '1.25em'; img.style.marginRight = '0.2em'; img.style.backgroundColor = 'white'; img.style.borderRadius = '50%'; button.appendChild(img); const span = document.createElement('span'); span.style.marginY = 'auto'; button.appendChild(span); poolButtonTextElement = span updateFeeDropdown() } export function updateFeeDropdown() { if (poolButtonTextElement===null) return const symbolItem = useChartOrderStore().selectedSymbol let text = '' text += (symbolItem.fee / 10000).toFixed(2) + '%' const index = symbolItem.feeGroup.findIndex((p) => p[1] === symbolItem.fee) if (symbolItem.liquiditySymbol) { const liq = symbolItem.liquidities[index] if (symbolItem.liquiditySymbol === 'USD') text += ` $${toHuman(liq)}` else text = ` ${toHuman(liq)} ${symbolItem.liquiditySymbol}` } poolButtonTextElement.textContent = text } export function initTVButtons() { initFeeDropdown(); } export function initWidget(el) { const symbol = prefs.selectedTicker === null ? 'default' : prefs.selectedTicker const interval = prefs.selectedTimeframe === null ? '15' : prefs.selectedTimeframe widget = window.tvWidget = new TradingView.widget({ // Widget Options // https://www.tradingview.com/charting-library-docs/latest/api/interfaces/Charting_Library.ChartingLibraryWidgetOptions library_path: "/charting_library/", // debug: true, autosize: true, symbol, interval, container: el, datafeed: DataFeed, // use this for ohlc locale: "en", disabled_features: ['main_series_scale_menu',], enabled_features: ['saveload_separate_drawings_storage',], // drawings_access: {type: 'white', tools: [],}, // show no tools custom_themes: tvCustomThemes, theme: useStore().theme, timezone: prefs.timezone, // Chart Overrides // https://www.tradingview.com/charting-library-docs/latest/customization/overrides/chart-overrides overrides: { "mainSeriesProperties.priceAxisProperties.log": false, } }); // debug dump all events // for( const event of subscribeEvents ) // widget.subscribe(event, ()=>console.log('event', event, arguments)) widget.subscribe('drawing_event', handleDrawingEvent) widget.subscribe('onSelectedLineToolChanged', onSelectedLineToolChanged) widget.subscribe('mouse_down', mouseDown) widget.subscribe('mouse_up', mouseUp) widget.headerReady().then(()=>initTVButtons()) widget.onChartReady(initChart) console.log('tv widget initialized') } function initChart() { console.log('init chart') chart = widget.activeChart() const themeName = useStore().theme; widget.changeTheme(themeName).catch((e)=>console.warn(`Could not change theme to ${themeName}`, e)) chart.crossHairMoved().subscribe(null, (point)=>setTimeout(()=>handleCrosshairMovement(point),0) ) chart.onSymbolChanged().subscribe(null, symbolChanged) chart.onIntervalChanged().subscribe(null, changeInterval) chart.onDataLoaded().subscribe(null, dataLoaded) const tzapi = chart.getTimezoneApi(); tzapi.onTimezoneChanged().subscribe(null, (tz)=>{if (tz==='exchange') tz='Etc/UTC'; s.timeZone=tz}) s.timeZone = tzapi.getTimezone().id // chart.onHoveredSourceChanged().subscribe(null, ()=>console.log('hovered source changed', arguments)) // chart.selection().onChanged().subscribe(null, s => console.log('selection', chart.selection().allSources())); const symbolExt = chart.symbolExt(); // console.log('symbolExt', symbolExt); if(symbolExt) { symbolChanged(symbolExt) } else { symbolChanged(null) } changeInterval(widget.symbolInterval().interval) co.chartReady = true console.log('chart ready') } // noinspection JSUnusedLocalSymbols export const ShapeCallback = { onDraw: () => {}, // start drawing a new shape for the builder onRedraw: () => {}, // the mouse moved while in drawing mode onUndraw: () => {}, // drawing was canceled by clicking on a different tool onAddPoint: () => {}, // the user clicked a point while drawing (that point is added to the points list) onCreate: (shapeId, shape, points, props) => {}, // the user has finished creating all the control points. drawing mode is exited and the initial shape is created. onPoints: (shapeId, shape, points) => {}, // the control points of an existing shape were changed onProps: (shapeId, shape, props) => {}, // the display properties of an existing shape were changed onMove: (shapeId, shape, points) => {}, // the entire shape was moved without dragging control points onDrag: (shapeId, shape, points) => {}, // the shape is being dragged: this gets called on every mouse movement update during a drag onHide: (shapeId, shape, props) => {}, onShow: (shapeId, shape, props) => {}, onClick: (shapeId, shape) => {}, // the shape was selected onDelete: (shapeId) => {}, } // noinspection JSUnusedLocalSymbols export const VerboseCallback = prototype(ShapeCallback, { onDraw: ()=>{console.log('onDraw')}, // onRedraw: ()=>{console.log('onRedraw')}, onUndraw: ()=>{console.log('onUndraw')}, onAddPoint: ()=>{console.log('onAddPoint')}, onCreate: (shapeId, shape, points, props)=>{console.log('onCreate')}, onPoints: (shapeId, shape, points)=>{console.log('onPoints')}, onProps: (shapeId, shape, props)=>{console.log('onProps')}, onMove: (shapeId, shape, points)=>{console.log('onMove')}, onDrag: (shapeId, shape, points) => {console.log('onDrag')}, onHide: (shapeId, shape, props)=>{console.log('onHide')}, onShow: (shapeId, shape, props)=>{console.log('onShow')}, onClick: (shapeId, shape)=>{console.log('onClick')}, onDelete: (shapeId)=>{console.log('onDelete')}, }) let drawingTool = null let previousDrawingTool = null let drawingCallbacks = null export function drawShape(shapeType, ...callbacks) { // puts the chart into a line-drawing mode for a new shape console.log('drawShape', callbacks, shapeType.name, shapeType.code) if( drawingCallbacks ) invokeCallbacks(drawingCallbacks, 'onUndraw') drawingCallbacks = callbacks drawingTool = null previousDrawingTool = widget.selectedLineTool() co.drawing = true widget.selectLineTool(shapeType.code) invokeCallbacks(callbacks, 'onDraw') } export function createShape(shapeType, points, options={}, ...callbacks) { const chart = widget.activeChart() drawingCallbacks = null cancelDrawing() if (typeof shapeType === 'string' ) options.shape = shapeType else options.shape = shapeType.code // programatically adds a shape to the chart let shapeId try { // console.log('creating shape', points, options) if (points.time || points.price) shapeId = chart.createShape(points, options) else if (points.length === 1) shapeId = chart.createShape(points[0], options) else shapeId = chart.createMultipointShape(points, options) } catch (e) { console.error('tvShape could not create', shapeType, points, options, e) return null } if (shapeId===null) { console.error('could not create shape', points, options) } allShapeIds.push(shapeId) if( callbacks.length ) shapeCallbacks[shapeId] = callbacks const shape = chart.getShapeById(shapeId) // console.log('tvShape created', shapeId) shape.bringToFront() const props = shape.getProperties() invokeCallbacks(callbacks, 'onCreate', shapeId, shape, points, props) return shapeId } export let allShapeIds = [] export function cancelDrawing() { if (co.drawing) { co.drawing = false widget.selectLineTool(previousDrawingTool) invokeCallbacks(drawingCallbacks, 'onUndraw') } } const shapeCallbacks = {} function onSelectedLineToolChanged() { const tool = widget.selectedLineTool(); console.log('line tool changed', tool) if (drawingTool===null) drawingTool = tool else if (tool!==drawingTool && co.drawing) cancelDrawing(); } export let dragging = false export let draggingShapeIds = [] function mouseDown() { // console.log('mouseDown') // todo push into drawing event queue instead, then set dragging there dragging = true } function mouseUp() { // console.log('mouseUp') dragging = false draggingShapeIds = [] } function handleCrosshairMovement(point) { crosshairHandler.invoke(point) // delayed invocation to await selection to register later in the tv loop } const crosshairHandler = new SingletonCoroutine(doHandleCrosshairMovement, 0) function doHandleCrosshairMovement(point) { // console.log('crosshair moved') crosshairPoint = point if (co.drawing) invokeCallbacks(drawingCallbacks, 'onRedraw') else if (dragging) { const selection = chart.selection().allSources(); if (selection.length) draggingShapeIds = selection // console.log('dragging selected', draggingShapeIds) for (const shapeId of draggingShapeIds) { let shape try { shape = chart.getShapeById(shapeId); } catch (e) { continue } const points = structuredClone(shape.getPoints()); const lpbe = shape._model._linePointBeingEdited points[lpbe===null?0:lpbe] = point // console.log('calling onDrag', points, shape) invokeCallbacks(shapeCallbacks[shapeId], 'onDrag', shapeId, shape, points) } } else if (draggingShapeIds.length > 0) { draggingShapeIds = [] } } let drawingEventQueue = [] let eventLock = false // this is set to true before events are processed, in order to avoid any loops let propsEvents = {} function handleDrawingEvent(id, event) { if (eventLock && event !== 'properties_changed' && event !== 'remove') { // properties has its own locking mechanism console.log('ignoring event', id, event) return } // it's important to decouple our actions from the TV thread. we must wait until the TV loop is completed // before interacting with the chart if (drawingEventQueue.length===0) setTimeout(drawingEventWorker,0) drawingEventQueue.push([id,event]) } function drawingEventWorker() { eventLock = true try { const queue = drawingEventQueue drawingEventQueue = [] propsEvents = {} for (const [id, event] of queue) { if (event === 'properties_changed') propsEvents[id] = event else doHandleDrawingEvent(id, event) } for (const [k,v] of Object.entries(propsEvents)) doHandleDrawingEvent(k, v) } finally { eventLock = false // console.log('events unlocked') } } function doHandleDrawingEvent(id, event) { // console.log('drawing event', id, event) let shape if (event==='remove') shape = null else { try { shape = chart.getShapeById(id) } catch { return } } if (event === 'create') { allShapeIds.push(id) const callbacks = drawingCallbacks // console.log('drawing callbacks', callbacks) if (callbacks !== null) { shapeCallbacks[id] = callbacks co.drawing = false const points = shape.getPoints() const props = shape.getProperties() invokeCallbacks(shapeCallbacks[id], 'onCreate', id, shape, points, props) invokeCallbacks(shapeCallbacks[id], 'onPoints', id, shape, points) invokeCallbacks(shapeCallbacks[id], 'onProps', id, shape, props) } } else if (event === 'points_changed') { if (dragging) return if (id in shapeCallbacks) { const points = shape.getPoints() // console.log('points', id, points, dragStartPoints) invokeCallbacks(shapeCallbacks[id], 'onPoints', id, shape, points) } } else if (event === 'properties_changed') { const props = shape.getProperties() if (id in shapeCallbacks) invokeCallbacks(shapeCallbacks[id], 'onProps', id, shape, props) else // otherwise it's an event on a shape we don't "own" console.log('warning: ignoring setProperties on TV shape', id, props) } else if (event === 'move') { if (id in shapeCallbacks) { invokeCallbacks(shapeCallbacks[id], 'onMove', id, shape) } } else if (event === 'remove') { allShapeIds = allShapeIds.filter((v)=>v!==id) if (id in shapeCallbacks) { invokeCallbacks(shapeCallbacks[id], 'onDelete', id) } } else if (event === 'click') { if (id in shapeCallbacks) { invokeCallbacks(shapeCallbacks[id], 'onClick', id, shape) } } else if (event === 'hide') { if (id in shapeCallbacks) { invokeCallbacks(shapeCallbacks[id], 'onHide', id, shape) } } else if (event === 'showDialog') { if (id in shapeCallbacks) { invokeCallbacks(shapeCallbacks[id], 'onShow', id, shape) } } else console.log('unknown drawing event', event) } export function deleteShapeId(id) { if( id in shapeCallbacks ) delete shapeCallbacks[id] // console.log('removing entity', id) chart.removeEntity(id) } const MEAN_RANGE_MULTIPLIER = 3 function chartMeanRange() { let range = 0 const series = chart.getSeries() const bars = series.data().bars(); const final = Math.max(bars.size() - 50, 0) let count = 0 for (let barIndex = bars.size() - 1; barIndex >= final; barIndex--) { count++ const [_time, _open, high, low, _close, _volume, _ms] = bars.valueAt(barIndex) range += Math.abs(high - low) } if (count > 0) range /= count else range = 1 return range * MEAN_RANGE_MULTIPLIER }