Files
web/src/orderbuild.js
2024-04-21 19:44:03 -04:00

286 lines
9.3 KiB
JavaScript

import {routeInverted, timestamp, uuid} from "@/misc.js";
import {MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js";
import {useOrderStore, useStore} from "@/store/store.js";
import {encodeIEE754} from "@/common.js";
import {defineStore} from "pinia";
import {computed, ref} from "vue";
import Color from "color";
export const MIN_EXECUTION_TIME = 60 // give at least one full minute for each tranche to trigger
function unimplemented() { throw Error('Unimplemented') }
// Builders are data objects which store a configuration state
// the component name must match a corresponding Vue component in the BuilderFactory.vue component, which is responsible
// for instantiating the UI component for a given builder dictionary, based on its builder.component field.
export function newBuilder( component, options = {}) {
const id = uuid()
return {
id, component, options,
allocation: 1.0, points: {}, shapes: {}, props: {}, valid: false,
}
}
// Orders hold an amount and builders
// noinspection JSUnusedLocalSymbols
const Order = {
id: uuid(),
amount: 0,
builders: [],
}
// the key is order.id and the value is a function() that returns an order
export const orderFuncs = {}
// the key is order.builder.id and the value is a function() that returns an array of tranches
// if null is returned, the builder inputs are not valid and the place order button will be disabled
export const builderFuncs = {}
function newDefaultOrder() {
return { id:uuid(), valid: false, amount:null, amountIsTokenA: true, buy: true, builders:[] }
}
export const useChartOrderStore = defineStore('chart_orders', () => {
const chartReady = ref(false)
const defaultOrder = newDefaultOrder()
const orders = ref([defaultOrder]) // order models in UI format
const selectedOrder = ref(null)
const selectedSymbol = ref(null)
const selectedPool = ref(null)
const intervalSecs = ref(0)
const baseToken = computed(()=>selectedSymbol.value === null ? null : selectedSymbol.value.base)
const quoteToken = computed(()=>selectedSymbol.value === null ? null : selectedSymbol.value.quote)
const price = computed(() => {
if (!selectedPool.value || !selectedSymbol.value)
return null
const s = useStore()
let result = s.poolPrices[[s.chainId, selectedPool.value[0]]]
if (selectedSymbol.value.inverted)
result = 1 / result
return result
})
const drawing = ref(false)
function newOrder() {
const order = newDefaultOrder()
orders.value.push(order)
selectedOrder.value = order
}
function removeOrder(order) {
let index = orders.value.findIndex((o)=>o.id===order.id)
if (index === -1) return
const result = orders.value.filter((o)=>o.id!==order.id)
if (result.length === 0) {
const order = newDefaultOrder()
result.push(order)
selectedOrder.value = order
}
else
selectedOrder.value = result[Math.max(0,index-1)] // select the order above the removed one
orders.value = result
}
function resetOrders() {
const order = newDefaultOrder()
orders.value = [order]
selectedOrder.value = order
}
return {
chartReady, selectedSymbol, selectedPool, intervalSecs, baseToken, quoteToken, price,
orders, drawing, newOrder, removeOrder, resetOrders,
}
})
export function applyLimit(tranche, price = null, isMinimum = null) {
if (price === null) {
const os = useOrderStore()
price = os.limitPrice
if (!price)
return
}
applyLine(tranche, price, 0, isMinimum)
}
function computeInterceptSlope(time0, price0, time1, price1) {
if (!time0 || !price0 && price0 !== 0 || !time1 || !price1 && price1 !== 0)
throw Error(`invalid line points data ${time0} ${price0} ${time1} ${price1}`)
const t0 = time0
const t1 = time1
if (t0 === t1)
throw Error("line points' times must be different")
const slope = (price1 - price0) / (t1 - t0)
const intercept = price1 - slope * t1
return [intercept, slope]
}
export function linePointsValue(time0, price0, time1, price1, unixTime = null) {
if (unixTime === null)
unixTime = timestamp()
try {
const [intercept, slope] = computeInterceptSlope(time0, price0, time1, price1)
return intercept + unixTime * slope
} catch (e) {
console.log('error computing line', e)
return null
}
}
export function applyLinePoints(tranche, time0, price0, time1, price1, isMinimum = null) {
const [intercept, slope] = computeInterceptSlope(time0, price0, time1, price1);
applyLine(tranche, intercept, slope, isMinimum)
}
export function applyLine(tranche, intercept, slope, isMinimum = null) {
console.log('intercept, slope', intercept, slope)
// intercept and slope are still in "human" units of decimal-adjusted prices
const os = useOrderStore()
const route = os.route
const inverted = routeInverted(route)
const scale = 10 ** (os.tokenA.d - os.tokenB.d)
let m = inverted ? -scale / slope : slope / scale
let b = inverted ? scale / intercept : intercept / scale
const cur = b + timestamp() * m
console.log('inverted b, m, cur', inverted, b, m, cur)
m = encodeIEE754(m)
b = encodeIEE754(b)
if (isMinimum === null)
isMinimum = os.limitIsMinimum
console.log('limit is minimum', isMinimum)
if (isMinimum) {
tranche.minIntercept = b;
tranche.minSlope = m;
} else {
tranche.maxIntercept = b;
tranche.maxSlope = m;
}
tranche.marketOrder = false;
}
export function applyLine2(tranche, isMinimum, intercept, slope, poolDecimals, inverted) {
console.log('intercept, slope', intercept, slope)
// intercept and slope are still in "human" units of decimal-adjusted prices
const scale = 10 ** -poolDecimals
let m = slope === 0 ? 0 : inverted ? -scale / slope : scale * slope
let b = inverted ? scale / intercept : scale * intercept
const cur = b + timestamp() * m
console.log('inverted b, m, cur', inverted, b, m, cur)
m = encodeIEE754(m)
b = encodeIEE754(b)
if (inverted)
isMinimum = !isMinimum
if (isMinimum) {
tranche.minIntercept = b;
tranche.minSlope = m;
} else {
tranche.maxIntercept = b;
tranche.maxSlope = m;
}
tranche.marketOrder = false;
}
export function timesliceTranches() {
const ts = []
const os = useOrderStore()
const n = os.tranches // num tranches
const interval = os.interval;
const timeUnitIndex = os.timeUnitIndex;
let duration =
timeUnitIndex === 0 ? interval * 60 : // minutes
timeUnitIndex === 1 ? interval * 60 * 60 : // hours
interval * 24 * 60 * 60; // days
let window
if (!os.intervalIsTotal) {
window = duration
duration *= n // duration is the total time for all tranches
} else {
window = Math.round(duration / n)
}
if (window < 60)
window = 60 // always allow at least one minute for execution
const amtPerTranche = Math.ceil(MAX_FRACTION / n)
duration -= 15 // subtract 15 seconds so the last tranche completes before the deadline
for (let i = 0; i < n; i++) {
const start = Math.floor(i * (duration / Math.max((n - 1), 1)))
const end = start + window
ts.push(newTranche({
fraction: amtPerTranche,
startTimeIsRelative: true,
startTime: start,
endTimeIsRelative: true,
endTime: end,
}))
}
return ts
}
export function builderDefaults(builder, defaults) {
for (const k in defaults)
if (builder[k] === undefined)
builder[k] = defaults[k] instanceof Function ? defaults[k]() : defaults[k]
}
export function linearWeights(n, s) {
if (n === 1) return [1]
const result = []
if (s === 0) {
// equal weighted
for (let i = 0; i < n; i++)
result.push(1 / n)
} else if (s === 1) {
result.push(1)
for (let i = 1; i < n; i++)
result.push(0)
} else if (s === -1) {
for (let i = 1; i < n; i++)
result.push(0)
result.push(1)
} else {
for (let i = 0; i < n; i++)
result.push((1 - s * (2 * i / (n - 1) - 1)) / n)
}
// console.log('weights', result)
return result
}
export function weightColors(weights, color) {
const c = new Color(color).rgb()
const max = Math.max(...weights)
const ns = weights.map((w) => w / max) // set largest weight to 100%
const adj = ns.map((w) => c.alpha(Math.pow(w, 0.67))) // https://en.wikipedia.org/wiki/Stevens's_power_law
return adj.map((a) => a.string())
}
export function allocationText(weight, amount, symbol) {
let text = ''
const hasWeight = weight!==null && weight!==undefined
if (hasWeight)
text += `${(weight * 100).toFixed(1)}%`
const hasAmount = amount!==null && amount!==undefined && amount > 0
const hasSymbol = symbol!==null && symbol!==undefined
if (hasAmount && hasSymbol) {
if (hasWeight)
text += ' = '
text += `${amount.toLocaleString('fullwide')} ${symbol}`
}
return text
}
export function deleteBuilder(order, builder) {
order.builders = order.builders.filter((b) => b !== builder)
}