diff --git a/src/trade-matcher/trade_gradient.ts b/src/trade-matcher/trade_gradient.ts new file mode 100644 index 0000000..4f285bf --- /dev/null +++ b/src/trade-matcher/trade_gradient.ts @@ -0,0 +1,243 @@ +import { BigNumber } from '../utils/numerics'; + +const R_ONE = BigNumber.from(1).shl(48); // = 2 ^ 48 +const M_ONE = BigNumber.from(1).shl(24); // = 2 ^ 24 +const EXP_ONE = BigNumber.from(1).shl(127); // = 2 ^ 127 +const MAX_VAL = BigNumber.from(1).shl(131); // = 2 ^ 131 +const RR = R_ONE.mul(R_ONE); // = 2 ^ 96 +const MM = M_ONE.mul(M_ONE); // = 2 ^ 48 +const RR_MUL_MM = RR.mul(MM); // = 2 ^ 144 +const RR_DIV_MM = RR.div(MM); // = 2 ^ 48 +const EXP_ONE_MUL_RR = EXP_ONE.mul(RR); // = 2 ^ 223 +const EXP_ONE_DIV_RR = EXP_ONE.div(RR); // = 2 ^ 31 +const EXP_ONE_DIV_MM = EXP_ONE.div(MM); // = 2 ^ 79 + +const MAX_UINT128 = BigNumber.from(2).pow(128).sub(1); +const MAX_UINT256 = BigNumber.from(2).pow(256).sub(1); + +function check(val: BigNumber, max: BigNumber) { + if (val.gte(0) && val.lte(max)) { + return val; + } + throw null; +} + +const uint128 = (n: BigNumber) => check(n, MAX_UINT128); +const add = (a: BigNumber, b: BigNumber) => check(a.add(b), MAX_UINT256); +const mul = (a: BigNumber, b: BigNumber) => check(a.mul(b), MAX_UINT256); +const mulDivF = (a: BigNumber, b: BigNumber, c: BigNumber) => + check(a.mul(b).div(c), MAX_UINT256); +const mulDivC = (a: BigNumber, b: BigNumber, c: BigNumber) => + check(a.mul(b).add(c).sub(1).div(c), MAX_UINT256); + +enum GradientType { + LINEAR_INCREASE, + LINEAR_DECREASE, + LINEAR_INV_INCREASE, + LINEAR_INV_DECREASE, + EXPONENTIAL_INCREASE, + EXPONENTIAL_DECREASE +} + +function calcTargetAmount( + gradientType: GradientType, + initialRate: BigNumber, + multiFactor: BigNumber, + timeElapsed: BigNumber, + sourceAmount: BigNumber +): BigNumber { + const rate = calcCurrentRate(gradientType, initialRate, multiFactor, timeElapsed); + return mulDivF(sourceAmount, rate[0], rate[1]); +} + +function calcSourceAmount( + gradientType: GradientType, + initialRate: BigNumber, + multiFactor: BigNumber, + timeElapsed: BigNumber, + targetAmount: BigNumber +): BigNumber { + const rate = calcCurrentRate(gradientType, initialRate, multiFactor, timeElapsed); + return mulDivC(targetAmount, rate[1], rate[0]); +} + +/** +* @dev Given the following parameters: +* r - the gradient's initial exchange rate +* m - the gradient's multiplication factor +* t - the time elapsed since strategy creation +* +* Calculate the current exchange rate for each one of the following gradients: +* +----------------+-----------+-----------------+----------------------------------------------+ +* | type | direction | formula | restriction | +* +----------------+-----------+-----------------+----------------------------------------------+ +* | linear | increase | r * (1 + m * t) | | +* | linear | decrease | r * (1 - m * t) | m * t < 1 (ensure a finite-positive rate) | +* | linear-inverse | increase | r / (1 - m * t) | m * t < 1 (ensure a finite-positive rate) | +* | linear-inverse | decrease | r / (1 + m * t) | | +* | exponential | increase | r * e ^ (m * t) | m * t < 16 (due to computational limitation) | +* | exponential | decrease | r / e ^ (m * t) | m * t < 16 (due to computational limitation) | +* +----------------+-----------+-----------------+----------------------------------------------+ +*/ +function calcCurrentRate( + gradientType: GradientType, + initialRate: BigNumber, // the 48-bit-mantissa-6-bit-exponent encoding of the initial exchange rate square root + multiFactor: BigNumber, // the 24-bit-mantissa-5-bit-exponent encoding of the multiplication factor times 2 ^ 24 + timeElapsed: BigNumber, /// the time elapsed since strategy creation +): [BigNumber, BigNumber] { + if ((R_ONE.shr(initialRate.div(R_ONE).toNumber())).eq(0)) { + throw new Error('InitialRateTooHigh'); + } + + if ((M_ONE.shr(multiFactor.div(M_ONE).toNumber())).eq(0)) { + throw new Error('MultiFactorTooHigh'); + } + + const r = initialRate.mod(R_ONE).shl(initialRate.div(R_ONE).toNumber()); // = floor(sqrt(initial_rate) * 2 ^ 48) < 2 ^ 96 + const m = multiFactor.mod(M_ONE).shl(multiFactor.div(M_ONE).toNumber()); // = floor(multi_factor * 2 ^ 24 * 2 ^ 24) < 2 ^ 48 + const t = timeElapsed; + + const rr = mul(r, r); // < 2 ^ 192 + const mt = mul(m, t); // < 2 ^ 80 + + if (gradientType == GradientType.LINEAR_INCREASE) { + // initial_rate * (1 + multi_factor * time_elapsed) + const temp1 = rr; /////////// < 2 ^ 192 + const temp2 = add(MM, mt); // < 2 ^ 81 + const temp3 = mulDivC(temp1, temp2, MAX_UINT256); + const temp4 = RR_MUL_MM; + return [mulDivF(temp1, temp2, temp3), temp4.div(temp3)]; // not ideal + } + + if (gradientType == GradientType.LINEAR_DECREASE) { + // initial_rate * (1 - multi_factor * time_elapsed) + const temp1 = mul(rr, sub(MM, mt)); // < 2 ^ 240 + const temp2 = RR_MUL_MM; + return [temp1, temp2]; + } + + if (gradientType == GradientType.LINEAR_INV_INCREASE) { + // initial_rate / (1 - multi_factor * time_elapsed) + const temp1 = rr; + const temp2 = sub(RR, mul(mt, RR_DIV_MM)); // < 2 ^ 128 (inner expression) + return [temp1, temp2]; + } + + if (gradientType == GradientType.LINEAR_INV_DECREASE) { + // initial_rate / (1 + multi_factor * time_elapsed) + const temp1 = rr; + const temp2 = add(RR, mul(mt, RR_DIV_MM)); // < 2 ^ 129 + return [temp1, temp2]; + } + + if (gradientType == GradientType.EXPONENTIAL_INCREASE) { + // initial_rate * e ^ (multi_factor * time_elapsed) + const temp1 = rr; //////////////////////// < 2 ^ 192 + const temp2 = exp(mul(mt, EXP_ONE_DIV_MM)); // < 2 ^ 159 (inner expression) + const temp3 = mulDivC(temp1, temp2, MAX_UINT256); + const temp4 = EXP_ONE_MUL_RR; + return [mulDivF(temp1, temp2, temp3), temp4.div(temp3)]; // not ideal + } + + if (gradientType == GradientType.EXPONENTIAL_DECREASE) { + // initial_rate / e ^ (multi_factor * time_elapsed) + const temp1 = mul(rr, EXP_ONE_DIV_RR); /////// < 2 ^ 223 + const temp2 = exp(mul(mt, EXP_ONE_DIV_MM)); // < 2 ^ 159 (inner expression) + return [temp1, temp2]; + } + + throw new Error(`Invalid gradientType ${gradientType}`); +} + +function sub(one: BigNumber, mt: BigNumber): BigNumber { + if (one.lte(mt)) { + throw new Error('InvalidRate'); + } + return one.sub(mt); +} + +/** +* @dev Compute e ^ (x / EXP_ONE) * EXP_ONE +* Input range: 0 <= x <= MAX_VAL - 1 +* Detailed description: +* - Rewrite the input as a sum of binary exponents and a single residual r, as small as possible +* - The exponentiation of each binary exponent is given (pre-calculated) +* - The exponentiation of r is calculated via Taylor series for e^x, where x = r +* - The exponentiation of the input is calculated by multiplying the intermediate results above +* - For example: e^5.521692859 = e^(4 + 1 + 0.5 + 0.021692859) = e^4 * e^1 * e^0.5 * e^0.021692859 +*/ +function exp(x: BigNumber): BigNumber { + // prettier-ignore + if (x.gte(MAX_VAL)) { + throw new Error('ExpOverflow'); + } + + let res = BigNumber.from(0); + + let y: BigNumber; + let z: BigNumber; + + z = y = x.mod(BigNumber.from('0x10000000000000000000000000000000')); // get the input modulo 2^(-3) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x10e1b3be415a0000'))); // add y^02 * (20! / 02!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x05a0913f6b1e0000'))); // add y^03 * (20! / 03!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x0168244fdac78000'))); // add y^04 * (20! / 04!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x004807432bc18000'))); // add y^05 * (20! / 05!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x000c0135dca04000'))); // add y^06 * (20! / 06!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x0001b707b1cdc000'))); // add y^07 * (20! / 07!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x000036e0f639b800'))); // add y^08 * (20! / 08!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x00000618fee9f800'))); // add y^09 * (20! / 09!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x0000009c197dcc00'))); // add y^10 * (20! / 10!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x0000000e30dce400'))); // add y^11 * (20! / 11!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x000000012ebd1300'))); // add y^12 * (20! / 12!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x0000000017499f00'))); // add y^13 * (20! / 13!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x0000000001a9d480'))); // add y^14 * (20! / 14!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x00000000001c6380'))); // add y^15 * (20! / 15!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x000000000001c638'))); // add y^16 * (20! / 16!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x0000000000001ab8'))); // add y^17 * (20! / 17!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x000000000000017c'))); // add y^18 * (20! / 18!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x0000000000000014'))); // add y^19 * (20! / 19!) + z = z.mul(y).div(EXP_ONE); res = res.add(z.mul(BigNumber.from('0x0000000000000001'))); // add y^20 * (20! / 20!) + res = res.div(BigNumber.from('0x21c3677c82b40000')).add(y).add(EXP_ONE); // divide by 20! and then add y^1 / 1! + y^0 / 0! + + if (!x.and(BigNumber.from('0x010000000000000000000000000000000')).eq(0)) res = res.mul(BigNumber.from('0x1c3d6a24ed82218787d624d3e5eba95f9')).div(BigNumber.from('0x18ebef9eac820ae8682b9793ac6d1e776')); // multiply by e^2^(-3) + if (!x.and(BigNumber.from('0x020000000000000000000000000000000')).eq(0)) res = res.mul(BigNumber.from('0x18ebef9eac820ae8682b9793ac6d1e778')).div(BigNumber.from('0x1368b2fc6f9609fe7aceb46aa619baed4')); // multiply by e^2^(-2) + if (!x.and(BigNumber.from('0x040000000000000000000000000000000')).eq(0)) res = res.mul(BigNumber.from('0x1368b2fc6f9609fe7aceb46aa619baed5')).div(BigNumber.from('0x0bc5ab1b16779be3575bd8f0520a9f21f')); // multiply by e^2^(-1) + if (!x.and(BigNumber.from('0x080000000000000000000000000000000')).eq(0)) res = res.mul(BigNumber.from('0x0bc5ab1b16779be3575bd8f0520a9f21e')).div(BigNumber.from('0x0454aaa8efe072e7f6ddbab84b40a55c9')); // multiply by e^2^(+0) + if (!x.and(BigNumber.from('0x100000000000000000000000000000000')).eq(0)) res = res.mul(BigNumber.from('0x0454aaa8efe072e7f6ddbab84b40a55c5')).div(BigNumber.from('0x00960aadc109e7a3bf4578099615711ea')); // multiply by e^2^(+1) + if (!x.and(BigNumber.from('0x200000000000000000000000000000000')).eq(0)) res = res.mul(BigNumber.from('0x00960aadc109e7a3bf4578099615711d7')).div(BigNumber.from('0x0002bf84208204f5977f9a8cf01fdce3d')); // multiply by e^2^(+2) + if (!x.and(BigNumber.from('0x400000000000000000000000000000000')).eq(0)) res = res.mul(BigNumber.from('0x0002bf84208204f5977f9a8cf01fdc307')).div(BigNumber.from('0x0000003c6ab775dd0b95b4cbee7e65d11')); // multiply by e^2^(+3) + + return res; +} + +// TODO: get the encoded-order as input (similar to how it's done in trade.ts) +export const getEncodedTradeTargetAmount = ( + gradientType: GradientType, + initialRate: BigNumber, + multiFactor: BigNumber, + timeElapsed: BigNumber, + sourceAmount: BigNumber +): BigNumber => { + try { + return uint128(calcTargetAmount(gradientType, initialRate, multiFactor, timeElapsed, sourceAmount)); + } catch (error) { + return BigNumber.from(0); /* rate = zero / amount = zero */ + } +}; + +// TODO: get the encoded-order as input (similar to how it's done in trade.ts) +export const getEncodedTradeSourceAmount = ( + gradientType: GradientType, + initialRate: BigNumber, + multiFactor: BigNumber, + timeElapsed: BigNumber, + targetAmount: BigNumber +): BigNumber => { + try { + return uint128(calcSourceAmount(gradientType, initialRate, multiFactor, timeElapsed, targetAmount)); + } catch (error) { + return MAX_UINT128; /* rate = amount / infinity = zero */ + } +}; + +export const getEncodedCurrentRate = calcCurrentRate; \ No newline at end of file diff --git a/tests/trade_gradient.spec.ts b/tests/trade_gradient.spec.ts new file mode 100644 index 0000000..12fdcd9 --- /dev/null +++ b/tests/trade_gradient.spec.ts @@ -0,0 +1,106 @@ +import { expect } from 'chai'; +import { getEncodedCurrentRate } from '../src/trade-matcher/trade_gradient'; +import { + encodeScaleInitialRate, + decodeScaleInitialRate, + encodeFloatInitialRate, + decodeFloatInitialRate, + encodeScaleMultiFactor, + decodeScaleMultiFactor, + encodeFloatMultiFactor, + decodeFloatMultiFactor, +} from '../src/utils/encoders'; +import { Decimal, BnToDec, DecToBn } from '../src/utils/numerics'; + +const ONE = new Decimal(1); +const TWO = new Decimal(2); + +const EXP_ONE = new Decimal(2).pow(127); +const MAX_VAL = new Decimal(2).pow(131); + +function expectedCurrentRate( + gradientType: number, + initialRate: Decimal, + multiFactor: Decimal, + timeElapsed: Decimal +) { + switch (gradientType) { + case 0: return initialRate.mul(ONE.add(multiFactor.mul(timeElapsed))); + case 1: return initialRate.mul(ONE.sub(multiFactor.mul(timeElapsed))); + case 2: return initialRate.div(ONE.sub(multiFactor.mul(timeElapsed))); + case 3: return initialRate.div(ONE.add(multiFactor.mul(timeElapsed))); + case 4: return initialRate.mul(multiFactor.mul(timeElapsed).exp()); + case 5: return initialRate.div(multiFactor.mul(timeElapsed).exp()); + } + throw new Error(`Invalid gradientType ${gradientType}`); +} + +function testCurrentRate( + gradientType: number, + initialRate: Decimal, + multiFactor: Decimal, + timeElapsed: Decimal, + maxError: string +) { + it(`testCurrentRate: gradientType,initialRate,multiFactor,timeElapsed = ${[gradientType, initialRate, multiFactor, timeElapsed]}`, async () => { + const rEncoded = encodeFloatInitialRate(encodeScaleInitialRate(initialRate)); + const mEncoded = encodeFloatMultiFactor(encodeScaleMultiFactor(multiFactor)); + const rDecoded = decodeScaleInitialRate(BnToDec(decodeFloatInitialRate(rEncoded))); + const mDecoded = decodeScaleMultiFactor(BnToDec(decodeFloatMultiFactor(mEncoded))); + const expected = expectedCurrentRate(gradientType, rDecoded, mDecoded, timeElapsed); + if (expected.isFinite() && expected.isPositive()) { + const retVal = getEncodedCurrentRate(gradientType, rEncoded, mEncoded, DecToBn(timeElapsed)); + const actual = BnToDec(retVal[0]).div(BnToDec(retVal[1])); + if (!actual.eq(expected)) { + const error = actual.div(expected).sub(1).abs(); + expect(error.lte(maxError)).to.be.equal( + true, + `\n- expected = ${expected.toFixed()}` + + `\n- actual = ${actual.toFixed()}` + + `\n- error = ${error.toFixed()}` + ); + } + } else { + expect(() => { + getEncodedCurrentRate(gradientType, rEncoded, mEncoded, DecToBn(timeElapsed)); + }).to.throw('InvalidRate'); + } + }); +} + +describe.only('trade_gradient', () => { + for (let a = 1; a <= 10; a++) { + for (let b = 1; b <= 10; b++) { + for (let c = 1; c <= 10; c++) { + const initialRate = new Decimal(a).mul(1234.5678); + const multiFactor = new Decimal(b).mul(0.00001234); + const timeElapsed = new Decimal(c).mul(3600); + testCurrentRate(0, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(1, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(2, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(3, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(4, initialRate, multiFactor, timeElapsed, '0.00000000000000000000000000000000000002'); + testCurrentRate(5, initialRate, multiFactor, timeElapsed, '0.00000000000000000000000000000000000002'); + } + } + } + + for (let a = -27; a <= 27; a++) { + for (let b = -14; b <= -1; b++) { + for (let c = 1; c <= 10; c++) { + const initialRate = new Decimal(10).pow(a); + const multiFactor = new Decimal(10).pow(b); + const timeElapsed = Decimal.min( + MAX_VAL.div(EXP_ONE).div(multiFactor).sub(1).ceil(), + TWO.pow(25).sub(1) + ).mul(c).div(10).ceil(); + testCurrentRate(0, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(1, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(2, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(3, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(4, initialRate, multiFactor, timeElapsed, '0.000000000000000000000000000000000002'); + testCurrentRate(5, initialRate, multiFactor, timeElapsed, '0.000000000000000000000000000000000002'); + } + } + } +});