UNPKG

@node-dlc/core

Version:
1,030 lines (955 loc) 24.7 kB
import BigNumber from 'bignumber.js'; import { expect } from 'chai'; import { CETPayout, decompose, groupByIgnoringDigits, RoundingInterval, roundPayout, splitIntoRanges, } from '../../lib/dlc/CETCalculator'; import { CoveredCall } from '../../lib/dlc/finance/CoveredCall'; import { ShortPut } from '../../lib/dlc/finance/ShortPut'; import { HyperbolaPayoutCurve } from '../../lib/dlc/HyperbolaPayoutCurve'; import { BigIntMath } from '../../lib/utils/BigIntUtils'; const decompositionTestCases: { decomposed: number[]; composed: string; base: number; nbDigits: number; }[] = [ { composed: '123456789', decomposed: [1, 2, 3, 4, 5, 6, 7, 8, 9], base: 10, nbDigits: 9, }, { composed: '4321', decomposed: [1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1], base: 2, nbDigits: 13, }, { composed: '0', decomposed: [0, 0, 0, 0], base: 8, nbDigits: 4, }, { composed: '2', decomposed: [0, 2], base: 10, nbDigits: 2, }, { composed: '1', decomposed: [1], base: 2, nbDigits: 1, }, ]; const groupingTestCases: { startIndex: bigint; endIndex: bigint; base: number; nbDigits: number; expected: number[][]; }[] = [ { startIndex: BigInt(123), endIndex: BigInt(123), base: 10, nbDigits: 3, expected: [[1, 2, 3]], }, { startIndex: BigInt(171), endIndex: BigInt(210), base: 16, nbDigits: 2, expected: [ [10, 11], [10, 12], [10, 13], [10, 14], [10, 15], [11], [12], [13, 0], [13, 1], [13, 2], ], }, { startIndex: BigInt(73899), endIndex: BigInt(73938), base: 16, nbDigits: 6, expected: [ [0, 1, 2, 0, 10, 11], [0, 1, 2, 0, 10, 12], [0, 1, 2, 0, 10, 13], [0, 1, 2, 0, 10, 14], [0, 1, 2, 0, 10, 15], [0, 1, 2, 0, 11], [0, 1, 2, 0, 12], [0, 1, 2, 0, 13, 0], [0, 1, 2, 0, 13, 1], [0, 1, 2, 0, 13, 2], ], }, { startIndex: BigInt(1234), endIndex: BigInt(4321), base: 10, nbDigits: 4, expected: [ [1, 2, 3, 4], [1, 2, 3, 5], [1, 2, 3, 6], [1, 2, 3, 7], [1, 2, 3, 8], [1, 2, 3, 9], [1, 2, 4], [1, 2, 5], [1, 2, 6], [1, 2, 7], [1, 2, 8], [1, 2, 9], [1, 3], [1, 4], [1, 5], [1, 6], [1, 7], [1, 8], [1, 9], [2], [3], [4, 0], [4, 1], [4, 2], [4, 3, 0], [4, 3, 1], [4, 3, 2, 0], [4, 3, 2, 1], ], }, { startIndex: BigInt(1201234), endIndex: BigInt(1204321), base: 10, nbDigits: 8, expected: [ [0, 1, 2, 0, 1, 2, 3, 4], [0, 1, 2, 0, 1, 2, 3, 5], [0, 1, 2, 0, 1, 2, 3, 6], [0, 1, 2, 0, 1, 2, 3, 7], [0, 1, 2, 0, 1, 2, 3, 8], [0, 1, 2, 0, 1, 2, 3, 9], [0, 1, 2, 0, 1, 2, 4], [0, 1, 2, 0, 1, 2, 5], [0, 1, 2, 0, 1, 2, 6], [0, 1, 2, 0, 1, 2, 7], [0, 1, 2, 0, 1, 2, 8], [0, 1, 2, 0, 1, 2, 9], [0, 1, 2, 0, 1, 3], [0, 1, 2, 0, 1, 4], [0, 1, 2, 0, 1, 5], [0, 1, 2, 0, 1, 6], [0, 1, 2, 0, 1, 7], [0, 1, 2, 0, 1, 8], [0, 1, 2, 0, 1, 9], [0, 1, 2, 0, 2], [0, 1, 2, 0, 3], [0, 1, 2, 0, 4, 0], [0, 1, 2, 0, 4, 1], [0, 1, 2, 0, 4, 2], [0, 1, 2, 0, 4, 3, 0], [0, 1, 2, 0, 4, 3, 1], [0, 1, 2, 0, 4, 3, 2, 0], [0, 1, 2, 0, 4, 3, 2, 1], ], }, { startIndex: BigInt(2200), endIndex: BigInt(4999), base: 10, nbDigits: 4, expected: [ [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [2, 7], [2, 8], [2, 9], [3], [4], ], }, { startIndex: BigInt(0), endIndex: BigInt(99), base: 10, nbDigits: 2, expected: [[0], [1], [2], [3], [4], [5], [6], [7], [8], [9]], }, { startIndex: BigInt(100), endIndex: BigInt(199), base: 10, nbDigits: 3, expected: [[1]], }, { startIndex: BigInt(100), endIndex: BigInt(200), base: 10, nbDigits: 3, expected: [[1], [2, 0, 0]], }, { startIndex: BigInt(11), endIndex: BigInt(18), base: 10, nbDigits: 2, expected: [ [1, 1], [1, 2], [1, 3], [1, 4], [1, 5], [1, 6], [1, 7], [1, 8], ], }, { startIndex: BigInt(11), endIndex: BigInt(23), base: 2, nbDigits: 5, expected: [ [0, 1, 0, 1, 1], [0, 1, 1], [1, 0], ], }, { startIndex: BigInt(5677), endIndex: BigInt(8621), base: 2, nbDigits: 14, expected: [ [0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1], [0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1], [0, 1, 0, 1, 1, 0, 0, 0, 1, 1], [0, 1, 0, 1, 1, 0, 0, 1], [0, 1, 0, 1, 1, 0, 1], [0, 1, 0, 1, 1, 1], [0, 1, 1], [1, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 1, 0], [1, 0, 0, 0, 0, 1, 1, 0, 0], [1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0], [1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0], [1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0], ], }, ]; describe('CETCalculator', () => { describe('outcome decomposition tests', () => { it('should properly decompose values', () => { for (const test of decompositionTestCases) { expect( decompose(BigInt(parseInt(test.composed)), test.base, test.nbDigits), ).deep.equal(test.decomposed); } }); }); describe('grouping tests', () => { it('should properly compute all groupings', () => { for (const test of groupingTestCases) { expect( groupByIgnoringDigits( test.startIndex, test.endIndex, test.base, test.nbDigits, ), ).deep.equal(test.expected); } }); }); describe('descending hyperbola a=1 d=5000 tests', () => { const hyperbola = new HyperbolaPayoutCurve( new BigNumber(1), new BigNumber(0), new BigNumber(0), new BigNumber(500000), new BigNumber(0), new BigNumber(0), true, ); it('should properly split and round with one interval', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(10), }, ]; const totalCollateral = BigInt(100); const from = BigInt(0); const to = BigInt(999999); const fromPayout = totalCollateral; const toPayout = BigInt(0); const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); expect(ranges).deep.equal([ { payout: BigInt(100), indexFrom: BigInt(0), indexTo: BigInt(5263) }, { payout: BigInt(90), indexFrom: BigInt(5264), indexTo: BigInt(5882) }, { payout: BigInt(80), indexFrom: BigInt(5883), indexTo: BigInt(6666) }, { payout: BigInt(70), indexFrom: BigInt(6667), indexTo: BigInt(7692) }, { payout: BigInt(60), indexFrom: BigInt(7693), indexTo: BigInt(9090) }, { payout: BigInt(50), indexFrom: BigInt(9091), indexTo: BigInt(11111) }, { payout: BigInt(40), indexFrom: BigInt(11112), indexTo: BigInt(14285), }, { payout: BigInt(30), indexFrom: BigInt(14286), indexTo: BigInt(20000), }, { payout: BigInt(20), indexFrom: BigInt(20001), indexTo: BigInt(33333), }, { payout: BigInt(10), indexFrom: BigInt(33334), indexTo: BigInt(100000), }, { payout: BigInt(0), indexFrom: BigInt(100001), indexTo: BigInt(999999), }, ]); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); it('should properly split and round with non-even rounding mod', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(15), }, ]; const totalCollateral = BigInt(100); const from = BigInt(0); const to = BigInt(999999); const fromPayout = totalCollateral; const toPayout = BigInt(0); const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); expect(ranges).to.deep.equal([ { payout: BigInt(100), indexFrom: BigInt(0), indexTo: BigInt(5128) }, { payout: BigInt(90), indexFrom: BigInt(5129), indexTo: BigInt(6060) }, { payout: BigInt(75), indexFrom: BigInt(6061), indexTo: BigInt(7407) }, { payout: BigInt(60), indexFrom: BigInt(7408), indexTo: BigInt(9523) }, { payout: BigInt(45), indexFrom: BigInt(9524), indexTo: BigInt(13333) }, { payout: BigInt(30), indexFrom: BigInt(13334), indexTo: BigInt(22222), }, { payout: BigInt(15), indexFrom: BigInt(22223), indexTo: BigInt(66666), }, { payout: BigInt(0), indexFrom: BigInt(66667), indexTo: BigInt(999999), }, ]); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); }); describe('ascending hyperbola a=-1 d=500000 f2=100 tests', () => { const hyperbola = new HyperbolaPayoutCurve( new BigNumber(-1), new BigNumber(0), new BigNumber(0), new BigNumber(500000), new BigNumber(0), new BigNumber(100), true, ); it('should properly split and round with one interval', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(10), }, ]; const totalCollateral = BigInt(100); const from = BigInt(0); const to = BigInt(999999); const fromPayout = BigInt(0); const toPayout = totalCollateral; const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); expect(ranges).to.deep.equal([ { payout: BigInt(0), indexFrom: BigInt(0), indexTo: BigInt(5263) }, { payout: BigInt(10), indexFrom: BigInt(5264), indexTo: BigInt(5882) }, { payout: BigInt(20), indexFrom: BigInt(5883), indexTo: BigInt(6666) }, { payout: BigInt(30), indexFrom: BigInt(6667), indexTo: BigInt(7692) }, { payout: BigInt(40), indexFrom: BigInt(7693), indexTo: BigInt(9090) }, { payout: BigInt(50), indexFrom: BigInt(9091), indexTo: BigInt(11111) }, { payout: BigInt(60), indexFrom: BigInt(11112), indexTo: BigInt(14285), }, { payout: BigInt(70), indexFrom: BigInt(14286), indexTo: BigInt(19999), }, { payout: BigInt(80), indexFrom: BigInt(20000), indexTo: BigInt(33333), }, { payout: BigInt(90), indexFrom: BigInt(33334), indexTo: BigInt(99999), }, { payout: BigInt(100), indexFrom: BigInt(100000), indexTo: BigInt(999999), }, ]); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); it('should properly split and round with multiple rounding intervals', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(7), }, { beginInterval: BigInt(5432), roundingMod: BigInt(12) }, { beginInterval: BigInt(6432), roundingMod: BigInt(25) }, ]; const totalCollateral = BigInt(100); const from = BigInt(0); const to = BigInt(999999); const fromPayout = BigInt(0); const toPayout = totalCollateral; const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); expect(ranges).to.deep.equal([ { payout: BigInt(0), indexFrom: BigInt(0), indexTo: BigInt(5181) }, { payout: BigInt(7), indexFrom: BigInt(5182), indexTo: BigInt(5431) }, { payout: BigInt(12), indexFrom: BigInt(5432), indexTo: BigInt(6097) }, { payout: BigInt(24), indexFrom: BigInt(6098), indexTo: BigInt(6431) }, { payout: BigInt(25), indexFrom: BigInt(6432), indexTo: BigInt(7999) }, { payout: BigInt(50), indexFrom: BigInt(8000), indexTo: BigInt(13333) }, { payout: BigInt(75), indexFrom: BigInt(13334), indexTo: BigInt(39999), }, { payout: BigInt(100), indexFrom: BigInt(40000), indexTo: BigInt(999999), }, ]); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); it('should properly split and round with non-even rounding mod', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(15), }, ]; const totalCollateral = BigInt(100); const from = BigInt(0); const to = BigInt(999999); const fromPayout = BigInt(0); const toPayout = totalCollateral; const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); expect(ranges).to.deep.equal([ { payout: BigInt(0), indexFrom: BigInt(0), indexTo: BigInt(5405) }, { payout: BigInt(15), indexFrom: BigInt(5406), indexTo: BigInt(6451) }, { payout: BigInt(30), indexFrom: BigInt(6452), indexTo: BigInt(7999) }, { payout: BigInt(45), indexFrom: BigInt(8000), indexTo: BigInt(10526) }, { payout: BigInt(60), indexFrom: BigInt(10527), indexTo: BigInt(15384), }, { payout: BigInt(75), indexFrom: BigInt(15385), indexTo: BigInt(28571), }, { payout: BigInt(90), indexFrom: BigInt(28572), indexTo: BigInt(199999), }, { payout: BigInt(100), indexFrom: BigInt(200000), indexTo: BigInt(999999), }, ]); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); }); describe('large descending hyperbola a=1 d=5000000000000 tests (50k 1BTC covered call)', () => { const hyperbola = new HyperbolaPayoutCurve( new BigNumber(1), new BigNumber(0), new BigNumber(0), new BigNumber(5000000000000), new BigNumber(0), new BigNumber(0), true, ); it('should properly split and round with one interval', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(100000), }, ]; const totalCollateral = BigInt(100000000); const from = BigInt(0); const to = BigInt(999999); const fromPayout = totalCollateral; const toPayout = BigInt(0); const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); it('should properly split and round with non-even rounding mod', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(150000), }, ]; const totalCollateral = BigInt(100000000); const from = BigInt(0); const to = BigInt(999999); const fromPayout = totalCollateral; const toPayout = BigInt(0); const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); }); describe('descending hyperbola a=1 d=7500000000000 f2=-7500000 tests', () => { const hyperbola = new HyperbolaPayoutCurve( new BigNumber(1), new BigNumber(0), new BigNumber(0), new BigNumber(7500000000000), new BigNumber(0), new BigNumber(-7500008), true, ); it('should properly split and round with one interval', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(250000), }, ]; const totalCollateral = BigInt(92499992); const from = BigInt(0); const to = BigInt(999999); const fromPayout = totalCollateral; const toPayout = BigInt(0); const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); }); describe('descending hyperbola (31520 1BTC covered call)', () => { const { payoutCurve: hyperbola, totalCollateral, maxOutcome, } = CoveredCall.buildCurve(BigInt(31520), BigInt(1e8), 2, 17); it('should properly split and round with two intervals', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(1), }, { beginInterval: BigInt(31520), roundingMod: BigInt(100000), }, ]; const from = BigInt(0); const to = maxOutcome; const fromPayout = totalCollateral; const toPayout = BigInt(0); const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); }); describe('ascending hyperbola (31520 1BTC short put)', () => { const contractSize = BigInt(1e8); const totalCollateral = contractSize - BigInt(23213); const { payoutCurve: hyperbola, maxOutcome } = ShortPut.buildCurve( BigInt(31520), contractSize, totalCollateral, 2, 17, ); it('should properly split and round with two intervals', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(100000), }, { beginInterval: BigInt(31520), roundingMod: BigInt(1), }, ]; const from = BigInt(0); const to = maxOutcome; const fromPayout = BigInt(0); const toPayout = totalCollateral; const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); }); describe('ascending hyperbola (62000 0.01 BTC short put)', () => { const contractSize = BigInt(1011502); const totalCollateral = BigInt(960926); const { payoutCurve: hyperbola, maxOutcome } = ShortPut.buildCurve( BigInt(62000), contractSize, totalCollateral, 2, 18, ); it('should properly split and round with two intervals', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(1011), }, { beginInterval: BigInt(62000), roundingMod: BigInt(1), }, ]; const from = BigInt(0); const to = maxOutcome; const fromPayout = BigInt(0); const toPayout = totalCollateral; const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); it('should properly split and round with many interval', () => { const roundingIntervals: RoundingInterval[] = [ { beginInterval: BigInt(0), roundingMod: BigInt(1011), }, { beginInterval: BigInt(32000), roundingMod: BigInt(4302), }, { beginInterval: BigInt(32001), roundingMod: BigInt(1011), }, { beginInterval: BigInt(34001), roundingMod: BigInt(7011), }, { beginInterval: BigInt(62000), roundingMod: BigInt(1), }, ]; const from = BigInt(0); const to = maxOutcome; const fromPayout = BigInt(0); const toPayout = totalCollateral; const ranges = splitIntoRanges( from, to, fromPayout, toPayout, totalCollateral, hyperbola, roundingIntervals, ); validateRanges( ranges, from, fromPayout, to, toPayout, hyperbola, roundingIntervals, totalCollateral, ); }); }); }); function validateRanges( ranges: CETPayout[], from: bigint, fromPayout: bigint, to: bigint, toPayout: bigint, hyperbola: HyperbolaPayoutCurve, roundingIntervals: RoundingInterval[], totalCollateral: bigint, ) { const reversedIntervals = [...roundingIntervals].reverse(); // for each rounded payout at indexTo, expect to be equal to range payout ranges.forEach((range) => { if (range.indexFrom === from || range.indexTo === from) return expect(range.payout).to.be.eq(fromPayout); if (range.indexFrom === to || range.indexTo === to) return expect(range.payout).to.be.eq(toPayout); const payout = hyperbola.getPayout(range.indexTo); const roundingIndex = reversedIntervals.findIndex( (interval) => interval.beginInterval <= range.indexTo, ); const rounding = roundingIndex !== -1 ? reversedIntervals[roundingIndex].roundingMod : BigInt(1); const roundedPayout = BigIntMath.clamp( BigInt(0), roundPayout(payout, rounding), totalCollateral, ); expect(range.payout).to.eq(roundedPayout); }); // for each rounded payout at indexFrom, expect to be equal to range payout ranges.forEach((range) => { if (range.indexFrom === from || range.indexTo === from) return expect(range.payout).to.be.eq(fromPayout); if (range.indexFrom === to || range.indexTo === to) return expect(range.payout).to.be.eq(toPayout); const payout = hyperbola.getPayout(range.indexFrom); const roundingIndex = reversedIntervals.findIndex( (interval) => interval.beginInterval <= range.indexTo, ); const rounding = roundingIndex !== -1 ? reversedIntervals[roundingIndex].roundingMod : BigInt(1); const roundedPayout = BigIntMath.clamp( BigInt(0), roundPayout(payout, rounding), totalCollateral, ); expect(roundedPayout).to.eq(range.payout); }); }