diff --git a/src/trade-matcher/trade.ts b/src/trade-matcher/trade.ts index b1b60e5..0f3ffd6 100644 --- a/src/trade-matcher/trade.ts +++ b/src/trade-matcher/trade.ts @@ -25,6 +25,7 @@ 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); +const minFactor = (a: BigNumber, b: BigNumber) => mulDivC(a, b, MAX_UINT256); // // x * (A * y + B * z) ^ 2 @@ -50,8 +51,8 @@ const getEncodedTradeBySourceAmount = ( const temp2 = add(mul(y, A), mul(z, B)); const temp3 = mul(temp2, x); - const factor1 = mulDivC(temp1, temp1, MAX_UINT256); - const factor2 = mulDivC(temp3, A, MAX_UINT256); + const factor1 = minFactor(temp1, temp1); + const factor2 = minFactor(temp3, A); const factor = BigNumberMax(factor1, factor2); const temp4 = mulDivC(temp1, temp1, factor); @@ -88,8 +89,8 @@ const getEncodedTradeByTargetAmount = ( const temp2 = add(mul(y, A), mul(z, B)); const temp3 = sub(temp2, mul(x, A)); - const factor1 = mulDivC(temp1, temp1, MAX_UINT256); - const factor2 = mulDivC(temp2, temp3, MAX_UINT256); + const factor1 = minFactor(temp1, temp1); + const factor2 = minFactor(temp2, temp3); const factor = BigNumberMax(factor1, factor2); const temp4 = mulDivC(temp1, temp1, factor); diff --git a/src/trade-matcher/trade_gradient.ts b/src/trade-matcher/trade_gradient.ts index 4f285bf..d2fffdb 100644 --- a/src/trade-matcher/trade_gradient.ts +++ b/src/trade-matcher/trade_gradient.ts @@ -1,17 +1,5 @@ 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); @@ -29,6 +17,25 @@ 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); +const minFactor = (a: BigNumber, b: BigNumber) => mulDivC(a, b, MAX_UINT256); + +const EXP_ONE = BigNumber.from("0x0080000000000000000000000000000000"); // 1 +const EXP_MID = BigNumber.from("0x0400000000000000000000000000000000"); // 8 +const EXP_MAX = BigNumber.from("0x2cb53f09f05cc627c85ddebfccfeb72758"); // ceil(ln2) * 129 +const EXP_LN2 = BigNumber.from("0x0058b90bfbe8e7bcd5e4f1d9cc01f97b58"); // ceil(ln2) + +const R_ONE = BigNumber.from(1).shl(48); // = 2 ^ 48 +const M_ONE = BigNumber.from(1).shl(24); // = 2 ^ 24 + +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 enum GradientType { LINEAR_INCREASE, @@ -62,23 +69,23 @@ function calcSourceAmount( } /** -* @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) | -* +----------------+-----------+-----------------+----------------------------------------------+ -*/ + * @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 < 129 * ln2 (computational limitation) | + * | exponential | decrease | r / e ^ (m * t) | m * t < 129 * ln2 (computational limitation) | + * +----------------+-----------+-----------------+----------------------------------------------+ + */ function calcCurrentRate( gradientType: GradientType, initialRate: BigNumber, // the 48-bit-mantissa-6-bit-exponent encoding of the initial exchange rate square root @@ -104,7 +111,7 @@ function calcCurrentRate( // 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 temp3 = minFactor(temp1, temp2); const temp4 = RR_MUL_MM; return [mulDivF(temp1, temp2, temp3), temp4.div(temp3)]; // not ideal } @@ -134,7 +141,7 @@ function calcCurrentRate( // 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 temp3 = minFactor(temp1, temp2); const temp4 = EXP_ONE_MUL_RR; return [mulDivF(temp1, temp2, temp3), temp4.div(temp3)]; // not ideal } @@ -149,6 +156,9 @@ function calcCurrentRate( throw new Error(`Invalid gradientType ${gradientType}`); } +/** + * @dev Ensure a finite positive rate + */ function sub(one: BigNumber, mt: BigNumber): BigNumber { if (one.lte(mt)) { throw new Error('InvalidRate'); @@ -157,21 +167,41 @@ function sub(one: BigNumber, mt: BigNumber): BigNumber { } /** -* @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 -*/ + * @dev Compute e ^ (x / EXP_ONE) * EXP_ONE + * Input range: 0 <= x <= EXP_MAX - 1 + * Detailed description: + * - For x < EXP_MID, this function computes e ^ x + * - For x < EXP_MAX, this function computes e ^ mod(x, ln2) * 2 ^ div(x, ln2) + * - The latter relies on the following identity: + * e ^ x = + * e ^ x * 2 ^ k / 2 ^ k = + * e ^ x * 2 ^ k / e ^ (k * ln2) = + * e ^ x / e ^ (k * ln2) * 2 ^ k = + * e ^ (x - k * ln2) * 2 ^ k + * - Replacing k with div(x, ln2) gives the solution above + * - The value of ln2 is represented as ceil(ln2 * EXP_ONE) + */ function exp(x: BigNumber): BigNumber { - // prettier-ignore - if (x.gte(MAX_VAL)) { - throw new Error('ExpOverflow'); + if (x.lt(EXP_MID)) { + return _exp(x); // slightly more accurate + } + if (x.lt(EXP_MAX)) { + return _exp(x.mod(EXP_LN2)).shl(x.div(EXP_LN2).toNumber()); } + throw new Error('ExpOverflow'); +} +/** + * @dev Compute e ^ (x / EXP_ONE) * EXP_ONE + * Input range: 0 <= x <= EXP_ONE * 16 - 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 { let res = BigNumber.from(0); let y: BigNumber; diff --git a/tests/trade_gradient.spec.ts b/tests/trade_gradient.spec.ts index 12fdcd9..9cf554e 100644 --- a/tests/trade_gradient.spec.ts +++ b/tests/trade_gradient.spec.ts @@ -15,8 +15,40 @@ 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); +const EXP_ONE = TWO.pow(127); +const EXP_MAX = EXP_ONE.mul(TWO.ln()).ceil().mul(129); + +const getInitialRate = (x: Decimal) => decodeScaleInitialRate(BnToDec(decodeFloatInitialRate(encodeFloatInitialRate(encodeScaleInitialRate(x))))); +const getMultiFactor = (x: Decimal) => decodeScaleMultiFactor(BnToDec(decodeFloatMultiFactor(encodeFloatMultiFactor(encodeScaleMultiFactor(x))))); + +function testConfiguration( + paramName: string, + preConfig: Decimal, + postConfig: (x: Decimal) => (Decimal), + maxAbsoluteError: string, + maxRelativeError: string +) { + it(`testConfiguration: ${paramName} = ${preConfig}`, async () => { + const expected = preConfig; + const actual = postConfig(preConfig); + if (!actual.eq(expected)) { + expect(actual.lt(expected)).to.be.equal( + true, + `\n- expected = ${expected.toFixed()}` + + `\n- actual = ${actual.toFixed()}` + ); + const absoluteError = actual.sub(expected).abs(); + const relativeError = actual.div(expected).sub(1).abs(); + expect(absoluteError.lte(maxAbsoluteError) || relativeError.lte(maxRelativeError)).to.be.equal( + true, + `\n- expected = ${expected.toFixed()}` + + `\n- actual = ${actual.toFixed()}` + + `\n- absoluteError = ${absoluteError.toFixed()}` + + `\n- relativeError = ${relativeError.toFixed()}` + ); + } + }); +} function expectedCurrentRate( gradientType: number, @@ -68,7 +100,27 @@ function testCurrentRate( }); } -describe.only('trade_gradient', () => { +describe('trade_gradient', () => { + for (let a = 1; a <= 100; a++) { + const initialRate = new Decimal(a).mul(1234.5678); + testConfiguration('initialRate', initialRate, getInitialRate, '0', '0.00000000000002'); + } + + for (let b = 1; b <= 100; b++) { + const multiFactor = new Decimal(b).mul(0.00001234); + testConfiguration('multiFactor', multiFactor, getMultiFactor, '0', '0.0000002'); + } + + for (let a = -28; a <= 28; a++) { + const initialRate = new Decimal(10).pow(a); + testConfiguration('initialRate', initialRate, getInitialRate, '0.0000000000000005', '0.00000000000002'); + } + + for (let b = -14; b <= -1; b++) { + const multiFactor = new Decimal(10).pow(b); + testConfiguration('multiFactor', multiFactor, getMultiFactor, '0.000000000000004', '0.00000007'); + } + for (let a = 1; a <= 10; a++) { for (let b = 1; b <= 10; b++) { for (let c = 1; c <= 10; c++) { @@ -91,15 +143,48 @@ describe.only('trade_gradient', () => { 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(4).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.00000000000000000000000000000000000006'); + testCurrentRate(5, initialRate, multiFactor, timeElapsed, '0.00000000000000000000000000000000000006'); + } + } + } + + for (const a of [-27, -10, 0, 10, 27]) { + for (const b of [-14, -9, -6, -1]) { + for (const c of [1, 4, 7, 10]) { + const initialRate = new Decimal(10).pow(a); + const multiFactor = new Decimal(10).pow(b); + const timeElapsed = Decimal.min( + EXP_MAX.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'); + testCurrentRate(4, initialRate, multiFactor, timeElapsed, '0.000000000004'); + testCurrentRate(5, initialRate, multiFactor, timeElapsed, '0.0000000000000000000000000000000000003'); + } + } + } + + for (const a of [-27, -10, 0, 10, 27]) { + for (const b of [-14, -9, -6, -1]) { + for (const c of [19, 24, 29]) { + const initialRate = new Decimal(10).pow(a); + const multiFactor = new Decimal(10).pow(b); + const timeElapsed = new Decimal(2).pow(c).sub(1); + testCurrentRate(0, initialRate, multiFactor, timeElapsed, '0.0000000000000000000000000000000000000000003'); + testCurrentRate(1, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(2, initialRate, multiFactor, timeElapsed, '0'); + testCurrentRate(3, initialRate, multiFactor, timeElapsed, '0'); } } }