rate limit DCA

This commit is contained in:
tim
2025-03-19 20:27:10 -04:00
parent cd84e7c3c9
commit 8750951de5
14 changed files with 651 additions and 325 deletions

View File

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

View File

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

View File

@@ -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}])
}
}

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<template> <template>
<v-text-field label="Tranches" type="number" variant="outlined" aria-valuemin="1" min="1" max="255" <v-text-field label="Tranches" type="number" variant="outlined" aria-valuemin="1" min="1" max="255"
v-model="os.tranches" :rules="[validateRequired,validateTranches]" v-auto-select> v-model="os.tranches" :rules="[validateRequired,validateTranches]" v-auto-select>
<template v-slot:append-inner>tranches</template> <template v-slot:append-inner>tranches</template>
</v-text-field> </v-text-field>
<v-text-field type="number" variant="outlined" :min="1" v-model="os.interval" class="interval" <v-text-field type="number" variant="outlined" :min="1" v-model="os.interval" class="interval"
:label="os.intervalIsTotal ? 'Completion time' : 'Time between tranches'" v-auto-select> :label="os.intervalIsTotal ? 'Completion time' : 'Time between tranches'" v-auto-select>
<template v-slot:prepend-inner> <template v-slot:prepend-inner>

View File

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

View File

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

View File

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

View File

@@ -1,249 +1,268 @@
<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 ) function buildTranches() {
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(props.order.buy, w, w * props.order.amount, co.selectedSymbol.base.s, amountSymbol.value))) const warnings = []
let interval = props.builder.interval
let parts = props.builder.tranches
if (interval < 60) {
interval = 60
parts = (props.builder.endTime - props.builder.startTime) / interval
warnings.push(`DCA parts cannot be closer than one minute apart. Using ${parts} parts.`)
}
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}
}
const endTimes = computed(()=>{ const parts = computed({
const ts = times.value get() { return props.builder.tranches },
const window = Math.max(MIN_EXECUTION_TIME, Math.floor((ts[ts.length-1]-ts[0])/props.builder.rungs)) set(v) {
return ts.map((t)=>t+window) v = Number(v)
}) v = Math.max(1, Math.min(1000,v))
const absoluteTimes = computed(()=>{ props.builder.tranches = v
// console.log('absoluteTimes', props.builder.relative, times.value) let interval = (props.builder.endTime-props.builder.startTime) / v
// if (!props.builder.relative) if (interval < 60) {
return times.value interval = 60
// const now = s.clock props.builder.endTime = props.builder.startTime + interval * v
// return times.value.map((t)=>now+t) setPoints(null, true)
})
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 intervalIsTotal = ref(false)
const absTimeA = computed({ const displayedInterval = computed({
get() { return _timeEndpoints.value[0] }, 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) { set(v) {
if (v!==null) v = Number(v)
v = Number(v) let newValue = v * timeUnits[timeUnitIndex.value][1];
updateA(v) 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 absTimeB = computed({
get() { return _timeEndpoints.value[1] }, 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
}
const startTime = computed({
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) { set(v) {
if (v!==null) emitUpdate({endTime: v, interval: Math.abs(v - startTime.value)})
v = Number(v)
updateB(v)
} }
}) })
const _timeEndpoints = ref([props.builder.timeA, props.builder.timeB]) const barStart = computed(()=>ohlcStart(startTime.value, props.builder.interval))
const timeEndpoints = computed({ const barEnd = computed(()=>ohlcStart(endTime.value, props.builder.interval))
get() { return _timeEndpoints.value}, let shapeWidth = barEnd.value - barStart.value // NOT reactively computed. We compare in onPoints()
set(v) {
const [a, b] = v function emitUpdatedPoints(a, b) {
update(a,b, true, true) 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 setPoints(points=null, shapeCorrectionNeeded=false) {
if (points === null)
points = [{time:props.builder.startTime}, {time:props.builder.endTime}]
let [a, b] = points
const period = co.intervalSecs;
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])
}
}
}
const callbacks = {
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 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)
function updateA(a) { let shape = widget.activeChart().getShapeById(shapeId)
update(a, _timeEndpoints.value[1], true, false) 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 updateB(b) { function deleteShapes() {
update(_timeEndpoints.value[0], b, false, true) deleteShapeId(shapeId)
} }
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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -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>

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

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

View File

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

View File

@@ -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}
}, },
}) })