Files
web/src/components/chart/DiagonalBuilder.vue
2025-04-11 21:31:04 -04:00

444 lines
13 KiB
Vue

<template>
<rung-builder :name="name" :description="description"
:order="order" :builder="builder" v-model="endpoints"
:shape="DLine" :mode="0"
:get-model-value="getModelValue" :set-model-value="setModelValue"
:set-values="setLines" :set-weights="setWeights"
:set-shapes="setShapes"
:std-width="stdWidth" :build-tranches="buildTranches">
<table v-if="!co.drawing">
<tbody>
<tr>
<td>&nbsp;</td>
<td colspan="3">
<div class="d-flex align-center">
<v-switch class="d-inline-flex" v-model="extendLeft" inline
:true-value="false" :false-value="true"
true-icon="mdi-circle-outline" false-icon="mdi-arrow-left"
/>
<span class="mx-3">Extend</span>
<v-switch class="d-inline-flex" v-model="extendRight" inline
false-icon="mdi-circle-outline" true-icon="mdi-arrow-right"
/>
</div>
</td>
</tr>
<tr>
<td class="label" rowspan="2">Line A</td>
<td>
<absolute-time-entry v-model="time1A"/>
</td>
<td>
<v-text-field type="number" v-model="price1A" min="0"
density="compact" hide-details variant="outlined"
class="mx-1 my-2 price"
:color="color"
label="Price"
/>
</td>
<td rowspan="2" class="weight"
:style="'vertical-align: '+(price2A!==null?'bottom':'center')">
{{ allocationTexts[weights.length-1] }}
</td>
</tr>
<tr>
<td>
<absolute-time-entry v-model="time1B"/>
</td>
<td>
<v-text-field type="number" v-model="price1B" min="0"
density="compact" hide-details variant="outlined"
class="mx-1 my-2 price"
:color="color"
label="Price"
/>
</td>
</tr>
<tr v-for="i in innerIndexes" class="ml-5">
<td class="text-right">&nbsp;</td>
<td colspan="2" class="text-center">&mdash; Interior Line &mdash;</td>
<td class="weight">{{ allocationTexts[i] }}</td>
</tr>
<tr v-if="weights.length>1">
<td rowspan="2" class="label">Line B</td>
<td>
<absolute-time-entry v-model="time2A"/>
</td>
<td>
<v-text-field type="number" v-model="price2A" min="0"
density="compact" hide-details variant="outlined"
class="mx-1 my-2 price"
:color="color"
label="Price"
/>
</td>
<td rowspan="2" class="weight" style="vertical-align: top">{{ allocationTexts[0] }}</td>
</tr>
<tr v-if="weights.length>1">
<td>
<absolute-time-entry v-model="time2B"/>
</td>
<td>
<v-text-field type="number" v-model="price2B" min="0"
density="compact" hide-details variant="outlined"
class="mx-1 my-2 price"
:color="color"
label="Price"
/>
</td>
</tr>
</tbody>
</table>
<one-time-hint name="click-chart" activator="#tv-widget" location="center" :when="builder.lineA===null && !co.drew" text="Click the chart!"/>
</rung-builder>
</template>
<script setup>
import {applyLinePoints, builderDefaults, useChartOrderStore} from "@/orderbuild.js";
import {sideColor} from "@/misc.js";
import {MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js";
import RungBuilder from "@/components/chart/RungBuilder.vue";
import {computed, ref} from "vue";
import {allocationText, DLine} from "@/charts/shape.js";
import {vectorEquals, vectorInterpolate} from "@/vector.js";
import AbsoluteTimeEntry from "@/components/AbsoluteTimeEntry.vue";
import {useStore} from "@/store/store.js";
import OneTimeHint from "@/components/OneTimeHint.vue";
const s = useStore()
const co = useChartOrderStore()
const props = defineProps(['order', 'builder'])
const emit = defineEmits(['update:builder'])
function computeDefaultColor() {
const index = props.order.builders.indexOf(props.builder)
return sideColor(props.order.buy, index)
}
const defaultColor = computeDefaultColor()
const color = computed(()=>props.builder.color ? props.builder.color : defaultColor)
// Fields must be defined in order to be reactive
builderDefaults(props.builder, {
lineA: null, // [{time, price}, {time, price}]
lineB: null,
extendLeft: false,
extendRight: true,
rungs: 1,
balance: 0,
breakout: false,
color: defaultColor,
buy: true,
})
function buildTranches() {
const order = props.order
const builder = props.builder
const tranches = []
const warnings = []
console.log('buildTranches', builder, order, _endpoints.value)
const la = _endpoints.value[0] // use the flatline format which is a vector of length 4, useful for vectorInterpolate()
const lb = _endpoints.value[1]
const ws = weights.value
const symbol = co.selectedSymbol
for (let i = 0; i < ws.length; i++) {
const w = ws[i]
const t = newTranche({fraction: w * MAX_FRACTION})
const line = ws.length === 1 ? la : vectorInterpolate(la, lb, i/(ws.length-1))
const el = extendLeft.value
const er = extendRight.value
const reversed = line[0] > line[2]
if (reversed ? !er : !el)
t.startTime = reversed ? line[2] : line[0]
if (reversed ? !el : !er)
t.endTime = reversed ? line[0] : line[2]
if (t.endTime <= s.clock)
warnings.push(`Tranche already expired at ${new Date(t.endTime*1000)}`)
// console.log('tranche start/end',
// t.startTime === DISTANT_PAST ? 'PAST' : t.startTime,
// t.endTime === DISTANT_FUTURE ? 'FUTURE' : t.endTime)
applyLinePoints(t, symbol, order.buy, ...line, builder.breakout)
tranches.push(t)
}
// if( flipped.value )
// tranches.reverse()
return {tranches, warnings}
}
function flattenLine(l) {
return l === null ? null : [l[0].time, l[0].price, l[1].time, l[1].price]
}
function buildLine(f) {
// console.log('buildLine', f)
return f === null ? null : [{time: Number(f[0]), price: Number(f[1])}, {time: Number(f[2]), price: Number(f[3])}]
}
const _endpoints = ref([flattenLine(props.builder.lineA), flattenLine(props.builder.lineB)])
const endpoints = computed({
get() {
return _endpoints.value
},
set(v) {
let [a, b] = v
a = buildLine(a)
b = buildLine(b)
update(a, b)
}
})
const alignTimes = ref(true)
const time1A = computed({
get() { return _endpoints.value[0] === null ? null : _endpoints.value[0][0] },
set(v) {
const flatline0 = _endpoints.value[0];
update(
[{time:v, price: flatline0[1]}, {time:flatline0[2], price: flatline0[3]}],
_endpoints.value[1]
)
}
})
const price1A = computed({
get() { return _endpoints.value[0] === null ? null : _endpoints.value[0][1] },
set(v) {
const flatline0 = _endpoints.value[0];
update(
[{time:flatline0[0], price: Number(v)}, {time:flatline0[2], price: flatline0[3]}],
_endpoints.value[1]
)
}
})
const time1B = computed({
get() { return _endpoints.value[0] === null ? null : _endpoints.value[0][2] },
set(v) {
const flatline0 = _endpoints.value[0];
update(
[{time:flatline0[0], price: flatline0[1]}, {time:v, price: flatline0[3]}],
_endpoints.value[1]
)
}
})
const price1B = computed({
get() { return _endpoints.value[0] === null ? null : _endpoints.value[0][3] },
set(v) {
const flatline0 = _endpoints.value[0];
update(
[{time:flatline0[0], price: flatline0[1]}, {time:flatline0[2], price: Number(v)}],
_endpoints.value[1]
)
}
})
const time2A = computed({
get() { return _endpoints.value[1] === null ? null : _endpoints.value[1][0] },
set(v) {
const flatline = _endpoints.value[1];
update(
_endpoints.value[0],
[{time:v, price: flatline[1]}, {time:flatline[2], price: flatline[3]}],
)
}
})
const price2A = computed({
get() { return _endpoints.value[1] === null ? null : _endpoints.value[1][1] },
set(v) {
const flatline = _endpoints.value[1];
update(
_endpoints.value[0],
[{time:flatline[0], price: Number(v)}, {time:flatline[2], price: flatline[3]}],
)
}
})
const time2B = computed({
get() { return _endpoints.value[1] === null ? null : _endpoints.value[1][2] },
set(v) {
const flatline = _endpoints.value[1];
update(
_endpoints.value[0],
[{time:flatline[0], price: flatline[1]}, {time:v, price: flatline[3]}],
)
}
})
const price2B = computed({
get() { return _endpoints.value[1] === null ? null : _endpoints.value[1][3] },
set(v) {
const flatline = _endpoints.value[1];
update(
_endpoints.value[0],
[{time:flatline[0], price: flatline[1]}, {time:flatline[2], price: Number(v)}],
)
}
})
function update(a, b) { // a and b are lines of two points
if (!vectorEquals(props.builder.lineA, a) || !vectorEquals(props.builder.lineB, b)) {
_endpoints.value = [flattenLine(a), flattenLine(b)]
const newBuilder = {...props.builder}
newBuilder.lineA = a
newBuilder.lineB = b
emit('update:builder', newBuilder)
}
}
let shapeA = null
let shapeB = null
function setShapes(a,b) {
shapeA = a
shapeB = b
}
const _extendLeft = ref(true)
const extendLeft = computed({
get() {return _extendLeft.value},
set(v) {
if (v !== _extendLeft.value) {
_extendLeft.value = v;
const b = {...props.builder}
b.extendLeft = v
// shapeA.setModel({extendLeft: v})
// shapeB.setModel({extendLeft: v})
emit('update:builder', b)
}
}
})
const _extendRight = ref(true)
const extendRight = computed({
get() {return _extendRight.value},
set(v) {
if (v !== _extendRight.value) {
_extendRight.value = v;
const b = {...props.builder}
b.extendRight = v
// shapeA.setModel({extendRight: v})
// shapeB.setModel({extendRight: v})
emit('update:builder', b)
}
}
})
const innerIndexes = computed(() => {
const n = flatLines.value.length
const result = []
for (let i = 1; i < n - 1; i++)
result.push(n - 1 - i)
return result
})
// these are set by the RungBuilder
const flatLines = ref([])
const weights = ref([])
function setLines(ls) {
flatLines.value = ls
}
function setWeights(ws) {
weights.value = ws
}
const amountSymbol = computed(()=>props.order.amountIsTokenA ? co.selectedSymbol.base.s : co.selectedSymbol.quote.s )
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(props.order.buy, w, w * props.order.amount, co.selectedSymbol.base.s, amountSymbol.value)))
const stdWidth = computed(()=>[0, co.meanRange, 0, co.meanRange])
function getModelValue(model) {
// model is the DLine shape's model object
if (!model.pointA || !model.pointB)
return null
if (model.extendLeft !== _extendLeft.value)
extendLeft.value = model.extendLeft
if (model.extendRight !== _extendRight.value)
extendRight.value = model.extendRight
const result = flattenLine([model.pointA, model.pointB]);
// console.log('getModelValue', {...model}, result)
return result // this is the vector the RungBuilder will interpolate
}
function setModelValue(model, value) {
// const oldModel = {...model};
// console.log('setModelValue', oldModel, value)
const oldLine = !model.pointA || !model.pointB ? null : [model.pointA, model.pointB]
const line = buildLine(value)
if (dirtyLine(oldLine, line)) {
if (line===null) {
model.pointA = null
model.pointB = null
}
else {
model.pointA = line[0]
model.pointB = line[1]
}
}
if (model.extendLeft !== _extendLeft.value)
model.extendLeft = _extendLeft.value
if (model.extendRight !== _extendRight.value)
model.extendRight = _extendRight.value
// console.log('setModelValue end', oldModel, value, model)
}
function dirtyLine(a, b) {
const result = a === b ? false :
a === null && b !== null || a !== null && b === null ||
a[0].time !== b[0].time || a[0].price !== b[0].price || a[1].time !== b[1].time || a[1].price !== b[1].price
// console.log('dirtyLine', result, a, b)
return result
}
const name = computed(()=>props.builder.breakout?(props.order.buy?'Breakout':'Breakdown'):'Limit')
const description = computed(()=>{
const buy = props.order.buy
const above = buy === props.builder.breakout
const plural = props.builder.rungs > 1 ? 's' : ''
return (buy?'Buy ':'Sell ')+(above?'above':'below')+' the line'+(plural?'s':'')
})
</script>
<style scoped lang="scss">
td.weight {
min-width: 5em;
max-width: 20em;
padding-left: 0.5em;
padding-right: 0.5em;
text-align: right;
}
td.label {
$w: 4em;
width: $w;
min-width: $w;
max-width: $w;
text-align: right;
padding-right: .5em;
}
.price {
$w: 6em;
//width: $w;
min-width: $w;
max-width: 10em;
}
</style>