import {useChartOrderStore} from "@/orderbuild.js"; import {invokeCallbacks, prototype} from "@/common.js"; import {DataFeed, feelessTickerKey, getAllSymbols, lookupSymbol} from "@/charts/datafeed.js"; import {intervalToSeconds, SingletonCoroutine} from "@/misc.js"; import {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() export function addSymbolChangedCallback(cb) { symbolChangedCbs.push(cb) } export function removeSymbolChangedCallback(cb) { symbolChangedCbs = symbolChangedCbs.filter((i)=>i!==cb) } function symbolChanged(symbol) { if (symbol===null) co.selectedSymbol = null else { const info = lookupSymbol(symbol.ticker) symbolChangedCbs.forEach((cb) => cb(info)) co.selectedSymbol = info } updateFeeDropdown() } export async function setSymbol(symbol) { await new Promise(resolve => { if (co.selectedSymbol?.ticker !== symbol.ticker) widget.activeChart().setSymbol(symbol.ticker, null, resolve) 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, _timeframe) { co.intervalSecs = intervalToSeconds(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 feeDropdown = null export function initFeeDropdown(w) { widget = w widget.createDropdown( { title: 'Fees', tooltip: 'Choose Fee Tier', items: [/*{title: 'Automatic Fee Selection', onSelect: () => {log('autofees')}}*/], icon: ``, } ).then(dropdown => { feeDropdown = dropdown; updateFeeDropdown() }) } export function updateFeeDropdown() { if (feeDropdown === null) return const symbolItem = useChartOrderStore().selectedSymbol let items if (symbolItem === null) items = [{title: '0.00%'}] else { const feeGroup = symbolItem.feeGroup items = feeGroup.map((p) => { const [_addr, fee] = p return { title: (fee / 10000).toFixed(2) + '%', onSelect: ()=>{ if (fee !== symbolItem.fee) selectPool(fee) }, } }) } feeDropdown.applyOptions({items}) } function selectPool(fee) { const co = useChartOrderStore(); const s = co.selectedSymbol; const ticker = feelessTickerKey(s.ticker) + '|' + fee if (ticker !== s.ticker) setSymbolTicker(ticker).catch((e)=>console.error('Could not change TV symbol to', ticker)) } export function initWidget(el) { widget = window.tvWidget = new TradingView.widget({ library_path: "/charting_library/", // debug: true, autosize: true, symbol: 'default', interval: '15', container: el, datafeed: DataFeed, // use this for ohlc locale: "en", disabled_features: [], enabled_features: ['saveload_separate_drawings_storage'], drawings_access: {type: 'white', tools: [],}, // show no tools custom_themes: tvCustomThemes, theme: useStore().theme, }); // 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(()=>initFeeDropdown(widget)) 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] = point // console.log('drag calling onPoints', points, shape, lpbe) invokeCallbacks(shapeCallbacks[shapeId], 'onPoints', 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 }