Skip to content

Commit

Permalink
Add support for gradient-strategies trading
Browse files Browse the repository at this point in the history
  • Loading branch information
barak manos committed Aug 12, 2024
1 parent 9d84803 commit 8d799a7
Show file tree
Hide file tree
Showing 2 changed files with 349 additions and 0 deletions.
243 changes: 243 additions & 0 deletions src/trade-matcher/trade_gradient.ts
Original file line number Diff line number Diff line change
@@ -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;
106 changes: 106 additions & 0 deletions tests/trade_gradient.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
}
});

0 comments on commit 8d799a7

Please sign in to comment.