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

@@ -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>