From 042f96b37cd0059af778a6dd14c2c250cafaab6d Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 5 Feb 2024 20:02:49 -0400 Subject: [PATCH] charting refactor into shapes, not debugged --- src/blockchain/ohlcs.js | 2 +- src/blockchain/orderlib.js | 2 +- src/{ => charts}/chart.js | 140 +++++--- src/charts/shape.js | 202 ++++++++++++ src/{blockchain => }/common.js | 14 +- .../chart/{Builder.vue => BuilderFactory.vue} | 3 + src/components/chart/Chart.vue | 2 +- src/components/chart/ChartOrder.vue | 4 +- src/components/chart/DCABuilder.vue | 5 +- src/components/chart/LimitBuilder.vue | 311 +++++++++++++++--- src/components/chart/Test.vue | 17 + src/components/chart/Toolbar.vue | 22 +- src/orderbuild.js | 4 +- 13 files changed, 614 insertions(+), 114 deletions(-) rename src/{ => charts}/chart.js (54%) create mode 100644 src/charts/shape.js rename src/{blockchain => }/common.js (60%) rename src/components/chart/{Builder.vue => BuilderFactory.vue} (88%) create mode 100644 src/components/chart/Test.vue diff --git a/src/blockchain/ohlcs.js b/src/blockchain/ohlcs.js index 61bb852..913ea25 100644 --- a/src/blockchain/ohlcs.js +++ b/src/blockchain/ohlcs.js @@ -26,7 +26,7 @@ export function subOHLCs( chainId, poolPeriods ) { socket.emit('subOHLCs', chainId, toSub) } -export function unsubPrices( chainId, poolPeriods ) { +export function unsubOHLCs( chainId, poolPeriods ) { const toUnsub = [] for( const [pool,period] of poolPeriods ) { const key = `${pool}|${period}` diff --git a/src/blockchain/orderlib.js b/src/blockchain/orderlib.js index ce327f4..28fd3b1 100644 --- a/src/blockchain/orderlib.js +++ b/src/blockchain/orderlib.js @@ -1,5 +1,5 @@ import {uint32max, uint64max} from "@/misc.js"; -import {decodeIEE754, encodeIEE754} from "@/blockchain/common.js"; +import {decodeIEE754, encodeIEE754} from "@/common.js"; export const MAX_FRACTION = 65535; export const NO_CHAIN = uint64max; diff --git a/src/chart.js b/src/charts/chart.js similarity index 54% rename from src/chart.js rename to src/charts/chart.js index e3177e2..82a0695 100644 --- a/src/chart.js +++ b/src/charts/chart.js @@ -1,27 +1,11 @@ import {useChartOrderStore} from "@/orderbuild.js"; -import {prototype} from "@/blockchain/common.js"; +import {invokeCallbacks, prototype} from "@/common.js"; export let widget = null export let chart = null export let crosshairPoint = null -/* -TradingView drawing tool codes: -https://www.tradingview.com/charting-library-docs/latest/api/modules/Charting_Library/#supportedlinetools -text anchored_text note anchored_note signpost double_curve arc icon emoji sticker arrow_up arrow_down arrow_left arrow_right price_label price_note arrow_marker flag vertical_line horizontal_line cross_line horizontal_ray trend_line info_line trend_angle arrow ray extended parallel_channel disjoint_angle flat_bottom anchored_vwap pitchfork schiff_pitchfork_modified schiff_pitchfork balloon comment inside_pitchfork pitchfan gannbox gannbox_square gannbox_fixed gannbox_fan fib_retracement fib_trend_ext fib_speed_resist_fan fib_timezone fib_trend_time fib_circles fib_spiral fib_speed_resist_arcs fib_channel xabcd_pattern cypher_pattern abcd_pattern callout triangle_pattern 3divers_pattern head_and_shoulders fib_wedge elliott_impulse_wave elliott_triangle_wave elliott_triple_combo elliott_correction elliott_double_combo cyclic_lines time_cycles sine_line long_position short_position forecast date_range price_range date_and_price_range bars_pattern ghost_feed projection rectangle rotated_rectangle circle ellipse triangle polyline path curve cursor dot arrow_cursor eraser measure zoom brush highlighter regression_trend fixed_range_volume_profile -*/ - -export const ShapeType = { - Segment: {name: 'Trend Line', code: 'trend_line'}, - Ray: {name: 'Ray', code: 'ray'}, - Line: {name: 'Extended Line', code: 'extended'}, - HRay: {name: 'Horizontal Ray', code: 'horizontal_ray'}, - HLine: {name: 'Horizontal Line', code: 'horizontal_line'}, - VLine: {name: 'Vertical Line', code: 'vertical_line'} -} - - export function initWidget(el) { widget = window.tvWidget = new TradingView.widget({ library_path: "/tradingview/charting_library/", @@ -48,36 +32,41 @@ function initChart() { chart.crossHairMoved().subscribe(null, (point)=>{ crosshairPoint=point const co = useChartOrderStore() - invoke(co.drawingCallbacks, 'onRedraw') + invokeCallbacks(co.drawingCallbacks, 'onRedraw') }) } // noinspection JSUnusedLocalSymbols export const ShapeCallback = { - // a better word than "draw-er", an ShapeCallback renders one or more shapes in accordance with its - // backing builder. for example, a TWAPArtist would manage multiple vertical line shapes, starting - // at the current time and spaced out by the tranche interval. - // use this base class with misc.js/prototype() - - // - // ShapeType Events - // - 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, points, props)=>{}, // the user has finished creating all the control points. drawing mode is exited and the initial shape is created. - onPoints: (shapeId, points)=>{}, // the control points of an existing shape were changed - onProps: (shapeId, props)=>{}, // the display properties of an existing shape were changed - onMove: (shapeId, points)=>{}, // the entire shape was moved. todo same as onPoints? - onHide: (shapeId, props)=>{}, - onShow: (shapeId, props)=>{}, - onClick: (shapeId)=>{}, // the shape was selected - onDeleted: (shapeId)=>{}, + 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, points, props) => { + }, // the user has finished creating all the control points. drawing mode is exited and the initial shape is created. + onPoints: (shapeId, points) => { + }, // the control points of an existing shape were changed + onProps: (shapeId, props) => { + }, // the display properties of an existing shape were changed + onMove: (shapeId, points) => { + }, // the entire shape was moved. todo same as onPoints? + onHide: (shapeId, props) => { + }, + onShow: (shapeId, props) => { + }, + onClick: (shapeId) => { + }, // the shape was selected + onDelete: (shapeId) => { + }, } +// noinspection JSUnusedLocalSymbols export const VerboseCallback = prototype(ShapeCallback, { onDraw: ()=>{console.log('onDraw')}, // onRedraw: ()=>{console.log('onRedraw')}, @@ -111,28 +100,40 @@ export const BuilderUpdateCallback = prototype(ShapeCallback,{ }) -function invoke(callbacks, prop, ...args) { - if (!callbacks) return - callbacks.forEach((cb)=>{if(prop in cb) cb[prop].call(cb, ...args)}) -} - - 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) const co = useChartOrderStore() if( co.drawingCallbacks ) - invoke(co.drawingCallbacks, 'onUndraw') + invokeCallbacks(co.drawingCallbacks, 'onUndraw') co.drawingCallbacks = callbacks co.drawing = true widget.selectLineTool(shapeType.code) - invoke(callbacks, 'onDraw') + invokeCallbacks(callbacks, 'onDraw') +} + + +export function createShape(shapeType, points, options, ...callbacks) { + options.shape = shapeType.code + // programatically adds a shape to the chart + let shapeId + 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) + if( callbacks.length ) + shapeCallbacks[shapeId] = callbacks + console.log('created shape', shapeId) + return shapeId } export function cancelDrawing() { const co = useChartOrderStore() if (co.drawing) { - invoke(co.drawingCallbacks, 'onUndraw') + invokeCallbacks(co.drawingCallbacks, 'onUndraw') co.drawing = false } } @@ -151,8 +152,17 @@ export function builderShape(builder, tag, shapeType, ...callbacks) { export function setPoints( shapeId, points ) { + if( points.time || points.price ) + points = [points] console.log('setting points', shapeId, points) - widget.activeChart().getShapeById(shapeId).setPoints(points) + let shape + try { + shape = chart.getShapeById(shapeId) + } + catch (e) { + return + } + shape.setPoints(points) } @@ -177,27 +187,49 @@ function handleDrawingEvent(id, event) { co.drawing = false const points = shape.getPoints() const props = shape.getProperties() - invoke(shapeCallbacks[id], 'onCreate', id, points, props) - invoke(shapeCallbacks[id], 'onPoints', id, points) - invoke(shapeCallbacks[id], 'onProps', id, props) + 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) - invoke(shapeCallbacks[id], 'onPoints', id, points) + invokeCallbacks(shapeCallbacks[id], 'onPoints', id, points) } } else if (event === 'properties_changed') { console.log('id in shapes', id in shapeCallbacks, id, shapeCallbacks) if (id in shapeCallbacks) { const props = shape.getProperties() console.log('props', props) - invoke(shapeCallbacks[id], 'onProps', id, 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) { - invoke(shapeCallbacks[id], 'onDelete', id) + 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 deleteShape(id) { + if( id in shapeCallbacks ) + delete shapeCallbacks[id] + chart.removeEntity(id) +} diff --git a/src/charts/shape.js b/src/charts/shape.js new file mode 100644 index 0000000..98d0106 --- /dev/null +++ b/src/charts/shape.js @@ -0,0 +1,202 @@ +// noinspection JSPotentiallyInvalidUsageOfThis + +import {invokeCallback, mixin} from "@/common.js"; +import {chart, createShape, deleteShape, drawShape} from "@/charts/chart.js"; +import {ref, watch} from "vue"; + + +// +// Usage of Shapes: +// const shape = new Shape(ShapeType.HLine) +// shape.draw() +// shape.points +// > [{time:17228394, price:42638.83}] +// +// shape.model is a vue ref({}) which stores the shape-specific control points, as defined by the shape subclass +// use the shape.model.* fields in components to get reactive effects with the chart. +// + + +/* +The `name` field must match the user-facing name in the TradingView toolbar UI +TradingView drawing tool codes: +https://www.tradingview.com/charting-library-docs/latest/api/modules/Charting_Library/#supportedlinetools +text anchored_text note anchored_note signpost double_curve arc icon emoji sticker arrow_up arrow_down arrow_left arrow_right price_label price_note arrow_marker flag vertical_line horizontal_line cross_line horizontal_ray trend_line info_line trend_angle arrow ray extended parallel_channel disjoint_angle flat_bottom anchored_vwap pitchfork schiff_pitchfork_modified schiff_pitchfork balloon comment inside_pitchfork pitchfan gannbox gannbox_square gannbox_fixed gannbox_fan fib_retracement fib_trend_ext fib_speed_resist_fan fib_timezone fib_trend_time fib_circles fib_spiral fib_speed_resist_arcs fib_channel xabcd_pattern cypher_pattern abcd_pattern callout triangle_pattern 3divers_pattern head_and_shoulders fib_wedge elliott_impulse_wave elliott_triangle_wave elliott_triple_combo elliott_correction elliott_double_combo cyclic_lines time_cycles sine_line long_position short_position forecast date_range price_range date_and_price_range bars_pattern ghost_feed projection rectangle rotated_rectangle circle ellipse triangle polyline path curve cursor dot arrow_cursor eraser measure zoom brush highlighter regression_trend fixed_range_volume_profile +*/ + +export const ShapeType = { + Segment: {name: 'Trend Line', code: 'trend_line'}, + Ray: {name: 'Ray', code: 'ray'}, + Line: {name: 'Extended Line', code: 'extended'}, + HRay: {name: 'Horizontal Ray', code: 'horizontal_ray'}, + HLine: {name: 'Horizontal Line', code: 'horizontal_line'}, + VLine: {name: 'Vertical Line', code: 'vertical_line'}, + PriceRange: {name: 'Price Range', code: 'price_range'}, +} + + +class Shape { + constructor(type, model={}) { + // 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 = this.pointsFromModel() // same format as TradingView points for the given shape + this.props = this.propsFromModel() // TradingView shape properties + console.log('points/props', this.points, this.props) + watch(this.model, this.onModelChanged) + if( this.points && this.points.length ) + this.create() + } + + // + // primary interface methods + // + + draw() { + // have the user draw this shape + console.log(`draw ${this.type.name}`) + if (this.id) + throw Error(`Shape already exists ${this.id}`) + drawShape(this.type, new ShapeTVCallbacks(this)) + } + + create() { + // programatically create the shape using the current this.points + console.log(`create ${this.type.name}`) + if(this.id) { + console.log('warning: re-creating a shape') + this.delete() + } + if(this.points.length === 0) + throw Error('Cannot create a shape with no points') + createShape(this.type, this.points, new ShapeTVCallbacks(this)) + } + + createOrDraw() { + if(this.id) return + if(this.points && this.points.length) + this.create() + else + this.draw() + } + + setPoints(points) { + this.points = points + invokeCallback(this, 'onPoints', points) + this.pointsToModel() + if (points && points.length) { + if (this.id) + chart.getEntity(this.id).setPoints(points) + else + this.create() + } + } + + setProps(props) { + this.props = props + invokeCallback(this, 'onProps', props) + this.propsToModel() + if (props && this.id) + chart.getEntity(this.id).setProperties(props) + } + + delete() { + if (this.id) { + deleteShape(this.id) + this.id = null + } + else + invokeCallback(this.callbacks, 'onDelete') + } + + + // + // Model synchronization + // + + onModelChanged() { + const points = this.pointsFromModel() + if( points && points !== this.points ) + this.setPoints(points) + const props = this.propsFromModel() + if( props && props !== this.props ) + this.setProps(props) + } + + pointsFromModel() {return null} + pointsToModel() {} // set the model using this.points + propsFromModel() {return null} + propsToModel() {} // set the model using this.props + + + // + // Overridable shape callbacks + // + + 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(points, props) {} // the user has finished creating all the control points. drawing mode is exited and the initial shape is created. + onPoints(points) {} // the control points of an existing shape were changed + onProps(props) {} // the display properties of an existing shape were changed + onMove(points) {} // the shape was moved by dragging a drawing element not the control point + onHide(props) {} + onShow(props) {} + onClick() {} // the shape was selected + onDelete() {} + +} + + +class ShapeTVCallbacks { + constructor(shape) { + console.log('shapetvcb', shape) + this.shape = shape + } + onCreate(shapeId, points, props) { + if( this.id ) + throw Error(`Created a shape ${shapeId}where one already existed ${this.id}`) + this.id=shapeId + invokeCallback(this.shape, 'onCreate', points, props) + } + onPoints(shapeId, points) { + if( points === this.shape.points ) return // prevent reactive feedback loops + this.shape.setPoints(points) + } + onProps(shapeId, props) { + if( props === this.shape.props ) return // prevent reactive feedback loops + this.shape.setProps(props) + } + + onDraw() {invokeCallback(this.shape, 'onDraw')} + onRedraw() {invokeCallback(this.shape, 'onRedraw')} + onUndraw() {invokeCallback(this.shape, 'onUndraw')} + onAddPoint() {invokeCallback(this.shape, 'onAddPoint')} + onMove(_shapeId, points) {invokeCallback(this.shape, 'onMove',points)} + onHide(_shapeId, props) {invokeCallback(this.shape, 'onHide',props)} + onShow(_shapeId, props) {invokeCallback(this.shape, 'onShow',props)} + onClick(_shapeId) {invokeCallback(this.shape, 'onClick')} + onDelete(_shapeId, props) {invokeCallback(this.shape, 'onDelete',props)} +} + + +export class HLine extends Shape { + constructor(model) { + mixin(model, {price:null,color:null}) + super(ShapeType.HLine, model); + } + + pointsToModel() { + console.log('pointsToModel', this.points, this.model) + this.model.price = this.points[0].price + } + pointsFromModel() { + console.log('pointsFromModel', this.model.price) + return this.model.price === null ? null : [{time:0,price:this.model.price}] + } + propsToModel() {this.model.color=this.props.linecolor} + propsFromModel() {return this.model.color ? {linecolor: this.model.color} : null} +} + diff --git a/src/blockchain/common.js b/src/common.js similarity index 60% rename from src/blockchain/common.js rename to src/common.js index 8df0e03..0f5a73d 100644 --- a/src/blockchain/common.js +++ b/src/common.js @@ -1,8 +1,9 @@ - export function mixin(child, ...parents) { + // child is modified directly, assigning fields from parents that are missing in child. parents fields are + // assigned by parents order, highest priority first for( const parent of parents ) { for ( const key in parent) { - if (parent.hasOwnProperty(key)) { + if (parent.hasOwnProperty(key) && !child.hasOwnProperty(key)) { child[key] = parent[key]; } } @@ -17,6 +18,15 @@ export function prototype(parent, child) { return result } +export function invokeCallback(cbs, prop, ...args) { + if (prop in cbs) cbs[prop].call(cbs, ...args) +} + +export function invokeCallbacks(callbacks, prop, ...args) { + if (!callbacks) return + callbacks.forEach((cb)=>invokeCallback(cb, prop, ...args)) +} + export function encodeIEE754(value) { const buffer = new ArrayBuffer(4); diff --git a/src/components/chart/Builder.vue b/src/components/chart/BuilderFactory.vue similarity index 88% rename from src/components/chart/Builder.vue rename to src/components/chart/BuilderFactory.vue index ad864ae..331eaf4 100644 --- a/src/components/chart/Builder.vue +++ b/src/components/chart/BuilderFactory.vue @@ -6,6 +6,7 @@ import {computed} from "vue"; import DCABuilder from "@/components/chart/DCABuilder.vue"; import LimitBuilder from "@/components/chart/LimitBuilder.vue"; +import Test from "@/components/chart/Test.vue"; const props = defineProps(['builder']) @@ -16,6 +17,8 @@ const component = computed(()=>{ return DCABuilder case 'LimitBuilder': return LimitBuilder + case 'Test': + return Test default: console.error('Unknown builder component '+props.builder.component) return null diff --git a/src/components/chart/Chart.vue b/src/components/chart/Chart.vue index 8916e47..1871db9 100644 --- a/src/components/chart/Chart.vue +++ b/src/components/chart/Chart.vue @@ -6,7 +6,7 @@ import "/tradingview/charting_library/charting_library.js" import "/tradingview/datafeeds/udf/dist/bundle.js" import {onMounted, ref} from "vue"; -import {initWidget} from "@/chart.js"; +import {initWidget} from "@/charts/chart.js"; const element = ref() diff --git a/src/components/chart/ChartOrder.vue b/src/components/chart/ChartOrder.vue index bde9d5b..4541a07 100644 --- a/src/components/chart/ChartOrder.vue +++ b/src/components/chart/ChartOrder.vue @@ -4,7 +4,7 @@
@@ -13,7 +13,7 @@ diff --git a/src/components/chart/Test.vue b/src/components/chart/Test.vue new file mode 100644 index 0000000..c4f7a0b --- /dev/null +++ b/src/components/chart/Test.vue @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/src/components/chart/Toolbar.vue b/src/components/chart/Toolbar.vue index db627e2..4de2505 100644 --- a/src/components/chart/Toolbar.vue +++ b/src/components/chart/Toolbar.vue @@ -1,12 +1,12 @@