massive Shape rework: keep both tvPoints/Props and ourPoints/Props; delegate model updates to subclasses; DCA/VLine working but Ladder/HLine not done.

This commit is contained in:
Tim
2024-04-16 16:25:31 -04:00
parent 8befffe1c5
commit 65be28fb0a
11 changed files with 874 additions and 208 deletions

View File

@@ -1,6 +1,7 @@
import {useChartOrderStore} from "@/orderbuild.js";
import {invokeCallbacks, prototype} from "@/common.js";
import {DataFeed, initFeeDropdown, lookupSymbol} from "@/charts/datafeed.js";
import {intervalToSeconds} from "@/misc.js";
export let widget = null
export let chart = null
@@ -24,6 +25,11 @@ function changeSymbol(symbol) {
}
function changeInterval(interval, _timeframe) {
useChartOrderStore().intervalSecs = intervalToSeconds(interval)
}
/* TradingView event keystrings
const subscribeEvents = [
'toggle_sidebar', 'indicators_dialog', 'toggle_header', 'edit_object_dialog', 'chart_load_requested',
@@ -69,8 +75,10 @@ function initChart() {
console.log('init chart')
chart = widget.activeChart()
chart.crossHairMoved().subscribe(null, (point)=>setTimeout(()=>handleCrosshairMovement(point),0) )
chart.onSymbolChanged().subscribe(null, changeSymbol);
chart.onSymbolChanged().subscribe(null, changeSymbol)
chart.onIntervalChanged().subscribe(null, changeInterval)
changeSymbol(chart.symbolExt())
changeInterval(widget.symbolInterval().interval)
useChartOrderStore().chartReady = true
console.log('chart ready')
}
@@ -242,7 +250,6 @@ function drawingEventWorker() {
const queue = drawingEventQueue
drawingEventQueue = []
propsEvents = {}
// console.log('events locked', queue)
for (const [id, event] of queue) {
if (event === 'properties_changed')
propsEvents[id] = event
@@ -259,8 +266,18 @@ function drawingEventWorker() {
}
function doHandleDrawingEvent(id, event) {
// console.log('drawing event', id, event)
const shape = event === 'remove' ? null : chart.getShapeById(id);
console.log('drawing event', id, event)
let shape
if (event==='remove')
shape = null
else {
try {
shape = chart.getShapeById(id)
}
catch {
return
}
}
if (event === 'create') {
const co = useChartOrderStore();
const callbacks = co.drawingCallbacks

View File

@@ -3,6 +3,8 @@
import {invokeCallback, mixin} from "@/common.js";
import {chart, createShape, deleteShapeId, dragging, draggingShapeIds, drawShape, widget} from "@/charts/chart.js";
import {unique} from "@/misc.js";
import {useChartOrderStore} from "@/orderbuild.js";
import model from "color";
//
@@ -37,34 +39,96 @@ export const ShapeType = {
}
class Shape {
constructor(type, model={}, onModel=null, onDelete=null, props=null) {
export class Shape {
constructor(type, onModel=null, onDelete=null, props=null) {
// the Shape object manages synchronizing internal data with a corresponding TradingView shape
// each shape in the class hierarchy overrides setModel() to cause effects in TradingView
// TV callbacks are returned to the on*() handlers, primarily onPoints() and onProps(). Handlers
// of TV callbacks should call updateModel() with just the kv's they want to change. Any changes
// are then passed onto the onModel() callback.
// the model object has various attributes defined by each shape subclass. each subclass's constructor
// must call setModel(model) after first calling super.constructor() and optionally declaring any model
// defaults on this.model.
// Shape: {
// lineColor, textColor, color, // lineColor and textColor default to color
// }
this.debug = false
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
this.points = this.pointsFromModel()
// console.log('construct points', this.points)
this.props = props === null ? this.propsFromModel() : mixin(props, this.propsFromModel())
this.model = {} // set to nothing at first
this.ourPoints = null
this.tvPoints = null
this.ourProps = {}
if (props !== null)
this.ourProps = mixin(props, this.ourProps)
this.tvProps = null
if (onModel !== null)
this.onModel = onModel
if (onDelete !== null )
this.onDelete = onDelete
this.create()
// Model values handled by this base class
this.model.color = null
this.model.lineColor = null
this.model.textColor = null
}
//
// primary interface methods
//
setModel(model) {
if (model.textColor || model.lineColor) {
if (model.textColor)
this.model.textColor = model.textColor
if (model.lineColor)
this.model.lineColor = model.lineColor
this.setProps(this.colorProps())
}
// todo text
}
onModel(model, changedKeys) {} // model was changed by a TradingView user action
updateModel(changes) {
if (this.debug) console.log('updateModel', this.id, changes)
const changedKeys = []
for (const k of Object.keys(changes)) {
if( changes[k] !== undefined && changes[k] !== this.model[k] ) {
changedKeys.push(k)
this.model[k] = changes[k]
}
}
if (changedKeys.length)
this.onModel(this.model, changedKeys)
}
colorProps() {
if (!this.model.color&&!this.model.lineColor&&!this.model.textColor) return null
const o = {}
const p = this.type.drawingProp
const lc = this.model.lineColor ? this.model.lineColor : this.model.color;
const tc = this.model.textColor ? this.model.textColor : this.model.color;
if (lc)
o[p+".linecolor"] = lc
if (tc)
o[p+".textcolor"] = tc
return o
}
draw() {
// have the user draw this shape
console.log(`draw ${this.type.name}`, this.model)
if (this.debug) console.log(`draw ${this.type.name}`, this.model)
if (this.id)
throw Error(`Shape already exists ${this.id}`)
const or = this.drawingOverrides();
// console.log('drawing overrides', or)
// if (this.debug) console.log('drawing overrides', or)
widget.applyOverrides(or)
drawShape(this.type, new ShapeTVCallbacks(this))
}
@@ -72,30 +136,28 @@ class Shape {
// return an object with property defaults, e.g. { "linetoolhorzline.linecolor": "#7f11e0" }
// https://www.tradingview.com/charting-library-docs/latest/api/modules/Charting_Library#drawingoverrides
drawingOverrides() {
if (this.model.color===null) return null
const o = {}
const p = this.type.drawingProp
o[p+".linecolor"] = this.model.color
o[p+".textcolor"] = this.model.color
return o
return this.colorProps()
}
create() {
if (this.id !== null) return
// programatically create the shape using the current this.points
if( this.points && this.points.length ) {
if( this.ourPoints && this.ourPoints.length ) {
this.doCreate()
}
}
doCreate() {
createShape(this.type, this.points, {overrides:this.props}, new ShapeTVCallbacks(this))
// createShape(this.type, this.points, {overrides:this.props}, new ShapeTVCallbacks(this))
this.tvPoints = [...this.ourPoints]
this.id = createShape(this.type, this.ourPoints, {overrides:this.ourProps}, new ShapeTVCallbacks(this))
if (this.debug) console.log('created', this.type.name, this.ourPoints, this.id)
}
createOrDraw() {
if(this.id) return
if(this.points && this.points.length)
this.doCreate(this.points)
if(this.ourPoints && this.ourPoints.length)
this.doCreate(this.ourPoints)
else
this.draw()
}
@@ -106,36 +168,25 @@ class Shape {
}
setModel(model, props=null) {
for( const [k,v] of Object.entries(model))
this.model[k] = v
this.setPointsIfDirty(this.pointsFromModel());
let p
const mp = this.propsFromModel();
if (props===null) {
p = mp
if (p===null)
return
}
else if (mp !== null)
p = mixin(props, mp)
this.setPropsIfDirty(p)
}
setPoints(points) {
this.points = points
// setting points to null will delete the shape from the chart. setting points to a valid value will cause the
// shape to be drawn.
if (this.debug) console.log('setPoints', points)
this.ourPoints = points
if (points === null || !points.length)
this.delete()
else {
if (this.id === null)
this.doCreate()
else {
else if (dirtyPoints(this.tvPoints, points)) {
const s = this.tvShape();
if (!dragging) {
if (this.debug) console.log('adjusting tv points', s, this.tvPoints, points)
if (!this.beingDragged()) {
if (this.debug) console.log('not dragging. use setPoints.')
s.setPoints(points)
}
else {
if (this.debug) console.log('dragging. use QUIET setPoints.')
// quiet setPoints doesn't disturb tool editing mode
const i = s._pointsConverter.apiPointsToDataSource(points)
// s._model.startChangingLinetool(this._source)
@@ -147,67 +198,39 @@ class Shape {
}
}
setPointsIfDirty(points) {
if (dirtyPoints(this.points, points))
this.setPoints(points)
}
onPoints(points) {} // the control points of an existing shape were changed
setProps(props) {
if(this.id)
this.tvShape().setProperties(props)
if (!props || Object.keys(props).length===0) return
if (this.debug) console.log('setProps', props)
this.ourProps = mixin(props, this.ourProps)
if(this.id) {
const p = dirtyItems(this.tvProps, props)
this.tvProps = this.tvProps === null ? p : mixin(p, this.tvProps)
this.tvShape().setProperties(p)
}
}
setPropsIfDirty(props) {
// console.log('dirtyProps', this.props, props, dirtyProps(this.props, props))
if( dirtyProps(this.props, props) )
this.setProps(props)
onProps(props) { // the display properties of an existing shape were changed
this.updateModel({lineColor:props.linecolor, textColor:props.textcolor})
}
beingDragged() {
return draggingShapeIds.indexOf(this.id) !== -1
}
delete() {
// console.log('shape.delete', this.id)
if (this.debug) console.log('shape.delete', this.id)
this.ourPoints = null
if (this.id === null) return
this.lock++
try {
deleteShapeId(this.id)
this.id = null
} finally {
this.lock--
}
deleteShapeId(this.id)
this.id = null
}
//
// 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}
propsToModel() {} // set the model using this.props
onModel(model) {} // called whenever points or props updates the model dictionary
//
// Overridable shape callbacks
// Overridable shape callbacks initiated by TradingView
//
onDraw() {} // start drawing a new shape for the builder
@@ -215,8 +238,6 @@ class Shape {
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
onDrag(points) {}
onHide(props) {}
@@ -226,7 +247,6 @@ class Shape {
}
function dirtyPoints(pointsA, pointsB) {
if (pointsB===null)
return pointsA !== null
@@ -243,21 +263,6 @@ function dirtyPoints(pointsA, pointsB) {
return false
}
// B is modifying A
function dirtyProps(propsA, propsB) {
if (propsB===null)
return propsA !== null
const entries = Object.entries(propsB);
if (propsA===null)
return entries.length > 0
for( const [k,v] of entries)
if ( !(k in propsA) || propsA[k] !== v )
return true
return false
}
// B is modifying A
function dirtyKeys(propsA, propsB) {
if (propsB===null)
@@ -265,7 +270,15 @@ function dirtyKeys(propsA, propsB) {
if (propsA===null)
return [...Object.keys(propsB)]
const keys = unique([...Object.keys(propsA), ...Object.keys(propsB)])
return keys.filter((k)=> !(k in propsA) || propsA[k] !== propsB[k])
return keys.filter((k)=> !(k in propsA) || propsA[k] !== undefined && propsA[k] !== propsB[k])
}
function dirtyItems(a, b) {
const result = {}
for (const k of dirtyKeys(this.tvProps, props))
result[k] = props[k]
return result
}
@@ -278,113 +291,173 @@ class ShapeTVCallbacks {
}
onCreate(shapeId, _tvShape, points, props) {
if( this.shape.id )
throw Error(`Created a shape ${shapeId} where one already existed ${this.shape.id}`)
this.shape.id = shapeId
if( this.shape.lock ) return
this.creating = true
invokeCallback(this.shape, 'onCreate', points, props)
}
onPoints(shapeId, _tvShape, points) {
this.shape.points = points
if (this.shape.debug) console.log('tvcb onPoints', points)
this.shape.tvPoints = points
this.shape.onPoints(points)
this.shape.onDirtyModel(this.shape.pointsToModel)
}
onProps(shapeId, _tvShape, props) {
// console.log('onProps', props)
if (this.creating) {
// if (this.shape.debug) console.log('tvOnProps', props)
if (this.creating) { // todo still useful?
this.creating = false
return
}
this.shape.props = props
this.shape.tvProps = props
this.shape.onProps(props)
this.shape.onDirtyModel(this.shape.propsToModel)
}
onDraw() {
if( this.shape.lock ) return
invokeCallback(this.shape, 'onDraw')
}
onRedraw() {
if( this.shape.lock ) return
invokeCallback(this.shape, 'onRedraw')
}
onUndraw() {
if( this.shape.lock ) return
invokeCallback(this.shape, 'onUndraw')
}
onAddPoint() {
if( this.shape.lock ) return
invokeCallback(this.shape, 'onAddPoint')
}
onMove(_shapeId, _tvShape, points) {
if( this.shape.lock ) return
invokeCallback(this.shape, 'onMove',points)
}
onDrag(_shapeId, _tvShape, points) {
if( this.shape.lock ) return
if (this.shape.debug) console.log('onDrag')
invokeCallback(this.shape, 'onDrag', points)
}
onHide(_shapeId, _tvShape, props) {
if( this.shape.lock ) return
invokeCallback(this.shape, 'onHide',props)
}
onShow(_shapeId, _tvShape, props) {
if( this.shape.lock ) return
invokeCallback(this.shape, 'onShow',props)
}
onClick(_shapeId, _tvShape) {
if( this.shape.lock ) return
invokeCallback(this.shape, 'onClick')
}
onDelete(shapeId) {
this.shape.id = null
if( this.shape.lock ) return
invokeCallback(this.shape, 'onDelete', shapeId)
}
}
export class HLine extends Shape {
export class Line extends Shape {
onDrag(points) {
const s = this.tvShape();
if (this.debug) {
console.log('shape', s)
console.log('currentMovingPoint', s._source.currentMovingPoint())
console.log('startMovingPoint', s._source.startMovingPoint())
console.log('isBeingEdited', s._source.isBeingEdited())
console.log('state', s._source.state())
}
}
}
export class HLine extends Line {
constructor(model, onModel=null, onDelete=null, props=null) {
super(ShapeType.HLine, model, onModel, onDelete, props)
}
pointsFromModel() {
if (this.model.price === null) return null
const time = this.points !== null && this.points.length > 0 ? this.points[0].time : 0
return [{time:time, price:this.model.price}]
setModel(model) {
if (model.price !== undefined && model.price !== this.model.price) {
this.model.price = model.price
this.setPoints([{time:0,price:this.model.price}])
}
}
pointsToModel() {
this.model.price = this.points[0].price
onPoints(points) {
super.onPoints(points);
}
propsFromModel() {
return this.model.color ? {linecolor: this.model.color} : null
pointsFromModel(model) {
if (model.price === null || model.price===undefined) return null
// take any time available, or 0
const time =
this.ourPoints !== null && this.ourPoints.length > 0 ? this.ourPoints[0].time :
this.tvPoints !== null && this.tvPoints.length > 0 ? this.tvPoints[0].time : 0
return [{time:time, price:model.price}]
}
propsToModel() {this.model.color=this.props.linecolor}
onDrag(points) {
const s = this.tvShape();
console.log('shape', s)
console.log('currentMovingPoint', s._source.currentMovingPoint())
console.log('startMovingPoint', s._source.startMovingPoint())
console.log('isBeingEdited', s._source.isBeingEdited())
console.log('state', s._source.state())
pointsToModel(points) {
return {price: this.ourPoints[0].price}
}
}
function timeAdjustmentTooSmall(orig, newValue) {
// TradingView adjusts our lines to be at the start of the intervals, so
// we ignore deltas smaller than one interval prior
return newValue === undefined ||
orig !== null && orig !== undefined && newValue !== null &&
newValue < orig && orig - newValue < useChartOrderStore().intervalSecs
}
function ohlcStart(time) {
const period = useChartOrderStore().intervalSecs
return Math.floor(time/period) * period
}
export class VLine extends Line {
constructor(model, onModel=null, onDelete=null, props=null) {
super(ShapeType.VLine, onModel, onDelete, props)
// Model
this.model.time = null
this.setModel(model) // call setModel at the end
}
onPoints(points) {
if (this.debug) console.log('vline onPoints', this.ourPoints, points)
super.onPoints(points);
const orig = this.ourPoints && this.ourPoints.length ? this.ourPoints[0].time : null
if (!timeAdjustmentTooSmall(orig, points[0].time)) {
if (this.debug) console.log('updateModel', points[0].time)
this.updateModel({time: points[0].time})
}
}
setModel(model) {
if (this.debug) console.log('vline setModel', this.model.time, model )
super.setModel(model)
if (model.time !== undefined && model.time !== this.model.time) {
this.model.time = model.time
const time = ohlcStart(model.time);
if (this.debug) console.log('vline setPoints', this.id, time)
this.setPoints([{time, price:1}])
}
}
delete() {
this.model.time = null
super.delete()
}
dirtyPoints(pointsA, pointsB) {
const a = pointsA ? pointsA[0].time : null
const b = pointsB ? pointsB[0].time : null
const result = !timeAdjustmentTooSmall(a, b)
if (this.debug) console.log('vline dirty points?', a, b, result)
return result
}
}

View File

@@ -3,7 +3,7 @@ export function mixin(child, ...parents) {
// assigned by parents order, highest priority first
for( const parent of parents ) {
for ( const key in parent) {
if (parent.hasOwnProperty(key) && !child.hasOwnProperty(key)) {
if (parent.hasOwnProperty(key) && !child.hasOwnProperty(key) && parent[key] !== undefined) {
child[key] = parent[key];
}
}
@@ -11,7 +11,6 @@ export function mixin(child, ...parents) {
return child;
}
export function prototype(parent, child) {
const result = Object.create(parent);
Object.assign(result, child)

View File

@@ -1,28 +1,65 @@
<template>
<div :key="builder.id">
<v-card-text :color="titleColor">
<slot name="title"><span>Unimplemented</span></slot>
</v-card-text>
<v-card-text>
<slot name="text">
Unimplemented builder panel
</slot>
<div><v-btn variant="tonal" color="error" @click="co.removeBuilder(builder)" prepend-icon="mdi-delete">DELETE</v-btn></div>
</v-card-text>
</div>
<row-bar :color="builder.color">
<color-band :color="builder.color"/>
<slot/>
<div class="align-self-center">
<v-menu>
<template v-slot:activator="{ props }">
<v-btn variant="plain" v-bind="props" icon="mdi-dots-vertical"/>
</template>
<v-list>
<!-- <v-list-subheader :title="'Limit '+ (lineAPrice?lineAPrice.toPrecision(5):'')"/>-->
<v-list-item title="Delete" key="withdraw" value="withdraw" prepend-icon="mdi-delete" color="red"
@click="deleteMyBuilder"/>
</v-list>
</v-menu>
</div>
</row-bar>
</template>
<script setup>
import {useChartOrderStore} from "@/orderbuild.js";
import {computed} from "vue";
import {builderFuncs, deleteBuilder, useChartOrderStore} from "@/orderbuild.js";
import ColorBand from "@/components/chart/ColorBand.vue";
import RowBar from "@/components/chart/RowBar.vue";
import {onBeforeUnmount, onMounted, onUnmounted, onUpdated, watchEffect} from "vue";
const props = defineProps(['builder', 'color', 'colorTag'])
const props = defineProps({
order: Object,
builder: Object,
buildTranches: {type: Function},
adjustShapes: {type: Function, default: null},
deleteShapes: {type: Function, default: null},
})
const emit = defineEmits(['update:builder'])
const co = useChartOrderStore()
const titleColor = computed(()=>props.color ? props.color : props.colorTag ? props.builder.props[props.colorTag]?.linecolor : props.builder.props.a.linecolor )
let lastId = props.builder.id
builderFuncs[props.builder.id] = props.buildTranches
onUpdated(()=>{
if (lastId !== props.builder.id ) {
delete builderFuncs[lastId]
builderFuncs[props.builder.id] = props.buildTranches
lastId = props.builder.id
}
})
onUnmounted(() => delete builderFuncs[lastId])
if (props.adjustShapes) {
watchEffect(props.adjustShapes)
onMounted(props.adjustShapes)
}
if (props.deleteShapes)
onBeforeUnmount(props.deleteShapes)
function deleteMyBuilder() {
deleteBuilder(props.order, props.builder);
}
</script>
<style scoped lang="scss">
</style>
</style>

View File

@@ -30,7 +30,7 @@
<div class="my-3">
<div v-if="order.builders.length===0"> <!--todo remove gralpha limitation of one builder-->
<span :style="colorStyle" class="ma-3">Add condition:</span>
<!-- <v-btn variant="flat" prepend-icon="mdi-clock-outline" @click="build('DCABuilder')">DCA</v-btn>-->
<v-btn :color="color" variant="text" prepend-icon="mdi-clock-outline" @click="build(order,'DCABuilder')">DCA</v-btn>
<v-btn :color="color" variant="text" prepend-icon="mdi-ray-vertex" @click="build(order,'LimitBuilder')">Limit</v-btn>
<!-- <v-btn variant="flat" prepend-icon="mdi-vector-line">Line</v-btn>-->
<!--

View File

@@ -1,33 +1,158 @@
<template>
<builder-panel :builder="builder">
<template v-slot:title>
<span>DCA</span>
<span v-if="!builder.points">Draw your timeframe on the chart!</span>
</template>
<template v-slot:text>
<input type="number" min="1" max="2">
</template>
</builder-panel>
<rung-builder name='DCA' :order="order" :builder="builder"
v-model:value-a="absTimeA" v-model:value-b="absTimeB" :mode="1" :shape="VLine"
:get-model-value="getModelValue" :set-model-value="setModelValue" :set-values="setValues"
:std-width="stdWidth" :build-tranches="buildTranches">
<v-list style="background-color: inherit">
<v-list-item v-for="t in absoluteTimes">{{t}}</v-list-item>
</v-list>
</rung-builder>
</template>
<script setup>
import BuilderPanel from "@/components/chart/BuilderPanel.vue";
import {drawShape, ShapeCallback, VerboseCallback} from "@/charts/chart.js";
import {prototype} from "@/common.js";
import {ShapeType} from "@/charts/shape.js";
import {builderDefaults, MIN_EXECUTION_TIME, useChartOrderStore} from "@/orderbuild.js";
import {VLine} from "@/charts/shape.js";
import {sideColor} from "@/misc.js";
import {useTheme} from "vuetify";
import {useOrderStore, useStore} from "@/store/store.js";
import {DISTANT_FUTURE, MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js";
import RungBuilder from "@/components/chart/RungBuilder.vue";
import {computed, ref} from "vue";
const props = defineProps(['builder'])
const s = useStore()
const os = useOrderStore()
const co = useChartOrderStore()
const theme = useTheme().current
const props = defineProps(['order', 'builder'])
const DCAArtist = prototype(VerboseCallback, {
onDraw(widget, chart) { console.log('dca start draw') },
onCreate(widget, chart) { console.log('dca start draw') },
const stdWidth = computed(()=>10 * co.intervalSecs)
function computeDefaultColor() {
const index = props.order.builders.indexOf(props.builder)
return sideColor(props.order.buy, index)
}
const defaultColor = computeDefaultColor()
// Fields must be defined in order to be reactive
builderDefaults(props.builder, {
timeA: s.clock, // todo 0
timeB: null,
// relative: true,
relative: false, // todo
rungs: 1,
skew: 0,
color: defaultColor,
})
drawShape(ShapeType.VLine, prototype(DCAArtist, {}))
const rawTimes = ref([])
const times = computed(()=>rawTimes.value.map((t)=>Math.round(t)))
const endTimes = computed(()=>{
if (props.builder.rungs === 1)
return DISTANT_FUTURE
const ts = times.value
const window = Math.max(MIN_EXECUTION_TIME, Math.floor((ts[ts.length-1]-ts[0])/props.builder.rungs))
return ts.map((t)=>t+window)
})
const absoluteTimes = computed(()=>{
console.log('absoluteTimes', props.builder.relative, times.value)
if (!props.builder.relative)
return times.value
const now = s.clock
return times.value.map((t)=>now+t)
})
const absTimeA = computed({
get() {
let result = props.builder.timeA
if (props.builder.relative)
result += s.clock
console.log('absTimeA', result)
return result
},
set(v) {
console.log('set absTimeA', props.builder.timeA, v)
if (props.builder.relative)
v -= s.clock
props.builder.timeA = v
console.log('absTimeA=',props.builder.timeA)
}
})
const absTimeB = computed({
get() {
let result = props.builder.timeB
if (props.builder.relative)
result += s.clock
console.log('absTimeB', result)
return result
},
set(v) {
console.log('set absTimeB', props.builder.timeB, v)
if ( v !== null && props.builder.relative )
v -= s.clock
props.builder.timeB = v
console.log('absTimeB=',props.builder.timeB)
}
})
function buildTranches() {
const order = props.order
const builder = props.builder
const tranches = []
console.log('buildTranches', builder, order, tranches)
const ts = times.value
const ets = endTimes.value
const ws = weights.value
for(let i=0; i<ts.length; i++) {
const t = newTranche({
fraction: ws[i] * MAX_FRACTION,
startTime: ts[i],
endTime: ets[i],
})
tranches.push(t)
}
return tranches
}
function getModelValue(model) {
if(!model) {
console.log('getModelValue', model)
return null
}
return model.time
}
function setModelValue(model, value) {
console.log('DCA set model value', model, value)
const v = value === null ? null : props.builder.relative ? s.clock + Math.round(value) : Math.round(value)
if (model.time !== v) {
console.log('DCA do set time', v)
model.time = v
}
}
function setValues(values) {
if (!props.builder.relative)
rawTimes.value = values
else {
const now = s.clock
const vs = []
for (let i = 0; i < values.length; i++)
vs.push(values[i] - now)
rawTimes.value = vs
}
}
function valueFromPoints(points) {
const result = points[0].time;
console.log('valueFromPoints', points, result);
return result
}
</script>
<style scoped lang="scss">
</style>
</style>

View File

@@ -75,10 +75,10 @@
<script setup>
import {computed, onBeforeUnmount, onMounted, onUnmounted, onUpdated, watch, watchEffect} from "vue";
import {cancelDrawing, chart} from "@/charts/chart.js";
import {applyLine2, builderFuncs, useChartOrderStore} from "@/orderbuild.js";
import {applyLine2, builderDefaults, builderFuncs, useChartOrderStore} from "@/orderbuild.js";
import Color from "color";
import {HLine} from "@/charts/shape.js";
import {builderDefaults, lightenColor2, lineColor} from "@/misc.js";
import {lightenColor2, sideColor} from "@/misc.js";
import {useTheme} from "vuetify";
import {useOrderStore} from "@/store/store.js";
import RowBar from "@/components/chart/RowBar.vue";
@@ -93,15 +93,13 @@ const emit = defineEmits(['update:builder'])
function computeDefaultColor() {
const index = props.order.builders.indexOf(props.builder)
return lineColor(props.order.buy, index)
return sideColor(props.order.buy, index)
}
const defaultColor = computeDefaultColor()
// Fields must be defined in order to be reactive
builderDefaults(props, emit, {
valid: false,
allocation: 1.0,
builderDefaults(props.builder, {
start: null, // todo
end: null, // todo
priceA: null,

View File

@@ -18,11 +18,8 @@
</template>
<script setup>
import {builderFuncs, useChartOrderStore} from "@/orderbuild.js";
import {builderDefaults} from "@/misc.js";
import {useOrderStore} from "@/store/store.js";
import {builderDefaults, builderFuncs, useChartOrderStore} from "@/orderbuild.js";
import {computed, onMounted, onUnmounted} from "vue";
import RowBar from "@/components/chart/RowBar.vue";
import {newTranche} from "@/blockchain/orderlib.js";
const co = useChartOrderStore()
@@ -30,7 +27,8 @@ const props = defineProps(['order', 'builder'])
const emit = defineEmits(['update:builder'])
// Fields must be defined in order to be reactive
builderDefaults(props, emit, {valid: true, slippage: 0.10,})
builderDefaults(props.builder, {valid: true, slippage: 0.10,})
const slippage = computed({
get() {console.log('slip',props.builder,props.builder.slippage); return props.builder.slippage},
set(v) {props.builder.slippage=v; emit('update:builder', props.builder)}

View File

@@ -0,0 +1,352 @@
<template>
<builder-panel :order="order" :builder="builder" :build-tranches="buildTranches"
:adjust-shapes="adjustShapes" :delete-shapes="deleteShapes">
<div style="min-width: 3em; font-size: larger" :style="colorStyle" class="align-self-start ml-2 pt-3">{{ name }}
</div>
<div>
<v-text-field type="number" v-model="rungs"
density="compact" hide-details class="mx-1 my-2" variant="outlined"
label="Rungs"
:color="color" :base-color="color" min="1"
:disabled="valueA===null"
style="width: 4.5em;"
/>
</div>
<slot/>
<div v-if="co.drawing" class="d-flex align-center pl-3">
<v-icon icon="mdi-chat-alert-outline" color="grey" class="mr-1"/>
Click the chart!
</div>
<div v-if="rungs>1" class="mx-2 d-flex align-center">
<v-slider v-if="rungs>1" direction="vertical" min="-100" max="100" v-model="skew100"
class="no-slider-bg ml-2 mr-4" hide-details/>
<v-text-field type="number" v-model="skew100" min="-100" max="100"
density="compact" hide-details variant="outlined" label="Skew" step="5"
:color="color" :base-color="color">
<template v-slot:prepend>
<v-btn icon="mdi-scale-balance" variant="plain" @click="builder.skew=0" :color="color"/>
</template>
</v-text-field>
</div>
</builder-panel>
</template>
<script setup>
import BuilderPanel from "@/components/chart/BuilderPanel.vue";
import {useOrderStore} from "@/store/store.js";
import {allocationText, deleteBuilder, linearWeights, useChartOrderStore, weightColors} from "@/orderbuild.js";
import {useTheme} from "vuetify";
import {linspace, sideColor} from "@/misc.js";
import {computed, watchEffect} from "vue";
import Color from "color";
import {cancelDrawing} from "@/charts/chart.js";
const os = useOrderStore()
const co = useChartOrderStore()
const theme = useTheme().current
const valueA = defineModel('valueA')
const valueB = defineModel('valueB')
const props = defineProps({
name: String,
order: Object,
builder: Object,
buildTranches: Function,
stdWidth: Number,
shape: Function, // shape() -> Shape
mode: { type: Number, default: 0 }, // rung addition mode: 0 = split, 1 = extend
getModelValue: Function, // getModelValue(model) -> value
setModelValue: Function, // setModelValue(model,value) -> void
setValues: Function, // setValues(values:Array) -> void
})
const skew100 = computed( {
get() {return props.builder.skew*100},
set(v) {props.builder.skew = v/100; }
} )
// validity checks
watchEffect(()=>{
const order = props.order
const builder = props.builder
props.builder.valid &&=
order && builder &&
builder.rungs >= 1 && valueA.value &&
(builder.rungs < 2 || valueB.value)
})
const rungs = computed({
get() {
return props.builder.rungs
},
set(r) {
// todo this is subclass specific: make TWAP extend the range rather than compressing it
if (!r) {
props.builder.rungs = 1
return
}
r = Number(r)
props.builder.rungs = r
const b = valueB.value
console.log('set rungs', r, valueA.value, b)
if ( r > 0 && b === null ) {
// convert single shape to a range
if (props.mode===0) {
const width = props.stdWidth
const mid = valueA.value
console.log('single to range', mid - width/2, mid + width/2)
valueA.value = mid - width/2
valueB.value = mid + width/2
}
else if (props.mode===1 ) {
valueB.value = valueA.value + props.stdWidth
}
else
throw Error(`Unknown rung mode ${props.mode}`)
}
else if ( r === 1 && b !== null ) {
// convert from a range to a single shape
if (props.mode===0)
valueA.value = (valueA.value + b) / 2
valueB.value = null
}
else {
// from multi to multi
if (props.mode===1)
valueB.value = valueA.value + props.stdWidth * (r-1)
}
}
})
const values = computed(()=>{
let a = valueA.value
let b = valueB.value
const r = props.builder.rungs
let result
if ( a===null || !r )
result = [] // no data
else if (r===1)
result = [a] // single shape
else
result = linspace(a, b, r) // linear spacing
props.setValues(result)
return result;
})
const weights = computed(() => linearWeights(props.builder.rungs, -props.builder.skew))
const amountSymbol = computed(()=>props.order.amountIsTokenA ? co.selectedSymbol.base.s : co.selectedSymbol.quote.s )
// colors
function computeDefaultColor() {
const index = props.order.builders.indexOf(props.builder)
return sideColor(props.order.buy, index)
}
const defaultColor = computeDefaultColor()
const color = computed({
get() {return props.builder.color},
set(v) {
const maxLightness = 60
const c = new Color(v).hsl()
props.builder.color = c.saturation <= maxLightness ? v : c.lightness(maxLightness).string()
}
})
const colors = computed( ()=> {
const color = props.builder.color !== null ? props.builder.color
: props.buy ? theme.value.colors.success : theme.value.colors.error
const ws = weights.value;
return weightColors(ws, color);
})
const colorStyle = computed(() => {
return {'color': color.value}
})
function allocText(weight) {
const alloc = props.builder.allocation
if (alloc===null) return ''
return allocationText(props.order.amount, weight * alloc, amountSymbol.value);
}
//
// SHAPE MANAGEMENT
//
// we keep two special control shapes as the edges of the range, with deletable shapes in-between
function createShape(value, model, onModel, onDelete) {
console.log('createShape setModelValue', model, value)
props.setModelValue(model, value)
return new props.shape(model, onModel, onDelete) // props.shape is the constructor function
}
function translateOnDrag(shape) {
const oldOnPoints = shape.onPoints
shape.onPoints = function (points) {
if (!this.beingDragged()) {
oldOnPoints.call(this, points)
return
}
const prev = props.getModelValue(this.model)
oldOnPoints.call(this, points)
const cur = props.getModelValue(this.model)
const delta = cur - prev
console.log('move prev/cur', prev, cur, delta)
if (delta !== 0) {
valueA.value += delta
valueB.value += delta
}
}
}
const shapeA = createShape(valueA.value, {color: defaultColor},
function (model) {
const value = props.getModelValue(model);
if (value!==null && value!==undefined)
valueA.value = value;
if (model.color)
props.builder.color = model.color;
},
deleteSelf)
if (props.mode===1)
translateOnDrag(shapeA)
const shapeB = createShape(valueB.value, {color:defaultColor},
function (model) {
const value = props.getModelValue(model);
if (value!==null && value!==undefined)
valueB.value = value;
if (model.color)
props.builder.color = model.color;
},
deleteSelf)
function interiorOnModel(model) {
const v = model.textColor || model.lineColor || model.color
if (v)
color.value = v
}
let interiorShapes = []
function createInteriorShape(price, weight) {
const model = {text: allocText(weight), color: props.builder.color};
const shape = createShape(price, model, interiorOnModel, deleteSelf)
shape.debug = true
translateOnDrag(shape)
interiorShapes.push(shape)
// shape.create() // should happen automatically when a model with valid points is set
}
function removeInteriorShape() {
if (interiorShapes.length) {
const shape = interiorShapes.pop()
shape.delete()
}
}
function adjustShapes() {
// this is where all the shapes are created or adjusted
console.log('adjustShapes()', valueA.value, valueB.value)
const vs = values.value
if (vs.length)
cancelDrawing()
const ws = weights.value
const colorStrings = colors.value
// shape properties
if( vs.length === 0 ) {
shapeA.delete()
shapeB.delete()
for( const shape of interiorShapes )
shape.delete()
interiorShapes = []
}
else if (vs.length === 1) {
//
// SINGLE SHAPE
//
if (!shapeA.beingDragged()) {
const model = {text: allocText(ws[0]), color: colorStrings[0]};
console.log('single shape A setModelValue', model, vs[0])
props.setModelValue(model, vs[0])
shapeA.setModel(model)
}
shapeB.delete()
if (interiorShapes.length) {
for( const shape of interiorShapes )
shape.delete()
interiorShapes = []
}
}
else {
//
// VALUE RANGE
//
if (!shapeA.beingDragged()) {
const model = {text: allocText(ws[0]), color: colorStrings[0]};
console.log('shape A not dragged setModelValue', model, vs[0])
props.setModelValue(model, vs[0])
shapeA.setModel(model)
}
if (!shapeB.beingDragged()) {
const last = colorStrings.length - 1
const model = {text: allocText(ws[last]), color: colorStrings[last]};
console.log('shape B not dragged setModelValue', model, vs[last])
props.setModelValue(model, vs[last])
shapeB.setModel(model)
}
const numInterior = Math.max(0,vs.length-2);
// trim excess interior shapes
while( interiorShapes.length > numInterior )
removeInteriorShape()
// adjust the interior shape values and/or add shapes
for( let i=1; i<vs.length-1; i++ ) {
const v = vs[i]
const w = ws[i];
if (i-1 === interiorShapes.length)
createInteriorShape(v, w)
else if (!interiorShapes[i-1].beingDragged()) {
const model = {text: allocText(w), color: colorStrings[i]};
console.log('interior setModelValue', model, v)
props.setModelValue(model, v)
interiorShapes[i-1].setModel(model)
}
}
}
return ''
}
function deleteSelf() {
deleteBuilder(props.order, props.builder);
}
function deleteShapes() {
shapeA.delete()
shapeB.delete()
for (const shape of interiorShapes)
shape.delete()
interiorShapes = []
}
console.log('valueA', valueA.value, shapeA)
if (!valueA.value)
shapeA.createOrDraw(); // initiate drawing mode
</script>
<style scoped lang="scss">
</style>

View File

@@ -172,18 +172,8 @@ export function pairPrice(chainId, baseToken, quoteToken, price) {
export const sleep = ms => new Promise(r => setTimeout(r, ms))
export function builderDefaults(props, emit, defaults) {
let changed = false
for (const k in defaults)
if (props.builder[k] === undefined) {
props.builder[k] = defaults[k] instanceof Function ? defaults[k]() : defaults[k]
changed = true
}
if (changed)
emit('update:builder', props.builder)
}
export function uuid() {
// noinspection JSUnresolvedReference
return crypto.randomUUID();
}
@@ -204,7 +194,7 @@ const colorRanges = {
sell: ['#CC0033', '#CCCC33'],
}
export function lineColor(buy, index) {
export function sideColor(buy, index) {
const range = buy ? colorRanges.buy : colorRanges.sell
const a = new Color(range[0]).rgb()
const b = new Color(range[1]).rgb()
@@ -229,3 +219,24 @@ export function unique(arr) {
}
return a;
}
export function linspace(a, b, n) {
if (n===1) return [(a+b)/2] // single line
const spacing = (b - a) / (n - 1)
// console.log('spacing', a, b)
const result = []
for (let i = 0; i < n; i++)
result.push(a + i * spacing)
return result;
}
export function intervalToSeconds(interval) {
if (interval.endsWith('T'))
throw Error('Tick intervals not supported')
return interval.endsWith('M') ? 30 * 24 * 60 * 60
: interval.endsWith('W') ? 7 * 24 * 60 * 60
: interval.endsWith('D') ? 24 * 60 * 60
: interval.endsWith('S') ? 1
: 60 // if no unit char, then it's minutes
}

View File

@@ -4,6 +4,10 @@ import {useOrderStore, useStore} from "@/store/store.js";
import {encodeIEE754} from "@/common.js";
import {defineStore} from "pinia";
import {computed, ref} from "vue";
import Color from "color";
export const MIN_EXECUTION_TIME = 60 // give at least one full minute for each tranche to trigger
function unimplemented() { throw Error('Unimplemented') }
@@ -13,7 +17,10 @@ function unimplemented() { throw Error('Unimplemented') }
// for instantiating the UI component for a given builder dictionary, based on its builder.component field.
export function newBuilder( component, options = {}) {
const id = uuid()
return {id, component, options, points: {}, shapes: {}, props: {}, build: unimplemented}
return {
id, component, options,
allocation: 1.0, points: {}, shapes: {}, props: {}, valid: false,
}
}
// Orders hold an amount and builders
@@ -44,6 +51,7 @@ export const useChartOrderStore = defineStore('chart_orders', () => {
const selectedOrder = ref(null)
const selectedSymbol = ref(null)
const selectedPool = ref(null)
const intervalSecs = ref(0)
const baseToken = computed(()=>selectedSymbol.value === null ? null : selectedSymbol.value.base)
const quoteToken = computed(()=>selectedSymbol.value === null ? null : selectedSymbol.value.quote)
const price = computed(() => {
@@ -87,7 +95,7 @@ export const useChartOrderStore = defineStore('chart_orders', () => {
}
return {
chartReady, selectedSymbol, selectedPool, baseToken, quoteToken, price,
chartReady, selectedSymbol, selectedPool, intervalSecs, baseToken, quoteToken, price,
orders, drawing, drawingCallbacks, newOrder, removeOrder, resetOrders,
}
})
@@ -222,3 +230,51 @@ export function timesliceTranches() {
return ts
}
export function builderDefaults(builder, defaults) {
for (const k in defaults)
if (builder[k] === undefined)
builder[k] = defaults[k] instanceof Function ? defaults[k]() : defaults[k]
}
export function linearWeights(n, s) {
if (n === 1) return [1]
const result = []
if (s === 0) {
// equal weighted
for (let i = 0; i < n; i++)
result.push(1 / n)
} else if (s === 1) {
result.push(1)
for (let i = 1; i < n; i++)
result.push(0)
} else if (s === -1) {
for (let i = 1; i < n; i++)
result.push(0)
result.push(1)
} else {
for (let i = 0; i < n; i++)
result.push((1 - s * (2 * i / (n - 1) - 1)) / n)
}
// console.log('weights', result)
return result
}
export function weightColors(weights, color) {
const c = new Color(color).rgb()
const max = Math.max(...weights)
const ns = weights.map((w) => w / max) // set largest weight to 100%
const adj = ns.map((w) => c.alpha(Math.pow(w, 0.67))) // https://en.wikipedia.org/wiki/Stevens's_power_law
return adj.map((a) => a.string())
}
export function allocationText(amount, weight, symbol) {
// console.log('weight', weight, alloc, props.amount)
const a = amount * weight
return `${(weight * 100).toFixed(1)}% = ${a.toLocaleString('fullwide')} ${symbol}`
}
export function deleteBuilder(order, builder) {
order.builders = order.builders.filter((b) => b !== builder)
// if (props.deleteShapes) // todo is this redundant?
// props.deleteShapes()
}