From e5399d9fc9e9206aeff431113789815801f85024 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 1 Feb 2024 23:11:20 -0400 Subject: [PATCH] interactive horizontal limit lines --- package.json | 1 + src/blockchain/common.js | 20 +++ src/chart.js | 203 +++++++++++++++++++++++++ src/components/Btn.vue | 8 +- src/components/Orders.vue | 4 +- src/components/SplitPane.vue | 4 +- src/components/chart/Builder.vue | 28 ++++ src/components/chart/BuilderPanel.vue | 28 ++++ src/components/chart/Chart.vue | 75 +-------- src/components/chart/ChartOrder.vue | 29 ++-- src/components/chart/ChartTranche.vue | 18 --- src/components/chart/DCABuilder.vue | 32 ++++ src/components/chart/LimitBuilder.vue | 86 +++++++++++ src/components/chart/LimitTranche.vue | 15 -- src/components/chart/LineTranche.vue | 34 ----- src/components/chart/MarketTranche.vue | 16 -- src/components/chart/Toolbar.vue | 49 ++++++ src/components/chart/TranchePanel.vue | 21 --- src/layouts/chart/ChartLayout.vue | 3 - src/orderbuild.js | 69 +++++++-- src/store/store.js | 23 +-- src/styles/style.scss | 39 +++-- src/views/ChartView.vue | 2 - yarn.lock | 30 +++- 24 files changed, 592 insertions(+), 245 deletions(-) create mode 100644 src/chart.js create mode 100644 src/components/chart/Builder.vue create mode 100644 src/components/chart/BuilderPanel.vue delete mode 100644 src/components/chart/ChartTranche.vue create mode 100644 src/components/chart/DCABuilder.vue create mode 100644 src/components/chart/LimitBuilder.vue delete mode 100644 src/components/chart/LimitTranche.vue delete mode 100644 src/components/chart/LineTranche.vue delete mode 100644 src/components/chart/MarketTranche.vue create mode 100644 src/components/chart/Toolbar.vue delete mode 100644 src/components/chart/TranchePanel.vue diff --git a/package.json b/package.json index f2d9627..bb51d2e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@mdi/font": "6.9.96", + "color": "^4.2.3", "core-js": "^3.29.0", "ethers": "^6.7.1", "pinia": "2.1.6", diff --git a/src/blockchain/common.js b/src/blockchain/common.js index b7b3ca3..8df0e03 100644 --- a/src/blockchain/common.js +++ b/src/blockchain/common.js @@ -1,3 +1,23 @@ + +export function mixin(child, ...parents) { + for( const parent of parents ) { + for ( const key in parent) { + if (parent.hasOwnProperty(key)) { + child[key] = parent[key]; + } + } + } + return child; +} + + +export function prototype(parent, child) { + const result = Object.create(parent); + Object.assign(result, child) + return result +} + + export function encodeIEE754(value) { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); diff --git a/src/chart.js b/src/chart.js new file mode 100644 index 0000000..e3177e2 --- /dev/null +++ b/src/chart.js @@ -0,0 +1,203 @@ +import {useChartOrderStore} from "@/orderbuild.js"; +import {prototype} from "@/blockchain/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/", + // debug: true, + autosize: true, + symbol: 'AAPL', + interval: '1D', + container: el, + datafeed: new Datafeeds.UDFCompatibleDatafeed("https://demo-feed-data.tradingview.com"), + locale: "en", + disabled_features: [], + enabled_features: [], + drawings_access: {type: 'white', tools: [],}, // show no tools + }); + widget.subscribe('drawing_event', handleDrawingEvent) + widget.subscribe('onSelectedLineToolChanged', onSelectedLineToolChanged) + widget.onChartReady(initChart) +} + + +function initChart() { + console.log('init chart') + chart = widget.activeChart() + chart.crossHairMoved().subscribe(null, (point)=>{ + crosshairPoint=point + const co = useChartOrderStore() + invoke(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)=>{}, +} + + +export const VerboseCallback = prototype(ShapeCallback, { + onDraw: ()=>{console.log('onDraw')}, + // onRedraw: ()=>{console.log('onRedraw')}, + onUndraw: ()=>{console.log('onUndraw')}, + onAddPoint: ()=>{console.log('onAddPoint')}, + onCreate: (shapeId, points, props)=>{console.log('onCreate')}, + onPoints: (shapeId, points)=>{console.log('onPoints')}, + onProps: (shapeId, props)=>{console.log('onProps')}, + onMove: (shapeId, points)=>{console.log('onMove')}, + onHide: (shapeId, props)=>{console.log('onHide')}, + onShow: (shapeId, props)=>{console.log('onShow')}, + onClick: (shapeId)=>{console.log('onClick')}, + onDelete: (shapeId)=>{console.log('onDelete')}, +}) + + + +export const BuilderUpdateCallback = prototype(ShapeCallback,{ + onCreate(shapeId, points, props) { + this.builder.shapes[this.tag] = shapeId; + }, + onPoints(shapeId, points) { + this.builder.points[this.tag] = points; + }, + onProps(shapeId, props) { + this.builder.props[this.tag] = props; + }, + onMove(shapeId, points) { + this.builder.points[this.tag] = points; + }, +}) + + +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) { + console.log('drawShape', callbacks, shapeType.name, shapeType.code) + const co = useChartOrderStore() + if( co.drawingCallbacks ) + invoke(co.drawingCallbacks, 'onUndraw') + co.drawingCallbacks = callbacks + co.drawing = true + widget.selectLineTool(shapeType.code) + invoke(callbacks, 'onDraw') +} + + +export function cancelDrawing() { + const co = useChartOrderStore() + if (co.drawing) { + invoke(co.drawingCallbacks, 'onUndraw') + co.drawing = false + } +} + + +export function builderUpdater(builder, tag='a') { + return prototype(BuilderUpdateCallback, {builder,tag}) +} + + +export function builderShape(builder, tag, shapeType, ...callbacks) { + const updater = builderUpdater(builder, tag) + console.log('updater', updater) + drawShape(shapeType, updater, ...callbacks) +} + + +export function setPoints( shapeId, points ) { + console.log('setting points', shapeId, points) + widget.activeChart().getShapeById(shapeId).setPoints(points) +} + + +const shapeCallbacks = {} + +function onSelectedLineToolChanged() { + const tool = widget.selectedLineTool(); + 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(); +} + +function handleDrawingEvent(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() + invoke(shapeCallbacks[id], 'onCreate', id, points, props) + invoke(shapeCallbacks[id], 'onPoints', id, points) + invoke(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) + } + } 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) + } + } else if (event === 'remove') { + if (id in shapeCallbacks) { + invoke(shapeCallbacks[id], 'onDelete', id) + } + } else + console.log('unknown drawing event', event) +} diff --git a/src/components/Btn.vue b/src/components/Btn.vue index 74e0c58..db169cc 100644 --- a/src/components/Btn.vue +++ b/src/components/Btn.vue @@ -1,7 +1,9 @@ diff --git a/src/components/Orders.vue b/src/components/Orders.vue index 7ee8baf..d135551 100644 --- a/src/components/Orders.vue +++ b/src/components/Orders.vue @@ -95,7 +95,7 @@ + + \ No newline at end of file diff --git a/src/components/chart/BuilderPanel.vue b/src/components/chart/BuilderPanel.vue new file mode 100644 index 0000000..2be65bf --- /dev/null +++ b/src/components/chart/BuilderPanel.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/src/components/chart/Chart.vue b/src/components/chart/Chart.vue index 5a95c51..8916e47 100644 --- a/src/components/chart/Chart.vue +++ b/src/components/chart/Chart.vue @@ -5,46 +5,19 @@ diff --git a/src/components/chart/ChartTranche.vue b/src/components/chart/ChartTranche.vue deleted file mode 100644 index fff0be3..0000000 --- a/src/components/chart/ChartTranche.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/components/chart/DCABuilder.vue b/src/components/chart/DCABuilder.vue new file mode 100644 index 0000000..06e2e23 --- /dev/null +++ b/src/components/chart/DCABuilder.vue @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/src/components/chart/LimitBuilder.vue b/src/components/chart/LimitBuilder.vue new file mode 100644 index 0000000..4f280a6 --- /dev/null +++ b/src/components/chart/LimitBuilder.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/components/chart/LimitTranche.vue b/src/components/chart/LimitTranche.vue deleted file mode 100644 index db492e1..0000000 --- a/src/components/chart/LimitTranche.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/components/chart/LineTranche.vue b/src/components/chart/LineTranche.vue deleted file mode 100644 index daa5a4c..0000000 --- a/src/components/chart/LineTranche.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/components/chart/MarketTranche.vue b/src/components/chart/MarketTranche.vue deleted file mode 100644 index 482ded0..0000000 --- a/src/components/chart/MarketTranche.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/components/chart/Toolbar.vue b/src/components/chart/Toolbar.vue new file mode 100644 index 0000000..db627e2 --- /dev/null +++ b/src/components/chart/Toolbar.vue @@ -0,0 +1,49 @@ + + + + + \ No newline at end of file diff --git a/src/components/chart/TranchePanel.vue b/src/components/chart/TranchePanel.vue deleted file mode 100644 index 7b95eeb..0000000 --- a/src/components/chart/TranchePanel.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/layouts/chart/ChartLayout.vue b/src/layouts/chart/ChartLayout.vue index 27248d7..fc539d3 100644 --- a/src/layouts/chart/ChartLayout.vue +++ b/src/layouts/chart/ChartLayout.vue @@ -1,8 +1,6 @@ @@ -11,6 +9,5 @@ import NavDrawer from "@/components/NavDrawer.vue"; import Footer from "@/components/Footer.vue"; import {useStore} from "@/store/store.js"; - const s = useStore() diff --git a/src/orderbuild.js b/src/orderbuild.js index f3524b7..4550029 100644 --- a/src/orderbuild.js +++ b/src/orderbuild.js @@ -2,10 +2,53 @@ import {routeInverted, timestamp} from "@/misc.js"; import {MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js"; import {useOrderStore} from "@/store/store.js"; import {encodeIEE754} from "@/blockchain/common.js"; +import {defineStore} from "pinia"; +import {computed, ref} from "vue"; -export function applyLimit(tranche, price=null, isMinimum=null) { - if( price === null ) { +function unimplemented() { throw Error('Unimplemented') } + +// Builders are data objects which store a configuration state +function TrancheBuilder( component, options = {}) { + const id = 'builder-'+Date.now(); + return {id, component, options, points: {}, shapes: {}, props: {}, build: unimplemented} +} + + +export const useChartOrderStore = defineStore('chart_orders', () => { + const builderIdList = ref([]) // this is where we keep the UI ordering + const builderList = computed(()=>{ + console.log('builder list', builderIdList.value.map((id)=>builderDict.value[id])) + return builderIdList.value.map((id)=>builderDict.value[id]) + }) // convenience + const builderDict = ref({}) // builders stored by id + + const drawing = ref(false) + const drawingCallbacks = ref(null) // only during draw mode + + function addBuilder(component, options={}) { + const b = TrancheBuilder(component,options) + builderIdList.value.push(b.id) + builderDict.value[b.id] = b + } + + function touchBuilder(builder) { + // noinspection SillyAssignmentJS + builderIdList.value = builderIdList.value + builderDict.value[builder.id] = builder + } + + function removeBuilder(builder) { + builderIdList.value = builderIdList.value.filter((v)=>v!==builder.id) + delete builderDict.value[builder.id] + } + + return { builderList, builderDict, drawing, drawingCallbacks, addBuilder, removeBuilder, touchBuilder } +}) + + +export function applyLimit(tranche, price = null, isMinimum = null) { + if (price === null) { const os = useOrderStore() price = os.limitPrice if (!price) @@ -29,27 +72,26 @@ function computeInterceptSlope(time0, price0, time1, price1) { } -export function linePointsValue(time0, price0, time1, price1, unixTime=null) { - if(unixTime===null) +export function linePointsValue(time0, price0, time1, price1, unixTime = null) { + if (unixTime === null) unixTime = timestamp() try { const [intercept, slope] = computeInterceptSlope(time0, price0, time1, price1) return intercept + unixTime * slope - } - catch (e) { - console.log('error computing line',e) + } catch (e) { + console.log('error computing line', e) return null } } -export function applyLinePoints(tranche, time0, price0, time1, price1, isMinimum=null) { +export function applyLinePoints(tranche, time0, price0, time1, price1, isMinimum = null) { const [intercept, slope] = computeInterceptSlope(time0, price0, time1, price1); applyLine(tranche, intercept, slope, isMinimum) } -export function applyLine(tranche, intercept, slope, isMinimum=null) { +export function applyLine(tranche, intercept, slope, isMinimum = null) { console.log('intercept, slope', intercept, slope) // intercept and slope are still in "human" units of decimal-adjusted prices const os = useOrderStore() @@ -62,7 +104,7 @@ export function applyLine(tranche, intercept, slope, isMinimum=null) { console.log('inverted b, m, cur', inverted, b, m, cur) m = encodeIEE754(m) b = encodeIEE754(b) - if( isMinimum === null ) + if (isMinimum === null) isMinimum = os.limitIsMinimum console.log('limit is minimum', isMinimum) if (isMinimum) { @@ -83,8 +125,8 @@ export function timesliceTranches() { const timeUnitIndex = os.timeUnitIndex; let duration = timeUnitIndex === 0 ? interval * 60 : // minutes - timeUnitIndex === 1 ? interval * 60 * 60 : // hours - interval * 24 * 60 * 60; // days + timeUnitIndex === 1 ? interval * 60 * 60 : // hours + interval * 24 * 60 * 60; // days let window if (!os.intervalIsTotal) { window = duration @@ -92,7 +134,7 @@ export function timesliceTranches() { } else { window = Math.round(duration / n) } - if( window < 60 ) + if (window < 60) window = 60 // always allow at least one minute for execution const amtPerTranche = Math.ceil(MAX_FRACTION / n) duration -= 15 // subtract 15 seconds so the last tranche completes before the deadline @@ -109,3 +151,4 @@ export function timesliceTranches() { } return ts } + diff --git a/src/store/store.js b/src/store/store.js index df5ccc1..7ac7f93 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,7 +1,7 @@ // Utilities import {defineStore} from 'pinia' import {knownTokens} from "@/knownTokens.js"; -import {computed, reactive, ref} from "vue"; +import {computed, ref} from "vue"; // USING THE STORE: // @@ -10,7 +10,7 @@ import {computed, reactive, ref} from "vue"; // // defineStore('foostore', ()=>{ // const foo = ref(true) -// const obj = reactive({}) +// const obj = ref({}) // function reset() { // obj.value = {} // foo.value = false @@ -36,11 +36,6 @@ console.log('version', version) export const useStore = defineStore('app', ()=> { const clock = ref(timestamp()) - setInterval(()=>{ - clock.value= timestamp() - console.log('triggered clock', clock.value) - }, 10*1000) - const nav = ref(false) // controls opening navigation drawer const _chainId = ref(null) @@ -121,6 +116,8 @@ export const useStore = defineStore('app', ()=> { this.extraTokens = extras } + setInterval(()=>clock.value=timestamp(), 10*1000) + return { nav, chainId, chainInfo, chain, provider, vaultInitCodeHash, account, vaults, transactionSenders, errors, extraTokens, poolPrices, vaultBalances, orders, vault, tokens, factory, helper, mockenv, mockCoins, @@ -190,15 +187,3 @@ export const usePrefStore = defineStore('prefs', ()=> { return {inverted,} }) -export const useShapeStore = defineStore('shapes', ()=> { - const shapes = reactive([]) - - function shape(id) { - for (const s of shapes) { - if (s.id === id) - return s - } - return null - } - return {shapes, shape} -}) diff --git a/src/styles/style.scss b/src/styles/style.scss index cdf874b..156252a 100644 --- a/src/styles/style.scss +++ b/src/styles/style.scss @@ -1,6 +1,12 @@ @use "/src/styles/vars" as v; #app { + .logo { + font-family: v.$heading-font-family; + font-weight: 500; + font-size: 24px; + } + .clickable { :hover { cursor: pointer; @@ -25,26 +31,27 @@ font-family: v.$heading-font-family; } - input { - text-align: center; - } - - .v-field__input { - justify-content: center; - } - - .v-text-field.text-end input { - text-align: end; - } - - .v-input { - margin-top: 1em; - margin-bottom: 1em; - } + //input { + // text-align: center; + //} + // + //.v-field__input { + // justify-content: center; + //} + // + //.v-text-field.text-end input { + // text-align: end; + //} + // + //.v-input { + // margin-top: 1em; + // margin-bottom: 1em; + //} .maxw { max-width: v.$card-maxw; } + } .uniswap-color { diff --git a/src/views/ChartView.vue b/src/views/ChartView.vue index 141eed4..ee6bb57 100644 --- a/src/views/ChartView.vue +++ b/src/views/ChartView.vue @@ -12,9 +12,7 @@