-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for gradient-strategies trading
- Loading branch information
barak manos
committed
Aug 12, 2024
1 parent
9d84803
commit 8d799a7
Showing
2 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} | ||
} | ||
}); |