diff --git a/src/charts/chart.js b/src/charts/chart.js index df29b84..097b36b 100644 --- a/src/charts/chart.js +++ b/src/charts/chart.js @@ -140,7 +140,7 @@ export function createShape(shapeType, points, options={}, ...callbacks) { shapeId = chart.createMultipointShape(points, options) if( callbacks.length ) shapeCallbacks[shapeId] = callbacks - console.log(`created ${shapeType.name}`, shapeId) + // console.log(`created ${shapeType.name}`, shapeId) const props = chart.getShapeById(shapeId).getProperties() invokeCallbacks(callbacks, 'onCreate', shapeId, points, props) return shapeId @@ -160,7 +160,7 @@ const shapeCallbacks = {} function onSelectedLineToolChanged() { const tool = widget.selectedLineTool(); - console.log('line tool changed', tool) + // console.log('line tool changed', tool) if( tool !== 'cursor' ) // 'cursor' cannot be selected manually and only happens just before the 'create' event is issued cancelDrawing(); } @@ -206,10 +206,13 @@ function handleCrosshairMovement(point) { 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) return + if (eventLock && event !== 'properties_changed') { // 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) @@ -219,70 +222,80 @@ function handleDrawingEvent(id, event) { function drawingEventWorker() { - const queue = drawingEventQueue - drawingEventQueue = [] - for (const [id,event] of queue) - doHandleDrawingEvent(id,event) -} - -function doHandleDrawingEvent(id, event) { - // console.log('drawing event', arguments) eventLock = true try { - const shape = event === 'remove' ? null : chart.getShapeById(id); - if (event === 'create') { - const co = useChartOrderStore(); - const callbacks = co.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, points, props) - invokeCallbacks(shapeCallbacks[id], 'onPoints', id, points) - invokeCallbacks(shapeCallbacks[id], 'onProps', id, props) - } - } else if (event === 'points_changed') { - if (id in shapeCallbacks) { - const points = shape.getPoints() - // console.log('points', points) - invokeCallbacks(shapeCallbacks[id], 'onPoints', id, points) - } - } else if (event === 'properties_changed') { - if (id in shapeCallbacks) { - const props = shape.getProperties() - // console.log('props', props) - invokeCallbacks(shapeCallbacks[id], 'onProps', id, props) - } - } else if (event === 'move') { - if (id in shapeCallbacks) { - invokeCallbacks(shapeCallbacks[id], 'onMove', id) - } - } else if (event === 'remove') { - if (id in shapeCallbacks) { - invokeCallbacks(shapeCallbacks[id], 'onDelete', id) - } - } else if (event === 'click') { - if (id in shapeCallbacks) { - invokeCallbacks(shapeCallbacks[id], 'onClick', id) - } - } else if (event === 'hide') { - if (id in shapeCallbacks) { - invokeCallbacks(shapeCallbacks[id], 'onHide', id) - } - } else if (event === 'show') { - if (id in shapeCallbacks) { - invokeCallbacks(shapeCallbacks[id], 'onShow', id) - } - } else - console.log('unknown drawing event', event) + const queue = drawingEventQueue + drawingEventQueue = [] + propsEvents = {} + // console.log('events locked', queue) + 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) + const shape = event === 'remove' ? null : chart.getShapeById(id); + if (event === 'create') { + const co = useChartOrderStore(); + const callbacks = co.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, points, props) + invokeCallbacks(shapeCallbacks[id], 'onPoints', id, points) + invokeCallbacks(shapeCallbacks[id], 'onProps', id, props) + } + } else if (event === 'points_changed') { + if (id in shapeCallbacks) { + const points = shape.getPoints() + // console.log('points', points) + invokeCallbacks(shapeCallbacks[id], 'onPoints', id, points) + } + } else if (event === 'properties_changed') { + const props = shape.getProperties() + if (id in shapeCallbacks) + invokeCallbacks(shapeCallbacks[id], 'onProps', id, 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) + } + } else if (event === 'remove') { + if (id in shapeCallbacks) { + invokeCallbacks(shapeCallbacks[id], 'onDelete', id) + } + } else if (event === 'click') { + if (id in shapeCallbacks) { + invokeCallbacks(shapeCallbacks[id], 'onClick', id) + } + } else if (event === 'hide') { + if (id in shapeCallbacks) { + invokeCallbacks(shapeCallbacks[id], 'onHide', id) + } + } else if (event === 'show') { + if (id in shapeCallbacks) { + invokeCallbacks(shapeCallbacks[id], 'onShow', id) + } + } else + console.log('unknown drawing event', event) +} + export function deleteShapeId(id) { if( id in shapeCallbacks ) delete shapeCallbacks[id] diff --git a/src/charts/datafeed.js b/src/charts/datafeed.js index 60a57bb..82cbec7 100644 --- a/src/charts/datafeed.js +++ b/src/charts/datafeed.js @@ -6,6 +6,8 @@ import { import {jBars} from './jBars.js'; import {metadata} from "@/version.js"; import FlexSearch from "flexsearch"; +import {useChartOrderStore} from "@/orderbuild.js"; + const lastBarsCache = new Map(); @@ -158,6 +160,7 @@ export default { onResolveErrorCallback('cannot resolve symbol'); return; } + useChartOrderStore().selectedSymbol = symbolItem // Symbol information object const symbolInfo = { ticker: symbolItem.full_name, diff --git a/src/charts/shape.js b/src/charts/shape.js index a300026..d68d16f 100644 --- a/src/charts/shape.js +++ b/src/charts/shape.js @@ -37,18 +37,19 @@ export const ShapeType = { class Shape { - constructor(type, model={}, onModel=null, onDelete=null) { + constructor(type, model={}, onModel=null, onDelete=null, props=null) { // the Shape object manages synchronizing internal data with a corresponding TradingView shape this.id = null // TradingView shapeId, or null if no TV shape created yet (drawing mode) this.type = type // ShapeType this.model = model // subclass-specific - this.points = null // TradingView points for the given shape - this.props = null + this.points = null + this.points = this.pointsFromModel() + console.log('construct points', this.points) + this.props = props === null ? this.propsFromModel() : mixin(props, this.propsFromModel()) if (onModel !== null) this.onModel = onModel if (onDelete !== null ) this.onDelete = onDelete - this.lock = 0 // used to prevent callbacks when we are the ones forcing the chart change this.create() } @@ -67,15 +68,13 @@ class Shape { create() { if (this.id !== null) return // programatically create the shape using the current this.points - const points = this.pointsFromModel() - if( points && points.length ) { - this.doCreate(points) + if( this.points && this.points.length ) { + this.doCreate() } } - doCreate(points) { - const props = this.propsFromModel() - createShape(this.type, points, {overrides:props}, new ShapeTVCallbacks(this)) + doCreate() { + createShape(this.type, this.points, {overrides:this.props}, new ShapeTVCallbacks(this)) } createOrDraw() { @@ -96,7 +95,7 @@ class Shape { setModel(model, props=null) { for( const [k,v] of Object.entries(model)) this.model[k] = v - this.setPoints(this.pointsFromModel()); + this.setPointsIfDirty(this.pointsFromModel()); let p const mp = this.propsFromModel(); if (props===null) { @@ -111,6 +110,7 @@ class Shape { setPoints(points) { + this.points = points if (points !== null && points.length) { if (this.id === null) this.doCreate(points) @@ -129,13 +129,19 @@ class Shape { } } } + // todo delete if points is null or empty? + } + + + setPointsIfDirty(points) { + if (dirtyPoints(this.points, points)) + this.setPoints(points) } setProps(props) { - if(this.id) { + if(this.id) this.tvShape().setProperties(props) - } } @@ -169,6 +175,15 @@ class Shape { // Model synchronization // + onDirtyModel(toModel) { + const old = {...this.model} + toModel.call(this) + const dirty = dirtyKeys(old, this.model) + // console.log('onDirtyModel', old, this.model, dirty) + if (dirty.length) + this.onModel(this.model) + } + pointsFromModel() {return null} pointsToModel() {} // set the model using this.points propsFromModel() {return null} @@ -198,6 +213,23 @@ class Shape { } +function dirtyPoints(pointsA, pointsB) { + if (pointsB===null) + return pointsA !== null + if (pointsA===null) + return pointsB.length > 0 + if (pointsA.length!==pointsB.length) + return true + for (const i in pointsA) { + const a = pointsA[i] + const b = pointsB[i] + if ( a.time !== b.time || a.price !== b.price ) + return true + } + return false +} + + // B is modifying A function dirtyProps(propsA, propsB) { if (propsB===null) @@ -212,6 +244,16 @@ function dirtyProps(propsA, propsB) { } +// B is modifying A +function dirtyKeys(propsA, propsB) { + if (propsB===null) + return propsA === null ? [] : [...Object.keys(propsA)] + if (propsA===null) + return [...Object.keys(propsB)] + return [...(new Set(Object.keys(propsB)).union(new Set(Object.keys(propsA))))].filter((k)=> !(k in propsA) || propsA[k] !== propsB[k]) +} + + class ShapeTVCallbacks { // These methods are called by TradingView and provide some default handling before invoking our own Shape callbacks @@ -230,16 +272,13 @@ class ShapeTVCallbacks { onPoints(shapeId, points) { this.shape.points = points this.shape.onPoints(points) - this.shape.pointsToModel() - this.shape.onModel(this.shape.model) + this.shape.onDirtyModel(this.shape.pointsToModel) } onProps(shapeId, props) { - console.log('props', shapeId, props) this.shape.props = props this.shape.onProps(props) - this.shape.propsToModel() - this.shape.onModel(this.shape.model) + this.shape.onDirtyModel(this.shape.propsToModel) } onDraw() { @@ -296,8 +335,8 @@ class ShapeTVCallbacks { export class HLine extends Shape { - constructor(model, onModel=null, onDelete=null) { - super(ShapeType.HLine, model, onModel, onDelete) + constructor(model, onModel=null, onDelete=null, props=null) { + super(ShapeType.HLine, model, onModel, onDelete, props) } pointsFromModel() { diff --git a/src/components/chart/BuilderFactory.vue b/src/components/chart/BuilderFactory.vue index c0026cb..e417074 100644 --- a/src/components/chart/BuilderFactory.vue +++ b/src/components/chart/BuilderFactory.vue @@ -1,5 +1,5 @@ diff --git a/src/components/chart/MarketBuilder.vue b/src/components/chart/MarketBuilder.vue index a68b285..c23e4a1 100644 --- a/src/components/chart/MarketBuilder.vue +++ b/src/components/chart/MarketBuilder.vue @@ -3,7 +3,7 @@
 
Market Order
- { const orders = ref([]) const selectedOrder = ref(null) + const selectedSymbol = ref(null) const drawing = ref(false) const drawingCallbacks = ref(null) // only during draw mode function newOrder() { console.log('cos new order') - const order = { id:uuid(), amount:1, builders:[] } + const order = { id:uuid(), amount:1, builders:[], amountIsTokenA: true, buy: true } orders.value.push(order) selectedOrder.value = order } @@ -52,7 +53,7 @@ export const useChartOrderStore = defineStore('chart_orders', () => { } return { - chartReady, orders, drawing, drawingCallbacks, newOrder, removeOrder, + chartReady, selectedSymbol, orders, drawing, drawingCallbacks, newOrder, removeOrder, } })