UNPKG

oswap-v2-sdk

Version:
338 lines (282 loc) 12.7 kB
const _ = require('lodash'); const poolLib = require('./pool-lib.js'); const { $swap, $trade_l_shares, $redeem_shares, $charge_interest, $get_utilization_ratio } = poolLib; function require_cond(cond, err) { if (!cond) throw Error(err); } let log = () => { } function setLogger(logger) { poolLib.setLogger(logger); log = logger; } function getPoolState(aaParams, stateVars) { const { balances, leveraged_balances, profits, recent, lp_shares } = stateVars; const { x_asset, y_asset } = aaParams; const getParam = (name, default_value) => { if (stateVars[name] !== undefined) return stateVars[name]; if (aaParams[name] !== undefined) return aaParams[name]; return default_value; }; const alpha = getParam('alpha', 0.5); const beta = 1 - alpha; const mid_price = getParam('mid_price', 0); const gamma = getParam('price_deviation', 0); let x0 = 0, y0 = 0, p_max, p_min; if (mid_price) { const s = lp_shares.linear; const s_curve = s * lp_shares.coef; x0 = s_curve / mid_price ** beta / gamma; y0 = x0 * mid_price; p_max = alpha / beta * gamma ** (1 / beta) * mid_price; p_min = alpha / beta / gamma ** (1 / alpha) * mid_price; } const shifts = { x0, y0 }; const bounds = { p_max, p_min }; const pool_props = { alpha, beta, gamma, mid_price, Lambda: getParam('pool_leverage', 1), swap_fee: getParam('swap_fee', 0.003), exit_fee: getParam('exit_fee', 0.005), arb_profit_tax: getParam('arb_profit_tax', 0), leverage_profit_tax: getParam('leverage_profit_tax', 0), leverage_token_tax: getParam('leverage_token_tax', 0), period_length: getParam('period_length', 3600), base_interest_rate: getParam('base_interest_rate', 0.2), x_asset, y_asset, shares_bonding_curve: aaParams.shares_bonding_curve || 'IXBHF6T4IKMYAFGRM54F5FVMXGKCTFNT', }; return { balances, leveraged_balances, profits, recent, lp_shares, pool_props, shifts, bounds }; } function toAsset(token, pool_props) { const { x_asset, y_asset } = pool_props; if (token === 'x') return x_asset; if (token === 'y') return y_asset; require_cond(token === x_asset || token === y_asset, `unknown token ${token}`); return token; } function toXY(token, pool_props) { const { x_asset, y_asset } = pool_props; if (token === x_asset) return 'x'; if (token === y_asset) return 'y'; require_cond(token === 'x' || token === 'y', `unknown token ${token}`); return token; } function getPrice(poolState) { const { balances, shifts: { x0, y0 }, pool_props: { alpha, beta } } = poolState; return alpha / beta * (balances.y + y0) / (balances.x + x0); } function getCurrentUtilizationRatio(poolState) { const { balances, leveraged_balances, shifts: { x0, y0 }, pool_props: { alpha } } = poolState; return $get_utilization_ratio(balances, leveraged_balances, x0, y0, alpha); } function getInterestRate(poolState) { return poolState.pool_props.base_interest_rate / (1 - getCurrentUtilizationRatio(poolState)); } // modifies balances, leveraged_balances, and profits fields of poolState function chargeInterest(poolState) { const { balances, leveraged_balances, profits, recent: { last_ts }, shifts: { x0, y0 }, pool_props } = poolState; const { alpha, Lambda } = pool_props; const i = getInterestRate(poolState); $charge_interest(balances, leveraged_balances, profits, x0, y0, last_ts, i, alpha, Lambda); } function getSwapParams(in_amount, in_token, poolState) { poolState = _.cloneDeep(poolState); // make a copy so that we don't modify the input params chargeInterest(poolState); const { balances, leveraged_balances, profits, recent, shifts: { x0, y0 }, pool_props } = poolState; in_token = toXY(in_token, pool_props); const y_in = in_token === 'y'; const { res, required_amount, param_value } = findParamToMatchAmount(in_amount, in_amount, delta_Yn => { const res = $swap(_.cloneDeep(balances), _.cloneDeep(leveraged_balances), _.cloneDeep(profits), _.cloneDeep(recent), x0, y0, y_in, delta_Yn, 0, -1, 0, 'ADDRESS', pool_props); return { res, required_amount: res.amount_Y }; }); return { res, delta_Yn: param_value }; } function getSwapParamsByOutput(out_amount, out_token, poolState) { poolState = _.cloneDeep(poolState); // make a copy so that we don't modify the input params chargeInterest(poolState); const { balances, leveraged_balances, profits, recent, shifts: { x0, y0 }, pool_props } = poolState; out_token = toXY(out_token, pool_props); const y_in = out_token === 'x'; const in_token = out_token === 'x' ? 'y' : 'x'; const delta_Yn_initial_estimation = balances[in_token + 'n'] * out_amount / (balances[out_token + 'n'] - out_amount); const { res, required_amount, param_value } = findParamToMatchAmount(out_amount, delta_Yn_initial_estimation, delta_Yn => { const res = $swap(_.cloneDeep(balances), _.cloneDeep(leveraged_balances), _.cloneDeep(profits), _.cloneDeep(recent), x0, y0, y_in, delta_Yn, 0, -1, 0, 'ADDRESS', pool_props); return { res, required_amount: res.net_amount_X }; }); return { res, delta_Yn: param_value }; } function getLeveragedBuyParams(in_amount, in_token, leverage, poolState) { poolState = _.cloneDeep(poolState); // make a copy so that we don't modify the input params chargeInterest(poolState); const { balances, leveraged_balances, profits, recent, shifts: { x0, y0 }, pool_props } = poolState; in_token = toAsset(in_token, pool_props); const { res, required_amount, param_value } = findParamToMatchAmount(in_amount, in_amount, delta => { const res = $trade_l_shares(_.cloneDeep(balances), _.cloneDeep(leveraged_balances), _.cloneDeep(profits), _.cloneDeep(recent), x0, y0, leverage, in_token, -delta, 0, 'ADDRESS', pool_props); return { res, required_amount: res.gross_delta }; }); return { res, delta: param_value }; } function getLeveragedSellParams(in_amount, token, leverage, entry_price, poolState) { poolState = _.cloneDeep(poolState); // make a copy so that we don't modify the input params chargeInterest(poolState); const { balances, leveraged_balances, profits, recent, shifts: { x0, y0 }, pool_props } = poolState; token = toAsset(token, pool_props); const { res, required_amount, param_value } = findParamToMatchAmount(in_amount, in_amount * (leverage - 1), delta => { const res = $trade_l_shares(_.cloneDeep(balances), _.cloneDeep(leveraged_balances), _.cloneDeep(profits), _.cloneDeep(recent), x0, y0, leverage, token, delta, entry_price, 'ADDRESS', pool_props); return { res, required_amount: -res.shares }; }); return { res, delta: param_value }; } function getLinearSharesFunction(shares_bonding_curve) { switch (shares_bonding_curve) { case 'IXBHF6T4IKMYAFGRM54F5FVMXGKCTFNT': return ($issued_shares) => $issued_shares; case 'FVFJQZVUWUANWRWXJ5LWVYDUP2XF7BIB': return ($issued_shares) => $issued_shares ** 2; } throw Error(`unknown bonding curve ${shares_bonding_curve}`); } function getRedemptionResult(received_shares_amount, asset, poolState) { poolState = _.cloneDeep(poolState); // make a copy so that we don't modify the input params chargeInterest(poolState); const { balances, leveraged_balances, profits, recent, shifts: { x0, y0 }, lp_shares, pool_props } = poolState; if (asset) asset = toAsset(asset, pool_props); const get_linear_shares = getLinearSharesFunction(pool_props.shares_bonding_curve); const new_issued = lp_shares.issued - received_shares_amount; const new_linear = get_linear_shares(new_issued); const res = $redeem_shares(lp_shares.linear, balances, leveraged_balances, profits, recent, x0, y0, lp_shares.linear - new_linear, asset, pool_props); return res; } function findParamToMatchAmount(target_amount, initial_estimation, f) { let param_value = initial_estimation; // initial estimation let bestAbove = { distance: Infinity, required_amount: null, param_value: null, }; let bestBelow = { distance: -Infinity, required_amount: null, param_value: null, }; let min_failing = Infinity; let prev_param_value; let prev_distance = Infinity; let prev_slope; let prev_required_amount; let count = 0; while (true) { count++; if (count > 100) throw Error(`too many iterations, target ${target_amount}, last ${required_amount}`) log(`${count}: trying value ${param_value}`); try { var { required_amount, res } = f(param_value); } catch (e) { log(`value ${param_value} failed`, e); if (param_value < min_failing) min_failing = param_value; param_value = (param_value + (prev_param_value || 0)) / 2; continue; } const distance = required_amount - target_amount; if ( bestAbove.distance < Infinity && bestBelow.distance > -Infinity && (distance > 0 && distance >= bestAbove.distance || distance < 0 && distance <= bestBelow.distance) ) { log(`distance ${distance} out of best range, will try its middle`); param_value = (bestAbove.param_value + bestBelow.param_value) / 2; continue; } if (distance > 0 && distance < bestAbove.distance) bestAbove = { distance, required_amount, param_value }; if (distance < 0 && distance > bestBelow.distance) bestBelow = { distance, required_amount, param_value }; const approach = prev_distance / distance; const delta_param_value = param_value - prev_param_value; // 1st derivative let slope = prev_param_value ? (param_value - prev_param_value) / (required_amount - prev_required_amount) : param_value / required_amount; // if (distance < 1000) // too noisy probably due to rounding // slope = param_value / required_amount; log(`result`, { param_value, required_amount, res, distance, approach, delta_param_value, slope }); if (required_amount === target_amount) return { res, required_amount, param_value }; if (param_value === prev_param_value) { log(`would repeat value ${param_value}`); if (Math.abs(distance) <= 10) return { res, required_amount, param_value, bNoExactMatch: true }; throw Error(`repeated value ${param_value} and distance is ${distance}`); } prev_param_value = param_value; if (required_amount !== prev_required_amount) // slope is not Infinity param_value += slope * (target_amount - required_amount); else { log(`repeated amount ${required_amount}`); if (bestBelow.param_value && bestAbove.param_value) param_value = (bestAbove.param_value + bestBelow.param_value) / 2; else if (bestAbove.param_value) param_value = (bestAbove.param_value + prev_param_value) / 2; else if (bestBelow.param_value) param_value = (bestBelow.param_value + prev_param_value) / 2; else throw Error(`repeated amount ${required_amount} and no next param value`); } if (0 && prev_slope) { // 2nd term of Taylor series const second_derivative = (slope - prev_slope) / (required_amount - prev_required_amount); param_value += 1 / 2 * second_derivative * (target_amount - required_amount) ** 2; log('2nd derivative', 1 / 2 * second_derivative * (target_amount - required_amount) ** 2) } // param_value = round(param_value); if (param_value < 0) { log(`next param value would be negative ${param_value}, will half instead`) param_value = prev_param_value / 2; } if (bestAbove.param_value && param_value > bestAbove.param_value) { log(`next param value ${param_value} would be above best above ${bestAbove.param_value}`); if (prev_param_value === bestAbove.param_value) throw Error(`would repeat ${prev_param_value}`); param_value = (prev_param_value + bestAbove.param_value) / 2; } if (param_value > min_failing) { log(`next param value ${param_value} would be above min failing ${min_failing}`); param_value = (prev_param_value + min_failing) / 2; } if (bestBelow.param_value && param_value < bestBelow.param_value) { log(`next param value ${param_value} would be below best below ${bestBelow.param_value}`); if (prev_param_value === bestBelow.param_value) throw Error(`would repeat ${prev_param_value}`); param_value = (prev_param_value + bestBelow.param_value) / 2; } prev_distance = distance; prev_slope = prev_required_amount && slope; prev_required_amount = required_amount; } } exports.setLogger = setLogger; exports.$swap = $swap; exports.$trade_l_shares = $trade_l_shares; exports.getPoolState = getPoolState; exports.toXY = toXY; exports.toAsset = toAsset; exports.getPrice = getPrice; exports.getCurrentUtilizationRatio = getCurrentUtilizationRatio; exports.getInterestRate = getInterestRate; exports.chargeInterest = chargeInterest; exports.getSwapParams = getSwapParams; exports.getSwapParamsByOutput = getSwapParamsByOutput; exports.getLeveragedBuyParams = getLeveragedBuyParams; exports.getLeveragedSellParams = getLeveragedSellParams; exports.getRedemptionResult = getRedemptionResult;