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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>-->
|
||||
<!--
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
|
||||
352
src/components/chart/RungBuilder.vue
Normal file
352
src/components/chart/RungBuilder.vue
Normal 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>
|
||||
35
src/misc.js
35
src/misc.js
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user