rate limit DCA
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import {useChartOrderStore} from "@/orderbuild.js";
|
import {useChartOrderStore} from "@/orderbuild.js";
|
||||||
import {invokeCallbacks, prototype} from "@/common.js";
|
import {invokeCallbacks, prototype} from "@/common.js";
|
||||||
import {DataFeed, feelessTickerKey, getAllSymbols, lookupSymbol} from "@/charts/datafeed.js";
|
import {DataFeed, defaultSymbol, feelessTickerKey, getAllSymbols, lookupSymbol} from "@/charts/datafeed.js";
|
||||||
import {intervalToSeconds, SingletonCoroutine} from "@/misc.js";
|
import {intervalToSeconds, SingletonCoroutine} from "@/misc.js";
|
||||||
import {usePrefStore, useStore} from "@/store/store.js";
|
import {usePrefStore, useStore} from "@/store/store.js";
|
||||||
import {tvCustomThemes} from "../../theme.js";
|
import {tvCustomThemes} from "../../theme.js";
|
||||||
@@ -24,9 +24,10 @@ export function removeSymbolChangedCallback(cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function symbolChanged(symbol) {
|
function symbolChanged(symbol) {
|
||||||
const info = symbol===null ? null : lookupSymbol(symbol.ticker)
|
const info = symbol===null ? (defaultSymbol===null?'default':defaultSymbol) : lookupSymbol(symbol.ticker)
|
||||||
co.selectedSymbol = info
|
co.selectedSymbol = info
|
||||||
prefs.selectedTicker = info?.ticker
|
console.log('setting prefs ticker', info.ticker)
|
||||||
|
prefs.selectedTicker = info.ticker
|
||||||
symbolChangedCbs.forEach((cb) => cb(info))
|
symbolChangedCbs.forEach((cb) => cb(info))
|
||||||
updateFeeDropdown()
|
updateFeeDropdown()
|
||||||
console.log('symbol changed', info)
|
console.log('symbol changed', info)
|
||||||
@@ -149,6 +150,7 @@ export function initWidget(el) {
|
|||||||
drawings_access: {type: 'white', tools: [],}, // show no tools
|
drawings_access: {type: 'white', tools: [],}, // show no tools
|
||||||
custom_themes: tvCustomThemes,
|
custom_themes: tvCustomThemes,
|
||||||
theme: useStore().theme,
|
theme: useStore().theme,
|
||||||
|
timezone: prefs.timezone,
|
||||||
|
|
||||||
// Chart Overrides
|
// Chart Overrides
|
||||||
// https://www.tradingview.com/charting-library-docs/latest/customization/overrides/chart-overrides
|
// https://www.tradingview.com/charting-library-docs/latest/customization/overrides/chart-overrides
|
||||||
@@ -182,8 +184,7 @@ function initChart() {
|
|||||||
chart.onIntervalChanged().subscribe(null, changeInterval)
|
chart.onIntervalChanged().subscribe(null, changeInterval)
|
||||||
chart.onDataLoaded().subscribe(null, dataLoaded)
|
chart.onDataLoaded().subscribe(null, dataLoaded)
|
||||||
const tzapi = chart.getTimezoneApi();
|
const tzapi = chart.getTimezoneApi();
|
||||||
tzapi.onTimezoneChanged().subscribe(null, (tz)=>{if (tz==='exchange') tz='Etc/UTC'; s.timeZone=tz})
|
tzapi.onTimezoneChanged().subscribe(null, (tz)=>{if (tz==='exchange') tz='Etc/UTC'; prefs.timezone=tz;})
|
||||||
s.timeZone = tzapi.getTimezone().id
|
|
||||||
// chart.onHoveredSourceChanged().subscribe(null, ()=>console.log('hovered source changed', arguments))
|
// chart.onHoveredSourceChanged().subscribe(null, ()=>console.log('hovered source changed', arguments))
|
||||||
// chart.selection().onChanged().subscribe(null, s => console.log('selection', chart.selection().allSources()));
|
// chart.selection().onChanged().subscribe(null, s => console.log('selection', chart.selection().allSources()));
|
||||||
const symbolExt = chart.symbolExt();
|
const symbolExt = chart.symbolExt();
|
||||||
@@ -358,7 +359,7 @@ function doHandleCrosshairMovement(point) {
|
|||||||
const lpbe = shape._model._linePointBeingEdited
|
const lpbe = shape._model._linePointBeingEdited
|
||||||
points[lpbe] = point
|
points[lpbe] = point
|
||||||
// console.log('drag calling onPoints', points, shape, lpbe)
|
// console.log('drag calling onPoints', points, shape, lpbe)
|
||||||
invokeCallbacks(shapeCallbacks[shapeId], 'onPoints', shapeId, shape, points)
|
invokeCallbacks(shapeCallbacks[shapeId], 'onDrag', shapeId, shape, points)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (draggingShapeIds.length > 0) {
|
else if (draggingShapeIds.length > 0) {
|
||||||
|
|||||||
@@ -131,8 +131,13 @@ function addSymbol(chainId, p, base, quote, inverted) {
|
|||||||
else
|
else
|
||||||
feeGroups[feelessKey] = [[symbolInfo.address, symbolInfo.fee]]
|
feeGroups[feelessKey] = [[symbolInfo.address, symbolInfo.fee]]
|
||||||
symbolInfo.feeGroup = feeGroups[feelessKey]
|
symbolInfo.feeGroup = feeGroups[feelessKey]
|
||||||
if (defaultSymbol===null && !invertedDefault(symbolInfo.base.a, symbolInfo.quote.a))
|
if (defaultSymbol===null) {
|
||||||
|
console.log(`invertedDefault(${symbolInfo.base.s}, ${symbolInfo.quote.s})`,invertedDefault(symbolInfo.base.a, symbolInfo.quote.a))
|
||||||
|
}
|
||||||
|
if (defaultSymbol===null && !invertedDefault(symbolInfo.base.a, symbolInfo.quote.a)) {
|
||||||
|
console.log('setting default symbol', symbolInfo.base.s, symbolInfo.quote.s, symbolInfo.base.a, symbolInfo.quote.a)
|
||||||
defaultSymbol = _symbols[ticker]
|
defaultSymbol = _symbols[ticker]
|
||||||
|
}
|
||||||
log('new symbol', ticker, _symbols[ticker])
|
log('new symbol', ticker, _symbols[ticker])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,4 +689,4 @@ export const DataFeed = {
|
|||||||
|
|
||||||
|
|
||||||
let _rolloverBumper = null
|
let _rolloverBumper = null
|
||||||
let defaultSymbol = null
|
export let defaultSymbol = null
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const ShapeType = {
|
|||||||
HLine: {name: 'Horizontal Line', code: 'horizontal_line', drawingProp: 'linetoolhorzline'},
|
HLine: {name: 'Horizontal Line', code: 'horizontal_line', drawingProp: 'linetoolhorzline'},
|
||||||
VLine: {name: 'Vertical Line', code: 'vertical_line', drawingProp: 'linetoolvertline'},
|
VLine: {name: 'Vertical Line', code: 'vertical_line', drawingProp: 'linetoolvertline'},
|
||||||
PriceRange: {name: 'Price Range', code: 'price_range'},
|
PriceRange: {name: 'Price Range', code: 'price_range'},
|
||||||
|
DateRange: {name: 'Date Range', code: 'date_range', drawingProp: 'linetooldaterange'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -335,6 +336,8 @@ export class Shape {
|
|||||||
|
|
||||||
onPoints(points) {} // the control points of an existing shape were changed
|
onPoints(points) {} // the control points of an existing shape were changed
|
||||||
|
|
||||||
|
onDrag(points) { console.log('shape ondrag'); this.onPoints(points) }
|
||||||
|
|
||||||
setProps(props) {
|
setProps(props) {
|
||||||
if (!props || Object.keys(props).length===0) return
|
if (!props || Object.keys(props).length===0) return
|
||||||
if (this.debug) console.log('setProps', this.id, props)
|
if (this.debug) console.log('setProps', this.id, props)
|
||||||
@@ -384,7 +387,6 @@ export class Shape {
|
|||||||
onUndraw() {} // drawing was canceled by clicking on a different tool
|
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)
|
onAddPoint() {} // the user clicked a point while drawing (that point is added to the points list)
|
||||||
onMove(points) {} // the shape was moved by dragging a drawing element not the control point
|
onMove(points) {} // the shape was moved by dragging a drawing element not the control point
|
||||||
onDrag(points) {}
|
|
||||||
onHide(props) {}
|
onHide(props) {}
|
||||||
onShow(props) {}
|
onShow(props) {}
|
||||||
onClick() {} // the shape was selected
|
onClick() {} // the shape was selected
|
||||||
@@ -479,16 +481,6 @@ class ShapeTVCallbacks {
|
|||||||
|
|
||||||
|
|
||||||
export class Line extends Shape {
|
export class Line extends Shape {
|
||||||
onDrag(points) {
|
|
||||||
const s = this.tvShape();
|
|
||||||
if (this.debug) {
|
|
||||||
console.log('shape', s.id, 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -656,3 +648,17 @@ export class DLine extends Line {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class DateRange extends Shape {
|
||||||
|
constructor(model, onModel=null, onDelete=null, props=null) {
|
||||||
|
super(ShapeType.DateRange, onModel, onDelete, props)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setModel(model) {
|
||||||
|
super.setModel(model);
|
||||||
|
if (model.startTime !== this.model.startTime || model.endTime !== this.model.endTime)
|
||||||
|
this.setPoints([{time: model.startTime}, {time: model.endTime}])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,17 +13,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {useStore} from "@/store/store";
|
import {usePrefStore, useStore} from "@/store/store";
|
||||||
import {computed} from "vue";
|
import {computed} from "vue";
|
||||||
import {DateTime, Info} from "luxon";
|
import {DateTime, Info} from "luxon";
|
||||||
|
|
||||||
const s = useStore()
|
const prefs = usePrefStore()
|
||||||
const props = defineProps(['modelValue'])
|
const props = defineProps(['modelValue'])
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
const hideDetails = true
|
const hideDetails = true
|
||||||
|
|
||||||
const now = computed(()=>DateTime.fromSeconds(props.modelValue?props.modelValue:0).setZone(s.timeZone))
|
const now = computed(()=>DateTime.fromSeconds(props.modelValue?props.modelValue:0).setZone(prefs.timezone))
|
||||||
|
|
||||||
const year = computed({
|
const year = computed({
|
||||||
get() { return now.value.year },
|
get() { return now.value.year },
|
||||||
|
|||||||
@@ -93,16 +93,9 @@
|
|||||||
<template v-for="(t, i) in item.order.tranches">
|
<template v-for="(t, i) in item.order.tranches">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-right" colspan="2">
|
<td class="text-right" colspan="2">
|
||||||
<div class="text-right">
|
|
||||||
<div v-if="s.clock < item.trancheStatus[i].startTime">
|
|
||||||
Activates {{ timestampString(item.trancheStatus[i].startTime) }}
|
|
||||||
</div>
|
|
||||||
<div v-if="s.clock < item.trancheStatus[i].endTime && item.trancheStatus[i].endTime < DISTANT_FUTURE">
|
|
||||||
Expires {{ timestampString(item.trancheStatus[i].endTime) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mx-3" v-if="t.marketOrder">market order</div>
|
<div class="mx-3" v-if="t.marketOrder && t.rateLimitFraction === MAX_FRACTION">market order</div>
|
||||||
|
<div class="mx-3" v-if="t.marketOrder && t.rateLimitFraction < MAX_FRACTION">DCA {{Math.round(MAX_FRACTION/t.rateLimitFraction)}} parts</div>
|
||||||
<line-price class="mx-3" v-if="!t.marketOrder"
|
<line-price class="mx-3" v-if="!t.marketOrder"
|
||||||
:base="item.base" :quote="item.quote"
|
:base="item.base" :quote="item.quote"
|
||||||
:b="t.minLine.intercept" :m="t.minLine.slope" :is-breakout="!item.minIsLimit"
|
:b="t.minLine.intercept" :m="t.minLine.slope" :is-breakout="!item.minIsLimit"
|
||||||
@@ -112,7 +105,10 @@
|
|||||||
:b="t.maxLine.intercept" :m="t.maxLine.slope" :is-breakout="item.minIsLimit"
|
:b="t.maxLine.intercept" :m="t.maxLine.slope" :is-breakout="item.minIsLimit"
|
||||||
:show-btn="true"/>
|
:show-btn="true"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-right" v-if="item.state===OrderState.Open">
|
||||||
|
<div>{{ describeTrancheTime(item, i, true) }}</div>
|
||||||
|
<div>{{ describeTrancheTime(item, i, false) }}</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<suspense>
|
<suspense>
|
||||||
@@ -170,7 +166,7 @@ import {useStore} from "@/store/store";
|
|||||||
import {computed, defineAsyncComponent, onUnmounted, ref, watch} from "vue";
|
import {computed, defineAsyncComponent, onUnmounted, ref, watch} from "vue";
|
||||||
import Btn from "@/components/Btn.vue"
|
import Btn from "@/components/Btn.vue"
|
||||||
import {cancelOrder, useWalletStore} from "@/blockchain/wallet.js";
|
import {cancelOrder, useWalletStore} from "@/blockchain/wallet.js";
|
||||||
import {DISTANT_FUTURE, isOpen, OrderState} from "@/blockchain/orderlib.js";
|
import {DISTANT_FUTURE, isOpen, MAX_FRACTION, OrderState} from "@/blockchain/orderlib.js";
|
||||||
import Pulse from "@/components/Pulse.vue";
|
import Pulse from "@/components/Pulse.vue";
|
||||||
import {OrderShapes} from "@/charts/ordershapes.js";
|
import {OrderShapes} from "@/charts/ordershapes.js";
|
||||||
import {useChartOrderStore} from "@/orderbuild.js";
|
import {useChartOrderStore} from "@/orderbuild.js";
|
||||||
@@ -376,7 +372,6 @@ const orders = computed(()=>{
|
|||||||
function describeTrancheTime(st, trancheIndex, isStart) {
|
function describeTrancheTime(st, trancheIndex, isStart) {
|
||||||
const t = st.order.tranches[trancheIndex]
|
const t = st.order.tranches[trancheIndex]
|
||||||
const ts = st.trancheStatus[trancheIndex]
|
const ts = st.trancheStatus[trancheIndex]
|
||||||
console.log('describeTT', t, ts)
|
|
||||||
let result = ''
|
let result = ''
|
||||||
if( t.minIsBarrier || t.maxIsBarrier )
|
if( t.minIsBarrier || t.maxIsBarrier )
|
||||||
return 'barrier'
|
return 'barrier'
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {computed} from "vue";
|
import {computed} from "vue";
|
||||||
import DCABuilder from "@/components/chart/DCABuilder.vue";
|
import DateBuilder from "@/components/chart/DateBuilder.vue";
|
||||||
import LimitBuilder from "@/components/chart/LimitBuilder.vue";
|
import LimitBuilder from "@/components/chart/LimitBuilder.vue";
|
||||||
import MarketBuilder from "@/components/chart/MarketBuilder.vue";
|
import MarketBuilder from "@/components/chart/MarketBuilder.vue";
|
||||||
import DiagonalBuilder from "@/components/chart/DiagonalBuilder.vue";
|
import DiagonalBuilder from "@/components/chart/DiagonalBuilder.vue";
|
||||||
|
import DCABuilder from "@/components/chart/DCABuilder.vue";
|
||||||
|
|
||||||
const props = defineProps(['order', 'builder'])
|
const props = defineProps(['order', 'builder'])
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ const component = computed(()=>{
|
|||||||
return LimitBuilder
|
return LimitBuilder
|
||||||
case 'DiagonalBuilder':
|
case 'DiagonalBuilder':
|
||||||
return DiagonalBuilder
|
return DiagonalBuilder
|
||||||
|
case 'DateBuilder':
|
||||||
|
return DateBuilder
|
||||||
default:
|
default:
|
||||||
console.error('Unknown builder component '+props.builder.component)
|
console.error('Unknown builder component '+props.builder.component)
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -2,17 +2,8 @@
|
|||||||
<row-bar :color="builder.color">
|
<row-bar :color="builder.color">
|
||||||
<color-band :color="builder.color"/>
|
<color-band :color="builder.color"/>
|
||||||
<slot/>
|
<slot/>
|
||||||
<div class="align-self-center">
|
<div class="align-self-center ml-auto mr-3 trashcan">
|
||||||
<v-menu>
|
<v-btn icon="mdi-delete" @click="deleteMyBuilder"/>
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-btn variant="plain" v-bind="props" icon="mdi-dots-vertical"/>
|
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<!-- <v-list-subheader :title="'Limit '+ (priceA?priceA.toPrecision(5):'')"/>-->
|
|
||||||
<v-list-item title="Delete" key="withdraw" value="withdraw" prepend-icon="mdi-delete" color="red"
|
|
||||||
@click="deleteMyBuilder"/>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</div>
|
</div>
|
||||||
</row-bar>
|
</row-bar>
|
||||||
</template>
|
</template>
|
||||||
@@ -60,5 +51,11 @@ function deleteMyBuilder() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@import "@/styles/vars";
|
||||||
|
|
||||||
|
.trashcan {
|
||||||
|
:hover {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,33 +3,13 @@
|
|||||||
<row-bar :color="color">
|
<row-bar :color="color">
|
||||||
<color-band :color="color"/>
|
<color-band :color="color"/>
|
||||||
<div style="width: 100%" class="justify-start align-content-start">
|
<div style="width: 100%" class="justify-start align-content-start">
|
||||||
<v-text-field type="number" inputmode="numeric" pattern="[0-9]*\.?[0-9]*" v-model="order.amount" variant="outlined"
|
<order-amount :order="props.order" :color="color"/>
|
||||||
density="compact"
|
|
||||||
:hint="available" :persistent-hint="true"
|
|
||||||
min="0"
|
|
||||||
class="amount py-2" :color="color"
|
|
||||||
:label="order.amountIsTokenA ? 'Amount':('Value in '+co.selectedSymbol.quote.s)"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<v-btn variant="outlined" :color="color" class="ml-3"
|
|
||||||
:text="(order.buy ? 'Buy ' : 'Sell ') + co.selectedSymbol.base.s"
|
|
||||||
@click="order.buy=!order.buy"/>
|
|
||||||
</template>
|
|
||||||
<template #prepend-inner>
|
|
||||||
<v-btn variant="text" text="max" class="px-0" size="small"
|
|
||||||
:disabled="!maxAmount || order.amountIsTokenA===order.buy && !co.price" @click="setMax"/>
|
|
||||||
</template>
|
|
||||||
<template #append-inner>
|
|
||||||
<v-btn :text="order.amountIsTokenA?co.baseToken.s:co.quoteToken.s+' worth'"
|
|
||||||
:color="color" variant="text" @click="toggleAmountToken"/>
|
|
||||||
</template>
|
|
||||||
</v-text-field>
|
|
||||||
<template v-for="b in builders">
|
<template v-for="b in builders">
|
||||||
<builder-factory :order="order" :builder="b"/>
|
<builder-factory :order="order" :builder="b"/>
|
||||||
</template>
|
</template>
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<div v-if="order.builders.length===0"> <!--todo remove gralpha limitation of one builder-->
|
<div v-if="order.builders.length===0"> <!--todo remove gralpha limitation of one builder-->
|
||||||
<v-tooltip text="Spread order across time" location="top">
|
<v-tooltip text="Up to 1000 equal parts spread across time" location="top">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<span v-bind="props">
|
<span v-bind="props">
|
||||||
<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-clock-outline" @click="build(order,'DCABuilder')">DCA</v-btn>
|
||||||
@@ -50,6 +30,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
|
<v-tooltip text="Up to 10 weighted parts spaced out in time" location="top">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<span v-bind="props">
|
||||||
|
<v-btn :color="color" variant="text" prepend-icon="mdi-reorder-vertical" @click="build(order,'DateBuilder')">Dates</v-btn>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
<v-tooltip text="Coming Soon! Stoplosses and Takeprofits" location="top">
|
<v-tooltip text="Coming Soon! Stoplosses and Takeprofits" location="top">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<span v-bind="props">
|
<span v-bind="props">
|
||||||
@@ -69,13 +56,14 @@
|
|||||||
import BuilderFactory from "@/components/chart/BuilderFactory.vue";
|
import BuilderFactory from "@/components/chart/BuilderFactory.vue";
|
||||||
import {builderFuncs, newBuilder, orderFuncs, useChartOrderStore} from "@/orderbuild.js";
|
import {builderFuncs, newBuilder, orderFuncs, useChartOrderStore} from "@/orderbuild.js";
|
||||||
import {useOrderStore, useStore} from "@/store/store.js";
|
import {useOrderStore, useStore} from "@/store/store.js";
|
||||||
import {computed, onUnmounted, onUpdated, ref, watchEffect} from "vue";
|
import {computed, onUnmounted, onUpdated, watchEffect} from "vue";
|
||||||
import {lightenColor, lightenColor2, toPrecision} from "@/misc.js";
|
import {toPrecision} from "@/misc.js";
|
||||||
import {useTheme} from "vuetify";
|
import {useTheme} from "vuetify";
|
||||||
import RowBar from "@/components/chart/RowBar.vue";
|
import RowBar from "@/components/chart/RowBar.vue";
|
||||||
import ColorBand from "@/components/chart/ColorBand.vue";
|
import ColorBand from "@/components/chart/ColorBand.vue";
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
import {newOrder} from "@/blockchain/orderlib.js";
|
import {newOrder} from "@/blockchain/orderlib.js";
|
||||||
|
import OrderAmount from "@/components/chart/OrderAmount.vue";
|
||||||
|
|
||||||
const props = defineProps(['order'])
|
const props = defineProps(['order'])
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
@@ -199,55 +187,5 @@ const color = computed(()=>new Color(props.order.buy?theme.value.colors.success:
|
|||||||
// const lightColorStyle = computed(() => { return {'background-color': lightColor.value} })
|
// const lightColorStyle = computed(() => { return {'background-color': lightColor.value} })
|
||||||
// const faintColorStyle = computed(() => { return {'background-color': faintColor.value} })
|
// const faintColorStyle = computed(() => { return {'background-color': faintColor.value} })
|
||||||
|
|
||||||
const inToken = computed( ()=>props.order.buy ? co.quoteToken : co.baseToken )
|
|
||||||
const maxAmount = computed(()=>{
|
|
||||||
const token = inToken.value;
|
|
||||||
if (!token)
|
|
||||||
return null
|
|
||||||
const balance = s.balances[token.a]
|
|
||||||
if( !balance )
|
|
||||||
return null
|
|
||||||
const divisor = os.amountIsTotal ? 1 : os.tranches
|
|
||||||
return balance / 10**token.d / divisor
|
|
||||||
})
|
|
||||||
const available = computed(()=>{
|
|
||||||
const max = maxAmount.value
|
|
||||||
return max === null ? '' : `Available: ${maxAmount.value} ${tokenIn.value.s}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const lastMaxValue = ref(-1)
|
|
||||||
|
|
||||||
function setMax() {
|
|
||||||
let amount = maxAmount.value
|
|
||||||
if (amount) {
|
|
||||||
const order = props.order
|
|
||||||
const price = co.price
|
|
||||||
if (order.amountIsTokenA===order.buy) {
|
|
||||||
if (order.buy)
|
|
||||||
amount /= price
|
|
||||||
else
|
|
||||||
amount *= price
|
|
||||||
}
|
|
||||||
amount = Number(amount.toPrecision(7))
|
|
||||||
lastMaxValue.value = amount
|
|
||||||
order.amount = amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleAmountToken() {
|
|
||||||
const order = props.order
|
|
||||||
order.amountIsTokenA=!order.amountIsTokenA
|
|
||||||
if (order.amount === lastMaxValue.value)
|
|
||||||
setMax()
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.amount {
|
|
||||||
max-width: 30em;
|
|
||||||
div.v-field {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,248 +1,267 @@
|
|||||||
<template class="d-flex align-content-center flex-column" style="height: 100%; width: 100%;">
|
<template class="d-flex align-content-center flex-column" style="height: 100%; width: 100%;">
|
||||||
<rung-builder name='DCA' :order="order" :builder="builder" v-model="timeEndpoints"
|
<builder-panel :order="order" :builder="builder" :build-tranches="buildTranches"
|
||||||
:shape="VLine"
|
:adjust-shapes="adjustShapes" :delete-shapes="deleteShapes">
|
||||||
:mode="1" :flip="flipped" :orientation="0"
|
<div class="d-flex flex-column" style="width: 100%;">
|
||||||
:get-model-value="getModelValue" :set-model-value="setModelValue"
|
<div class="d-flex flex-row">
|
||||||
:get-points-value="getPointsValue"
|
<div class="align-self-center">Start:</div>
|
||||||
:set-values="setValues" :set-weights="setWeights"
|
<absolute-time-entry v-model="startTime"/>
|
||||||
:std-width="stdWidth" :build-tranches="buildTranches">
|
</div>
|
||||||
|
|
||||||
<!--
|
<div class="d-flex flex-row">
|
||||||
<v-list style="background-color: inherit">
|
<div>
|
||||||
<v-list-item v-for="t in absoluteTimes">{{t}}</v-list-item>
|
<v-text-field label="Split into" type="number" variant="outlined"
|
||||||
</v-list>
|
aria-valuemin="1" aria-valuemax="100" min="1" max="1000" step="1"
|
||||||
-->
|
v-model="parts" v-auto-select class="parts mr-3">
|
||||||
|
<template v-slot:append-inner>
|
||||||
|
parts
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table>
|
<div class="mr-3">
|
||||||
<tbody>
|
<order-amount :order="props.order" :multiplier="props.builder.tranches" :color="null" style="width: 20em"/>
|
||||||
<tr>
|
</div>
|
||||||
<td>
|
|
||||||
<absolute-time-entry v-model="absTimeA"/>
|
|
||||||
</td>
|
|
||||||
<td class="weight">{{ weights.length ? allocationTexts[0] : '' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="weights.length>2" v-for="i in weights.length-2" class="ml-5"> <!-- vue uses 1-based loops -->
|
|
||||||
<td class="d-flex justify-end pr-3">{{ dateStrings[i] }}</td>
|
|
||||||
<td class="weight">{{ allocationTexts[i] }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="weights.length>1">
|
|
||||||
<td>
|
|
||||||
<absolute-time-entry v-model="absTimeB"/>
|
|
||||||
</td>
|
|
||||||
<td class="weight">{{ allocationTexts[weights.length-1] }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
<div>
|
||||||
</rung-builder>
|
<v-text-field type="number" variant="outlined" :min="1" v-model="displayedInterval" class="interval"
|
||||||
|
:label="intervalIsTotal ? 'Total completion time' : 'Time between parts'" v-auto-select>
|
||||||
|
<template v-slot:prepend-inner>
|
||||||
|
<v-btn variant="outlined"
|
||||||
|
:text="intervalIsTotal ? 'All within' : 'Spaced apart by'" class="within mr-2"
|
||||||
|
style="width: 10em"
|
||||||
|
@click="intervalIsTotal=!intervalIsTotal"/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append-inner>
|
||||||
|
<v-btn variant="outlined" :text="timeUnitsStr" style="width: 5em"
|
||||||
|
@click="toggleTimeUnits" class="time-units"/>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</builder-panel>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {builderDefaults, DEFAULT_SLIPPAGE, MIN_EXECUTION_TIME, useChartOrderStore} from "@/orderbuild.js";
|
import {builderDefaults, DEFAULT_SLIPPAGE, useChartOrderStore} from "@/orderbuild.js";
|
||||||
import {allocationText, VLine} from "@/charts/shape.js";
|
import {ShapeType} from "@/charts/shape.js";
|
||||||
import {sideColor} from "@/misc.js";
|
import {sideColor, vAutoSelect} from "@/misc.js";
|
||||||
import {useOrderStore, useStore} from "@/store/store.js";
|
import {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, watchEffect} from "vue";
|
import {computed, ref, watchEffect} from "vue";
|
||||||
import {chart, dragging} from "@/charts/chart.js";
|
import {createShape, deleteShapeId, widget} from "@/charts/chart.js";
|
||||||
import AbsoluteTimeEntry from "@/components/AbsoluteTimeEntry.vue";
|
import AbsoluteTimeEntry from "@/components/AbsoluteTimeEntry.vue";
|
||||||
import {DateTime} from "luxon";
|
import BuilderPanel from "@/components/chart/BuilderPanel.vue";
|
||||||
|
import {ohlcStart} from "@/charts/chart-misc.js";
|
||||||
|
import Color from "color";
|
||||||
|
import OrderAmount from "@/components/chart/OrderAmount.vue";
|
||||||
|
import {MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js";
|
||||||
|
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
const os = useOrderStore()
|
|
||||||
const co = useChartOrderStore()
|
const co = useChartOrderStore()
|
||||||
const props = defineProps(['order', 'builder'])
|
const props = defineProps(['order', 'builder'])
|
||||||
const emit = defineEmits(['update:builder'])
|
const emit = defineEmits(['update:builder'])
|
||||||
|
|
||||||
const minWidth = computed(()=>co.intervalSecs)
|
|
||||||
const stdWidth = computed(()=>10 * minWidth.value)
|
|
||||||
|
|
||||||
function computeDefaultColor() {
|
function computeDefaultColor() {
|
||||||
const index = props.order.builders.indexOf(props.builder)
|
const index = props.order.builders.indexOf(props.builder)
|
||||||
return sideColor(props.order.buy, index)
|
return sideColor(props.order.buy, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultColor = computeDefaultColor()
|
const color = computed(()=>computeDefaultColor())
|
||||||
|
|
||||||
|
const defaultTranches = 10
|
||||||
|
|
||||||
builderDefaults(props.builder, {
|
builderDefaults(props.builder, {
|
||||||
timeA: s.clock, // todo relative 0
|
startTime: s.clock, // todo relative 0
|
||||||
timeB: null,
|
interval: co.intervalSecs,
|
||||||
|
tranches: defaultTranches,
|
||||||
|
percentage: 100/defaultTranches,
|
||||||
// relative: true, // todo
|
// relative: true, // todo
|
||||||
relative: false,
|
relative: false,
|
||||||
slippage: DEFAULT_SLIPPAGE,
|
slippage: DEFAULT_SLIPPAGE,
|
||||||
rungs: 1,
|
color: color.value,
|
||||||
skew: 0,
|
valid: true,
|
||||||
color: defaultColor,
|
|
||||||
buy: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const times = ref([])
|
function adjustShapes() {}
|
||||||
const weights = ref([])
|
|
||||||
|
|
||||||
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 endTimes = computed(()=>{
|
|
||||||
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 dateStrings = computed(()=>absoluteTimes.value.map((t)=>{
|
|
||||||
const n = DateTime.fromSeconds(t).setZone(s.timeZone)
|
|
||||||
const y = n.toLocaleString({year:'numeric'})
|
|
||||||
const m = n.toLocaleString({month:'long'})
|
|
||||||
const d = n.toLocaleString({day:'numeric'})
|
|
||||||
const h = n.toLocaleString({hour:'numeric', minute:'numeric'})
|
|
||||||
return `${y} ${m} ${d} ${h}`
|
|
||||||
}))
|
|
||||||
|
|
||||||
watchEffect(()=>{
|
|
||||||
// auto scroll
|
|
||||||
if (!dragging && absoluteTimes.value.length) {
|
|
||||||
const endTime = absoluteTimes.value[absoluteTimes.value.length-1]
|
|
||||||
const range = chart.getVisibleRange()
|
|
||||||
const width = range.to - range.from
|
|
||||||
const now = s.clock
|
|
||||||
const extra = (Math.max(0,endTime - now) + stdWidth.value/2) / width
|
|
||||||
// console.log('visrange', range, width, absV)
|
|
||||||
if (range.to < endTime) {
|
|
||||||
// console.log('scrolling')
|
|
||||||
chart.setVisibleRange({from: now - width, to: now}, {
|
|
||||||
percentRightMargin: Math.round(100 * extra),
|
|
||||||
applyDefaultRightMargin: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const absTimeA = computed({
|
|
||||||
get() { return _timeEndpoints.value[0] },
|
|
||||||
set(v) {
|
|
||||||
if (v!==null)
|
|
||||||
v = Number(v)
|
|
||||||
updateA(v)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const absTimeB = computed({
|
|
||||||
get() { return _timeEndpoints.value[1] },
|
|
||||||
set(v) {
|
|
||||||
if (v!==null)
|
|
||||||
v = Number(v)
|
|
||||||
updateB(v)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const _timeEndpoints = ref([props.builder.timeA, props.builder.timeB])
|
|
||||||
const timeEndpoints = computed({
|
|
||||||
get() { return _timeEndpoints.value},
|
|
||||||
set(v) {
|
|
||||||
const [a, b] = v
|
|
||||||
update(a,b, true, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
function updateA(a) {
|
|
||||||
update(a, _timeEndpoints.value[1], true, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function updateB(b) {
|
|
||||||
update(_timeEndpoints.value[0], b, false, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function update(a, b, updateA, updateB) {
|
|
||||||
if (updateA) {
|
|
||||||
const minB = a + minWidth.value
|
|
||||||
if (b < minB)
|
|
||||||
b = minB
|
|
||||||
}
|
|
||||||
if (updateB) {
|
|
||||||
const maxA = b - minWidth.value
|
|
||||||
if (a > maxA)
|
|
||||||
a = maxA
|
|
||||||
}
|
|
||||||
_timeEndpoints.value = [a, b]
|
|
||||||
const newBuilder = {...props.builder}
|
|
||||||
newBuilder.timeA = a
|
|
||||||
newBuilder.timeB = b
|
|
||||||
emit('update:builder', newBuilder)
|
|
||||||
}
|
|
||||||
|
|
||||||
const flipped = computed(()=>{
|
|
||||||
const a = props.builder.timeA
|
|
||||||
const b = props.builder.timeB
|
|
||||||
return a !== null && b !== null && a > b
|
|
||||||
})
|
|
||||||
|
|
||||||
function buildTranches() {
|
function buildTranches() {
|
||||||
const order = props.order
|
|
||||||
const builder = props.builder
|
|
||||||
const tranches = []
|
|
||||||
const warnings = []
|
const warnings = []
|
||||||
|
let interval = props.builder.interval
|
||||||
console.log('buildTranches', builder, order, tranches)
|
let parts = props.builder.tranches
|
||||||
const ts = times.value
|
if (interval < 60) {
|
||||||
const ets = endTimes.value
|
interval = 60
|
||||||
const ws = weights.value
|
parts = (props.builder.endTime - props.builder.startTime) / interval
|
||||||
console.log('buildTranches times ends weights', ts, ets, ws)
|
warnings.push(`DCA parts cannot be closer than one minute apart. Using ${parts} parts.`)
|
||||||
for(let i=0; i<ts.length; i++) {
|
|
||||||
const endTime = Math.max(ets[i],ts[i]+60);
|
|
||||||
console.log('time check', endTime, s.clock)
|
|
||||||
if (endTime <= s.clock)
|
|
||||||
warnings.push(`Tranche already expired at ${new Date(endTime*1000)}`)
|
|
||||||
const t = newTranche({
|
|
||||||
fraction: ws[i] * MAX_FRACTION,
|
|
||||||
startTime: ts[i],
|
|
||||||
endTime, // always give at least 60 seconds of window to execute
|
|
||||||
slippage: builder.slippage,
|
|
||||||
})
|
|
||||||
tranches.push(t)
|
|
||||||
}
|
}
|
||||||
|
const rate = Math.ceil(MAX_FRACTION / parts)
|
||||||
|
const endTime = Math.ceil(props.builder.endTime + 10*props.builder.tranches)
|
||||||
|
const startTime = Math.floor(props.builder.startTime);
|
||||||
|
const tranches = [newTranche({marketOrder:true,
|
||||||
|
startTime, endTime, rateLimitFraction: rate, rateLimitPeriod: Math.floor(interval)})]
|
||||||
return {tranches, warnings}
|
return {tranches, warnings}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parts = computed({
|
||||||
function getModelValue(model) {
|
get() { return props.builder.tranches },
|
||||||
if(!model) {
|
set(v) {
|
||||||
console.log('getModelValue', model)
|
v = Number(v)
|
||||||
return null
|
v = Math.max(1, Math.min(1000,v))
|
||||||
|
props.builder.tranches = v
|
||||||
|
let interval = (props.builder.endTime-props.builder.startTime) / v
|
||||||
|
if (interval < 60) {
|
||||||
|
interval = 60
|
||||||
|
props.builder.endTime = props.builder.startTime + interval * v
|
||||||
|
setPoints(null, true)
|
||||||
}
|
}
|
||||||
return model.time
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const intervalIsTotal = ref(false)
|
||||||
|
const displayedInterval = computed({
|
||||||
|
get() {
|
||||||
|
let result = props.builder.interval / timeUnits[timeUnitIndex.value][1]
|
||||||
|
if (intervalIsTotal.value)
|
||||||
|
// express as per-part intervals
|
||||||
|
result *= props.builder.tranches
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
v = Number(v)
|
||||||
|
let newValue = v * timeUnits[timeUnitIndex.value][1];
|
||||||
|
if (intervalIsTotal.value)
|
||||||
|
newValue /= props.builder.tranches
|
||||||
|
if (Math.abs(newValue - props.builder.interval) >= Number.EPSILON) {
|
||||||
|
props.builder.interval = newValue
|
||||||
|
props.builder.endTime = props.builder.startTime + newValue * props.builder.tranches
|
||||||
|
setPoints(null, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeUnits = [['minutes', 60], ['hours', 3600], ['days', 86400]]
|
||||||
|
const _timeUnitIndex = ref(0);
|
||||||
|
const timeUnitIndex = computed({
|
||||||
|
get() {return _timeUnitIndex.value},
|
||||||
|
set(v) {_timeUnitIndex.value = v % timeUnits.length}
|
||||||
|
})
|
||||||
|
const timeUnitsStr = computed(()=>timeUnits[timeUnitIndex.value][0])
|
||||||
|
|
||||||
|
function toggleTimeUnits() {
|
||||||
|
timeUnitIndex.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPointsValue(points) {
|
const startTime = computed({
|
||||||
return points[0].price
|
get() { return props.builder.relative ? s.clock + props.builder.startTime : props.builder.startTime },
|
||||||
|
set(v) {emitUpdate({startTime: v})}
|
||||||
|
})
|
||||||
|
|
||||||
|
const endTime = computed({
|
||||||
|
get() { return startTime.value + props.builder.interval * props.builder.tranches },
|
||||||
|
set(v) {
|
||||||
|
emitUpdate({endTime: v, interval: Math.abs(v - startTime.value)})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const barStart = computed(()=>ohlcStart(startTime.value, props.builder.interval))
|
||||||
|
const barEnd = computed(()=>ohlcStart(endTime.value, props.builder.interval))
|
||||||
|
let shapeWidth = barEnd.value - barStart.value // NOT reactively computed. We compare in onPoints()
|
||||||
|
|
||||||
|
function emitUpdatedPoints(a, b) {
|
||||||
|
const updates = {}
|
||||||
|
if (a.time !== barStart.value)
|
||||||
|
updates.startTime = a.time
|
||||||
|
// only set the end if it was moved relative to the start
|
||||||
|
if (b.time - a.time === barEnd.value - barStart.value)
|
||||||
|
// the end has moved exactly the same amount as the start. add a relative amount.
|
||||||
|
updates.endTime = a.time + (endTime.value - startTime.value)
|
||||||
|
else
|
||||||
|
updates.endTime = b.time
|
||||||
|
updates.interval = (b.time - a.time) / props.builder.tranches
|
||||||
|
emitUpdate(updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setModelValue(model, value) {
|
function setPoints(points=null, shapeCorrectionNeeded=false) {
|
||||||
// console.log('DCA set model value', model, value)
|
if (points === null)
|
||||||
// const v = value === null ? null : props.builder.relative ? s.clock + Math.round(value) : Math.round(value)
|
points = [{time:props.builder.startTime}, {time:props.builder.endTime}]
|
||||||
const v = value === null ? null : Math.round(value)
|
let [a, b] = points
|
||||||
if (model.time !== v) {
|
const period = co.intervalSecs;
|
||||||
// console.log('DCA do set time', v)
|
|
||||||
model.time = v
|
if (b.time < a.time) {
|
||||||
|
[a, b] = [b, a]
|
||||||
|
shapeCorrectionNeeded = true
|
||||||
|
} else if (b.time === a.time) {
|
||||||
|
b.time = a.time + period
|
||||||
|
shapeCorrectionNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const curBarStart = ohlcStart(s.clock, period);
|
||||||
|
if (a.time < curBarStart) {
|
||||||
|
const width = b.time - a.time;
|
||||||
|
a.time = s.clock
|
||||||
|
b.time = a.time + width
|
||||||
|
shapeCorrectionNeeded = true
|
||||||
|
}
|
||||||
|
emitUpdatedPoints(a, b);
|
||||||
|
if (shapeCorrectionNeeded) {
|
||||||
|
const [sa, sb] = shape.getPoints()
|
||||||
|
const aTime = ohlcStart(a.time, period);
|
||||||
|
const bTime = ohlcStart(b.time, period);
|
||||||
|
if (sa.time !== aTime || sb.time !== bTime) {
|
||||||
|
const aa = {time: aTime}
|
||||||
|
const bb = {time: bTime}
|
||||||
|
shape.setPoints([aa, bb])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setValues(values) {
|
const callbacks = {
|
||||||
times.value = values.map((t)=>Math.round(t))
|
onDrag(shapeId, shape, points) {
|
||||||
|
let [a, b] = points
|
||||||
|
if (b.time < a.time)
|
||||||
|
[a, b] = [b, a]
|
||||||
|
emitUpdatedPoints(a, b)
|
||||||
|
},
|
||||||
|
onPoints(shapeId, shape, points) {
|
||||||
|
setPoints(points, shape);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWeights(ws) { weights.value = ws }
|
function emitUpdate(changes) {
|
||||||
|
for (const [k, v] of Object.entries(changes))
|
||||||
|
props.builder[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = computed(()=>{
|
||||||
|
let msg = props.order.buy ? 'Buy ' : 'Sell '
|
||||||
|
msg += props.builder.tranches + ' parts'
|
||||||
|
return msg
|
||||||
|
})
|
||||||
|
|
||||||
|
let shapeId = createShape(ShapeType.DateRange, [{time:barStart.value}, {time:barEnd.value}], {}, callbacks)
|
||||||
|
let shape = widget.activeChart().getShapeById(shapeId)
|
||||||
|
function setProperties() {
|
||||||
|
const color = computeDefaultColor()
|
||||||
|
shape.setProperties({
|
||||||
|
extendTop: true,
|
||||||
|
extendBottom: true,
|
||||||
|
linecolor: color,
|
||||||
|
backgroundColor: new Color(color).alpha(0.2).toString(),
|
||||||
|
backgroundTransparency: 60,
|
||||||
|
customText: {
|
||||||
|
text: text.value,
|
||||||
|
visible: true,
|
||||||
|
color: color,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setProperties()
|
||||||
|
|
||||||
|
watchEffect(setProperties)
|
||||||
|
|
||||||
|
function deleteShapes() {
|
||||||
|
deleteShapeId(shapeId)
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -251,5 +270,11 @@ td.weight {
|
|||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
}
|
}
|
||||||
|
.parts {
|
||||||
|
width: 8em;
|
||||||
|
}
|
||||||
|
.interval {
|
||||||
|
width: 22em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
256
src/components/chart/DateBuilder.vue
Normal file
256
src/components/chart/DateBuilder.vue
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
<template class="d-flex align-content-center flex-column" style="height: 100%; width: 100%;">
|
||||||
|
<rung-builder name='Dates' :order="order" :builder="builder" v-model="timeEndpoints"
|
||||||
|
:shape="VLine"
|
||||||
|
:mode="1" :flip="flipped" :orientation="0"
|
||||||
|
:get-model-value="getModelValue" :set-model-value="setModelValue"
|
||||||
|
:get-points-value="getPointsValue"
|
||||||
|
:set-values="setValues" :set-weights="setWeights"
|
||||||
|
: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>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<absolute-time-entry v-model="absTimeA"/>
|
||||||
|
</td>
|
||||||
|
<td class="weight">{{ weights.length ? allocationTexts[0] : '' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="weights.length>2" v-for="i in weights.length-2" class="ml-5"> <!-- vue uses 1-based loops -->
|
||||||
|
<td class="d-flex justify-end pr-3">{{ dateStrings[i] }}</td>
|
||||||
|
<td class="weight">{{ allocationTexts[i] }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="weights.length>1">
|
||||||
|
<td>
|
||||||
|
<absolute-time-entry v-model="absTimeB"/>
|
||||||
|
</td>
|
||||||
|
<td class="weight">{{ allocationTexts[weights.length-1] }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
</rung-builder>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {builderDefaults, DEFAULT_SLIPPAGE, MIN_EXECUTION_TIME, useChartOrderStore} from "@/orderbuild.js";
|
||||||
|
import {allocationText, VLine} from "@/charts/shape.js";
|
||||||
|
import {sideColor} from "@/misc.js";
|
||||||
|
import {useOrderStore, usePrefStore, 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, watchEffect} from "vue";
|
||||||
|
import {chart, dragging} from "@/charts/chart.js";
|
||||||
|
import AbsoluteTimeEntry from "@/components/AbsoluteTimeEntry.vue";
|
||||||
|
import {DateTime} from "luxon";
|
||||||
|
|
||||||
|
const s = useStore()
|
||||||
|
const os = useOrderStore()
|
||||||
|
const co = useChartOrderStore()
|
||||||
|
const prefs = usePrefStore()
|
||||||
|
const props = defineProps(['order', 'builder'])
|
||||||
|
const emit = defineEmits(['update:builder'])
|
||||||
|
|
||||||
|
const minWidth = computed(()=>co.intervalSecs)
|
||||||
|
const stdWidth = computed(()=>10 * minWidth.value)
|
||||||
|
|
||||||
|
function computeDefaultColor() {
|
||||||
|
const index = props.order.builders.indexOf(props.builder)
|
||||||
|
return sideColor(props.order.buy, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultColor = computeDefaultColor()
|
||||||
|
|
||||||
|
builderDefaults(props.builder, {
|
||||||
|
timeA: s.clock, // todo relative 0
|
||||||
|
timeB: null,
|
||||||
|
// relative: true, // todo
|
||||||
|
relative: false,
|
||||||
|
slippage: DEFAULT_SLIPPAGE,
|
||||||
|
rungs: 1,
|
||||||
|
skew: 0,
|
||||||
|
color: defaultColor,
|
||||||
|
buy: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const times = ref([])
|
||||||
|
const weights = ref([])
|
||||||
|
|
||||||
|
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 endTimes = computed(()=>{
|
||||||
|
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 dateStrings = computed(()=>absoluteTimes.value.map((t)=>{
|
||||||
|
const n = DateTime.fromSeconds(t).setZone(prefs.timezone)
|
||||||
|
const y = n.toLocaleString({year:'numeric'})
|
||||||
|
const m = n.toLocaleString({month:'long'})
|
||||||
|
const d = n.toLocaleString({day:'numeric'})
|
||||||
|
const h = n.toLocaleString({hour:'numeric', minute:'numeric'})
|
||||||
|
return `${y} ${m} ${d} ${h}`
|
||||||
|
}))
|
||||||
|
|
||||||
|
watchEffect(()=>{
|
||||||
|
// auto scroll
|
||||||
|
if (!dragging && absoluteTimes.value.length) {
|
||||||
|
const endTime = absoluteTimes.value[absoluteTimes.value.length-1]
|
||||||
|
const range = chart.getVisibleRange()
|
||||||
|
const width = range.to - range.from
|
||||||
|
const now = s.clock
|
||||||
|
const extra = (Math.max(0,endTime - now) + stdWidth.value/2) / width
|
||||||
|
// console.log('visrange', range, width, absV)
|
||||||
|
if (range.to < endTime) {
|
||||||
|
// console.log('scrolling')
|
||||||
|
chart.setVisibleRange({from: now - width, to: now}, {
|
||||||
|
percentRightMargin: Math.round(100 * extra),
|
||||||
|
applyDefaultRightMargin: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const absTimeA = computed({
|
||||||
|
get() { return _timeEndpoints.value[0] },
|
||||||
|
set(v) {
|
||||||
|
if (v!==null)
|
||||||
|
v = Number(v)
|
||||||
|
updateA(v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const absTimeB = computed({
|
||||||
|
get() { return _timeEndpoints.value[1] },
|
||||||
|
set(v) {
|
||||||
|
if (v!==null)
|
||||||
|
v = Number(v)
|
||||||
|
updateB(v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const _timeEndpoints = ref([props.builder.timeA, props.builder.timeB])
|
||||||
|
const timeEndpoints = computed({
|
||||||
|
get() { return _timeEndpoints.value},
|
||||||
|
set(v) {
|
||||||
|
const [a, b] = v
|
||||||
|
update(a,b, true, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function updateA(a) {
|
||||||
|
update(a, _timeEndpoints.value[1], true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateB(b) {
|
||||||
|
update(_timeEndpoints.value[0], b, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function update(a, b, updateA, updateB) {
|
||||||
|
if (updateA) {
|
||||||
|
const minB = a + minWidth.value
|
||||||
|
if (b < minB)
|
||||||
|
b = minB
|
||||||
|
}
|
||||||
|
if (updateB) {
|
||||||
|
const maxA = b - minWidth.value
|
||||||
|
if (a > maxA)
|
||||||
|
a = maxA
|
||||||
|
}
|
||||||
|
_timeEndpoints.value = [a, b]
|
||||||
|
const newBuilder = {...props.builder}
|
||||||
|
newBuilder.timeA = a
|
||||||
|
newBuilder.timeB = b
|
||||||
|
emit('update:builder', newBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
const flipped = computed(()=>{
|
||||||
|
const a = props.builder.timeA
|
||||||
|
const b = props.builder.timeB
|
||||||
|
return a !== null && b !== null && a > b
|
||||||
|
})
|
||||||
|
|
||||||
|
function buildTranches() {
|
||||||
|
const order = props.order
|
||||||
|
const builder = props.builder
|
||||||
|
const tranches = []
|
||||||
|
const warnings = []
|
||||||
|
|
||||||
|
console.log('buildTranches', builder, order, tranches)
|
||||||
|
const ts = times.value
|
||||||
|
const ets = endTimes.value
|
||||||
|
const ws = weights.value
|
||||||
|
console.log('buildTranches times ends weights', ts, ets, ws)
|
||||||
|
for(let i=0; i<ts.length; i++) {
|
||||||
|
const endTime = Math.max(ets[i],ts[i]+60);
|
||||||
|
console.log('time check', endTime, s.clock)
|
||||||
|
if (endTime <= s.clock)
|
||||||
|
warnings.push(`Tranche already expired at ${new Date(endTime*1000)}`)
|
||||||
|
const t = newTranche({
|
||||||
|
fraction: ws[i] * MAX_FRACTION,
|
||||||
|
startTime: ts[i],
|
||||||
|
endTime, // always give at least 60 seconds of window to execute
|
||||||
|
slippage: builder.slippage,
|
||||||
|
})
|
||||||
|
tranches.push(t)
|
||||||
|
}
|
||||||
|
return {tranches, warnings}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getModelValue(model) {
|
||||||
|
if(!model) {
|
||||||
|
console.log('getModelValue', model)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return model.time
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPointsValue(points) {
|
||||||
|
return points[0].price
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
const v = value === null ? null : Math.round(value)
|
||||||
|
if (model.time !== v) {
|
||||||
|
// console.log('DCA do set time', v)
|
||||||
|
model.time = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValues(values) {
|
||||||
|
times.value = values.map((t)=>Math.round(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWeights(ws) { weights.value = ws }
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
td.weight {
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
100
src/components/chart/OrderAmount.vue
Normal file
100
src/components/chart/OrderAmount.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<v-text-field type="number" inputmode="numeric" pattern="[0-9]*\.?[0-9]*" v-model="amount" variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
:hint="available" :persistent-hint="true"
|
||||||
|
min="0"
|
||||||
|
class="amount py-2" :color="color"
|
||||||
|
:label="props.order.amountIsTokenA ?
|
||||||
|
(multiplier ? 'Amount each' : 'Amount') :
|
||||||
|
((multiplier ? 'Value each in ' : 'Value in ')+co.selectedSymbol.quote.s)">
|
||||||
|
<template #prepend>
|
||||||
|
<v-btn v-if="!multiplier"
|
||||||
|
variant="outlined" :color="color" class="ml-3"
|
||||||
|
:text="(props.order.buy ? 'Buy ' : 'Sell ') + co.selectedSymbol.base.s"
|
||||||
|
@click="props.order.buy=!props.order.buy"/>
|
||||||
|
</template>
|
||||||
|
<template #prepend-inner>
|
||||||
|
<v-btn variant="text" text="max" class="px-0" size="small"
|
||||||
|
:disabled="!maxAmount || props.order.amountIsTokenA===props.order.buy && !co.price" @click="setMax"/>
|
||||||
|
</template>
|
||||||
|
<template #append-inner>
|
||||||
|
<v-btn :text="props.order.amountIsTokenA?co.baseToken.s:co.quoteToken.s+' worth'"
|
||||||
|
:color="color" variant="text" @click="toggleAmountToken" style="width: 7em"/>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useChartOrderStore} from "@/orderbuild.js";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {useStore} from "@/store/store.js";
|
||||||
|
|
||||||
|
const props = defineProps(['order', 'multiplier', 'color'])
|
||||||
|
|
||||||
|
const s = useStore()
|
||||||
|
const co = useChartOrderStore()
|
||||||
|
|
||||||
|
const amount = computed({
|
||||||
|
get() {
|
||||||
|
let result = props.order.amount
|
||||||
|
if (result !== null && props.multiplier)
|
||||||
|
result /= props.multiplier
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
if (v !== null && props.multiplier)
|
||||||
|
v *= props.multiplier
|
||||||
|
props.order.amount = v
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const available = computed(()=>{
|
||||||
|
const max = maxAmount.value
|
||||||
|
return max === null ? '' : `Available: ${maxAmount.value} ${inToken.value.s}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const inToken = computed( ()=>props.order.buy ? co.quoteToken : co.baseToken )
|
||||||
|
|
||||||
|
const maxAmount = computed(()=>{
|
||||||
|
const token = inToken.value;
|
||||||
|
if (!token)
|
||||||
|
return null
|
||||||
|
const balance = s.balances[token.a]
|
||||||
|
if( !balance )
|
||||||
|
return null
|
||||||
|
return balance / 10**token.d
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastMaxValue = ref(-1)
|
||||||
|
|
||||||
|
function setMax() {
|
||||||
|
let amount = maxAmount.value
|
||||||
|
if (amount) {
|
||||||
|
const order = props.order
|
||||||
|
const price = co.price
|
||||||
|
if (order.amountIsTokenA===order.buy) {
|
||||||
|
if (order.buy)
|
||||||
|
amount /= price
|
||||||
|
else
|
||||||
|
amount *= price
|
||||||
|
}
|
||||||
|
amount = Number(amount.toPrecision(7))
|
||||||
|
lastMaxValue.value = amount
|
||||||
|
order.amount = amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAmountToken() {
|
||||||
|
const order = props.order
|
||||||
|
order.amountIsTokenA=!order.amountIsTokenA
|
||||||
|
if (order.amount === lastMaxValue.value)
|
||||||
|
setMax()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.amount {
|
||||||
|
max-width: 30em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -97,7 +97,7 @@ export function intervalString(seconds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function timestampString(seconds) {
|
export function timestampString(seconds) {
|
||||||
const date = DateTime.fromSeconds(seconds).setZone(useStore().timeZone)
|
const date = DateTime.fromSeconds(seconds).setZone(usePrefStore().timezone)
|
||||||
return dateString(date)
|
return dateString(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ const REQUIRE_APPROVAL = import.meta.env.VITE_REQUIRE_APPROVAL !== 'NO';
|
|||||||
export const useStore = defineStore('app', ()=> {
|
export const useStore = defineStore('app', ()=> {
|
||||||
const clock = ref(timestamp()) // the clock ticks infrequently enough to be mostly stable for user display
|
const clock = ref(timestamp()) // the clock ticks infrequently enough to be mostly stable for user display
|
||||||
setInterval(()=>clock.value=timestamp(), 10*1000) // 10 secs
|
setInterval(()=>clock.value=timestamp(), 10*1000) // 10 secs
|
||||||
const timeZone = ref('Etc/UTC')
|
|
||||||
|
|
||||||
const nav = ref(false) // controls opening navigation drawer
|
const nav = ref(false) // controls opening navigation drawer
|
||||||
const theme = ref('dark')
|
const theme = ref('dark')
|
||||||
@@ -144,7 +143,7 @@ export const useStore = defineStore('app', ()=> {
|
|||||||
transactionSenders, errors, extraTokens, poolPrices, vaultBalances, orders, vault, version, upgrade, vaultOrders,
|
transactionSenders, errors, extraTokens, poolPrices, vaultBalances, orders, vault, version, upgrade, vaultOrders,
|
||||||
tokens, factory, helper, theme,
|
tokens, factory, helper, theme,
|
||||||
mockenv, mockCoins,
|
mockenv, mockCoins,
|
||||||
removeTransactionSender, error, closeError, addToken, clock, timeZone, balances,
|
removeTransactionSender, error, closeError, addToken, clock, balances,
|
||||||
approved, regionApproved, walletApproved,
|
approved, regionApproved, walletApproved,
|
||||||
getBalance
|
getBalance
|
||||||
}
|
}
|
||||||
@@ -215,7 +214,8 @@ export const usePrefStore = defineStore({
|
|||||||
const acceptedTos = ref('NO TOS ACCEPTED')
|
const acceptedTos = ref('NO TOS ACCEPTED')
|
||||||
const selectedTicker = ref(null)
|
const selectedTicker = ref(null)
|
||||||
const selectedTimeframe = ref(null)
|
const selectedTimeframe = ref(null)
|
||||||
return {inverted, acceptedTos, selectedTicker, selectedTimeframe}
|
const timezone = ref('Etc/UTC')
|
||||||
|
return {inverted, acceptedTos, selectedTicker, selectedTimeframe, timezone}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user