diff --git a/packages/core/__tests__/dlc/finance/Builder.spec.ts b/packages/core/__tests__/dlc/finance/Builder.spec.ts index c87e46de..e2999a7e 100644 --- a/packages/core/__tests__/dlc/finance/Builder.spec.ts +++ b/packages/core/__tests__/dlc/finance/Builder.spec.ts @@ -12,6 +12,8 @@ import sinon from 'sinon'; import { buildCoveredCallOrderOffer, buildCustomStrategyOrderOffer, + buildLongCallOrderOffer, + buildLongPutOrderOffer, buildRoundingIntervalsFromIntervals, buildShortPutOrderOffer, computeRoundingModulus, @@ -64,6 +66,36 @@ describe('OrderOffer Builder', () => { expect(() => orderOffer.validate()).to.not.throw(Error); }); + it('should build a long call OrderOffer correctly', () => { + const orderOffer = buildLongCallOrderOffer( + oracleAnnouncement, + contractSize, + strikePrice, + premium * 3, + premium, + 12, + 10000, + 'bitcoin', + ); + + expect(() => orderOffer.validate()).to.not.throw(Error); + }); + + it('should build a long put OrderOffer correctly', () => { + const orderOffer = buildLongPutOrderOffer( + oracleAnnouncement, + contractSize, + strikePrice, + premium * 3, + premium, + 12, + 10000, + 'bitcoin', + ); + + expect(() => orderOffer.validate()).to.not.throw(Error); + }); + it('should fail to build an OrderOffer with an invalid oracleAnnouncement', () => { oracleAnnouncement.announcementSig = Buffer.from('deadbeef', 'hex'); diff --git a/packages/core/__tests__/dlc/finance/LongCall.spec.ts b/packages/core/__tests__/dlc/finance/LongCall.spec.ts new file mode 100644 index 00000000..bfef8490 --- /dev/null +++ b/packages/core/__tests__/dlc/finance/LongCall.spec.ts @@ -0,0 +1,55 @@ +import { HyperbolaPayoutCurvePiece } from '@node-dlc/messaging'; +import { expect } from 'chai'; + +import { LongCall } from '../../../lib/dlc/finance/LongCall'; +import { HyperbolaPayoutCurve } from '../../../lib/dlc/HyperbolaPayoutCurve'; + +describe('LongCall', () => { + describe('1BTC-50k-base2-18digit curve', () => { + const strikePrice = BigInt(50000); + const contractSize = BigInt(10) ** BigInt(8); + const oracleBase = 2; + const oracleDigits = 18; + + const { payoutCurve } = LongCall.buildCurve( + strikePrice, + contractSize, + oracleBase, + oracleDigits, + ); + + describe('payout', () => { + const priceIncrease = BigInt(1000); + + it('should be positive as the position goes ITM', () => { + expect( + payoutCurve + .getPayout(strikePrice + priceIncrease) + .integerValue() + .toNumber(), + ).to.equal( + Number( + (contractSize * priceIncrease) / (strikePrice + priceIncrease), + ), + ); + }); + + it('should be zero at strike price', () => { + expect(payoutCurve.getPayout(strikePrice).toNumber()).to.equal(0); + }); + }); + + it('should serialize and deserialize properly', () => { + const payout = payoutCurve.getPayout(strikePrice); + + const _tlv = payoutCurve.toPayoutCurvePiece().serialize(); + const pf = HyperbolaPayoutCurvePiece.deserialize(_tlv); + + const deserializedCurve = HyperbolaPayoutCurve.fromPayoutCurvePiece(pf); + + expect(payout.toNumber()).to.be.eq( + deserializedCurve.getPayout(strikePrice).toNumber(), + ); + }); + }); +}); diff --git a/packages/core/__tests__/dlc/finance/LongPut.spec.ts b/packages/core/__tests__/dlc/finance/LongPut.spec.ts new file mode 100644 index 00000000..211f919e --- /dev/null +++ b/packages/core/__tests__/dlc/finance/LongPut.spec.ts @@ -0,0 +1,55 @@ +import { HyperbolaPayoutCurvePiece } from '@node-dlc/messaging'; +import { expect } from 'chai'; + +import { LongPut } from '../../../lib'; +import { HyperbolaPayoutCurve } from '../../../lib/dlc/HyperbolaPayoutCurve'; + +describe('LongPut', () => { + describe('1BTC-50k-base2-18digit curve', () => { + const strikePrice = BigInt(50000); + const contractSize = BigInt(10) ** BigInt(8); + const oracleBase = 2; + const oracleDigits = 18; + + const { payoutCurve } = LongPut.buildCurve( + strikePrice, + contractSize, + oracleBase, + oracleDigits, + ); + + describe('payout', () => { + const priceDecrease = BigInt(1000); + + it('should be positive as the position goes ITM', () => { + expect( + payoutCurve + .getPayout(strikePrice - priceDecrease) + .integerValue() + .toNumber(), + ).to.equal( + Number( + (contractSize * priceDecrease) / (strikePrice - priceDecrease), + ), + ); + }); + + it('should be zero at strike price', () => { + expect(payoutCurve.getPayout(strikePrice).toNumber()).to.equal(0); + }); + }); + + it('should serialize and deserialize properly', () => { + const payout = payoutCurve.getPayout(strikePrice); + + const _tlv = payoutCurve.toPayoutCurvePiece().serialize(); + const pf = HyperbolaPayoutCurvePiece.deserialize(_tlv); + + const deserializedCurve = HyperbolaPayoutCurve.fromPayoutCurvePiece(pf); + + expect(payout.toNumber()).to.be.eq( + deserializedCurve.getPayout(strikePrice).toNumber(), + ); + }); + }); +}); diff --git a/packages/core/lib/dlc/finance/Builder.ts b/packages/core/lib/dlc/finance/Builder.ts index d435f37c..5ef13d22 100644 --- a/packages/core/lib/dlc/finance/Builder.ts +++ b/packages/core/lib/dlc/finance/Builder.ts @@ -19,6 +19,8 @@ import { import { CoveredCall } from './CoveredCall'; import { LinearPayout } from './LinearPayout'; +import { LongCall } from './LongCall'; +import { LongPut } from './LongPut'; import { ShortPut } from './ShortPut'; export const UNIT_MULTIPLIER = { @@ -162,6 +164,7 @@ export const buildOptionOrderOffer = ( rounding: number, network: string, type: 'call' | 'put', + direction: 'long' | 'short', _totalCollateral?: number, ): OrderOfferV0 => { const eventDescriptor = getDigitDecompositionEventDescriptor(announcement); @@ -171,50 +174,98 @@ export const buildOptionOrderOffer = ( payoutFunction: PayoutFunctionV0; totalCollateral?: bigint; }; + const roundingIntervals = new RoundingIntervalsV0(); const roundingMod = computeRoundingModulus(rounding, contractSize); - if (type === 'call') { - payoutFunctionInfo = CoveredCall.buildPayoutFunction( - BigInt(strikePrice), - BigInt(contractSize), - eventDescriptor.base, - eventDescriptor.nbDigits, - ); - - totalCollateral = payoutFunctionInfo.totalCollateral; - roundingIntervals.intervals = [ - { - beginInterval: BigInt(0), - roundingMod: BigInt(1), - }, - { - beginInterval: BigInt(strikePrice), - roundingMod, - }, - ]; + if (direction === 'short') { + if (type === 'call') { + payoutFunctionInfo = CoveredCall.buildPayoutFunction( + BigInt(strikePrice), + BigInt(contractSize), + eventDescriptor.base, + eventDescriptor.nbDigits, + ); + + totalCollateral = payoutFunctionInfo.totalCollateral; + roundingIntervals.intervals = [ + { + beginInterval: BigInt(0), + roundingMod: BigInt(1), + }, + { + beginInterval: BigInt(strikePrice), + roundingMod, + }, + ]; + } else { + payoutFunctionInfo = ShortPut.buildPayoutFunction( + BigInt(strikePrice), + BigInt(contractSize), + BigInt(_totalCollateral), + eventDescriptor.base, + eventDescriptor.nbDigits, + ); + totalCollateral = BigInt(_totalCollateral); + roundingIntervals.intervals = [ + { + beginInterval: BigInt(0), + roundingMod, + }, + { + beginInterval: BigInt(strikePrice), + roundingMod: BigInt(1), + }, + ]; + } } else { - payoutFunctionInfo = ShortPut.buildPayoutFunction( - BigInt(strikePrice), - BigInt(contractSize), - BigInt(_totalCollateral), - eventDescriptor.base, - eventDescriptor.nbDigits, - ); totalCollateral = BigInt(_totalCollateral); - roundingIntervals.intervals = [ - { - beginInterval: BigInt(0), - roundingMod, - }, - { - beginInterval: BigInt(strikePrice), - roundingMod: BigInt(1), - }, - ]; + + if (type === 'call') { + payoutFunctionInfo = LongCall.buildPayoutFunction( + BigInt(strikePrice), + BigInt(contractSize), + totalCollateral, + eventDescriptor.base, + eventDescriptor.nbDigits, + ); + + roundingIntervals.intervals = [ + { + beginInterval: BigInt(0), + roundingMod: BigInt(1), + }, + { + beginInterval: BigInt(strikePrice), + roundingMod, + }, + ]; + } else { + payoutFunctionInfo = LongPut.buildPayoutFunction( + BigInt(strikePrice), + BigInt(contractSize), + totalCollateral, + eventDescriptor.base, + eventDescriptor.nbDigits, + ); + + roundingIntervals.intervals = [ + { + beginInterval: BigInt(0), + roundingMod, + }, + { + beginInterval: BigInt(strikePrice), + roundingMod: BigInt(1), + }, + ]; + } } + const payoutFunction = payoutFunctionInfo.payoutFunction; - const offerCollateral = totalCollateral - BigInt(premium); + + const offerCollateral = + direction === 'short' ? totalCollateral - BigInt(premium) : BigInt(premium); return buildOrderOffer( announcement, @@ -257,6 +308,7 @@ export const buildCoveredCallOrderOffer = ( rounding, network, 'call', + 'short', ); }; @@ -292,10 +344,85 @@ export const buildShortPutOrderOffer = ( rounding, network, 'put', + 'short', totalCollateral, ); }; +/** + * Builds an order offer for a long call + * + * @param {OracleAnnouncementV0} announcement oracle announcement + * @param {number} contractSize contract size in satoshis + * @param {number} strikePrice strike price of contract + * @param {number} maxGain maximum amount that can be gained (totalCollateral) + * @param {number} premium premium of contract in satoshis + * @param {number} feePerByte sats/vbyte + * @param {number} rounding rounding interval + * @param {string} network bitcoin network type + * @returns {OrderOfferV0} Returns order offer + */ +export const buildLongCallOrderOffer = ( + announcement: OracleAnnouncementV0, + contractSize: number, + strikePrice: number, + maxGain: number, + premium: number, + feePerByte: number, + rounding: number, + network: string, +): OrderOfferV0 => { + return buildOptionOrderOffer( + announcement, + contractSize, + strikePrice, + premium, + feePerByte, + rounding, + network, + 'call', + 'long', + maxGain, + ); +}; + +/** + * Builds an order offer for a long put + * + * @param {OracleAnnouncementV0} announcement oracle announcement + * @param {number} contractSize contract size in satoshis + * @param {number} strikePrice strike price of contract + * @param {number} maxGain maximum amount that can be gained (totalCollateral) + * @param {number} premium premium of contract in satoshis + * @param {number} feePerByte sats/vbyte + * @param {number} rounding rounding interval + * @param {string} network bitcoin network type + * @returns {OrderOfferV0} Returns order offer + */ +export const buildLongPutOrderOffer = ( + announcement: OracleAnnouncementV0, + contractSize: number, + strikePrice: number, + maxGain: number, + premium: number, + feePerByte: number, + rounding: number, + network: string, +): OrderOfferV0 => { + return buildOptionOrderOffer( + announcement, + contractSize, + strikePrice, + premium, + feePerByte, + rounding, + network, + 'put', + 'long', + maxGain, + ); +}; + /** * Builds an order offer for a linear curve * diff --git a/packages/core/lib/dlc/finance/LongCall.ts b/packages/core/lib/dlc/finance/LongCall.ts new file mode 100644 index 00000000..7d008378 --- /dev/null +++ b/packages/core/lib/dlc/finance/LongCall.ts @@ -0,0 +1,69 @@ +import { PayoutFunctionV0 } from '@node-dlc/messaging'; +import BN from 'bignumber.js'; + +import { HyperbolaPayoutCurve } from '../HyperbolaPayoutCurve'; + +const buildCurve = ( + strikePrice: bigint, + contractSize: bigint, + oracleBase: number, + oracleDigits: number, +): { + maxOutcome: bigint; + totalCollateral: bigint; + payoutCurve: HyperbolaPayoutCurve; +} => { + const a = new BN(-1); + const b = new BN(0); + const c = new BN(0); + const d = new BN((strikePrice * contractSize).toString()); + + const f_1 = new BN(0); + const f_2 = new BN(Number(contractSize)); + + const payoutCurve = new HyperbolaPayoutCurve(a, b, c, d, f_1, f_2); + + const maxOutcome = BigInt( + new BN(oracleBase).pow(oracleDigits).minus(1).toString(10), + ); + + return { + maxOutcome, + totalCollateral: contractSize, + payoutCurve, + }; +}; + +const buildPayoutFunction = ( + strikePrice: bigint, + contractSize: bigint, + totalCollateral: bigint, + oracleBase: number, + oracleDigits: number, +): { payoutFunction: PayoutFunctionV0; totalCollateral: bigint } => { + const { maxOutcome, payoutCurve } = buildCurve( + strikePrice, + contractSize, + oracleBase, + oracleDigits, + ); + + const payoutFunction = new PayoutFunctionV0(); + payoutFunction.endpoint0 = BigInt(0); + payoutFunction.endpointPayout0 = BigInt(0); + payoutFunction.extraPrecision0 = 0; + + payoutFunction.pieces.push({ + payoutCurvePiece: payoutCurve.toPayoutCurvePiece(), + endpoint: maxOutcome, + endpointPayout: totalCollateral, + extraPrecision: 0, + }); + + return { + payoutFunction, + totalCollateral, + }; +}; + +export const LongCall = { buildCurve, buildPayoutFunction }; diff --git a/packages/core/lib/dlc/finance/LongPut.ts b/packages/core/lib/dlc/finance/LongPut.ts new file mode 100644 index 00000000..6a75ccff --- /dev/null +++ b/packages/core/lib/dlc/finance/LongPut.ts @@ -0,0 +1,69 @@ +import { PayoutFunctionV0 } from '@node-dlc/messaging'; +import BN from 'bignumber.js'; + +import { HyperbolaPayoutCurve } from '../HyperbolaPayoutCurve'; + +const buildCurve = ( + strikePrice: bigint, + contractSize: bigint, + oracleBase: number, + oracleDigits: number, +): { + maxOutcome: bigint; + totalCollateral: bigint; + payoutCurve: HyperbolaPayoutCurve; +} => { + const a = new BN(1); + const b = new BN(0); + const c = new BN(0); + const d = new BN((strikePrice * contractSize).toString()); + + const f_1 = new BN(0); + const f_2 = new BN(-Number(contractSize)); + + const payoutCurve = new HyperbolaPayoutCurve(a, b, c, d, f_1, f_2); + + const maxOutcome = BigInt( + new BN(oracleBase).pow(oracleDigits).minus(1).toString(10), + ); + + return { + maxOutcome, + totalCollateral: contractSize, + payoutCurve, + }; +}; + +const buildPayoutFunction = ( + strikePrice: bigint, + contractSize: bigint, + totalCollateral: bigint, + oracleBase: number, + oracleDigits: number, +): { payoutFunction: PayoutFunctionV0; totalCollateral: bigint } => { + const { maxOutcome, payoutCurve } = buildCurve( + strikePrice, + contractSize, + oracleBase, + oracleDigits, + ); + + const payoutFunction = new PayoutFunctionV0(); + payoutFunction.endpoint0 = BigInt(0); + payoutFunction.endpointPayout0 = totalCollateral; + payoutFunction.extraPrecision0 = 0; + + payoutFunction.pieces.push({ + payoutCurvePiece: payoutCurve.toPayoutCurvePiece(), + endpoint: maxOutcome, + endpointPayout: BigInt(0), + extraPrecision: 0, + }); + + return { + payoutFunction, + totalCollateral, + }; +}; + +export const LongPut = { buildCurve, buildPayoutFunction }; diff --git a/packages/core/lib/index.ts b/packages/core/lib/index.ts index 962e1951..df137e5f 100644 --- a/packages/core/lib/index.ts +++ b/packages/core/lib/index.ts @@ -1,10 +1,12 @@ export * from './dlc/CETCalculator'; export * from './dlc/finance/Builder'; export * from './dlc/finance/CoveredCall'; +export * from './dlc/finance/ShortPut'; +export * from './dlc/finance/LongCall'; +export * from './dlc/finance/LongPut'; export * from './dlc/finance/CsoInfo'; export * from './dlc/finance/LinearPayout'; export * from './dlc/finance/OptionInfo'; -export * from './dlc/finance/ShortPut'; export * from './dlc/HyperbolaPayoutCurve'; export * from './dlc/PayoutCurve'; export * from './dlc/PolynomialPayoutCurve'; diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index 77cd0784..4b2887ab 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -97,7 +97,7 @@ "bip66": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", - "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", "requires": { "safe-buffer": "^5.0.1" } @@ -145,12 +145,12 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, "bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", "requires": { "base-x": "^3.0.2" } @@ -240,7 +240,7 @@ "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -265,7 +265,7 @@ "merkle-lib": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/merkle-lib/-/merkle-lib-2.0.10.tgz", - "integrity": "sha1-grjbrnXieneFOItz+ddyXQ9vMyY=" + "integrity": "sha512-XrNQvUbn1DL5hKNe46Ccs+Tu3/PYOlrcZILuGUhb95oKBPjc/nmIC8D462PQkipVDGKRvwhn+QFg2cCdIvmDJA==" }, "minimalistic-assert": { "version": "1.0.1", @@ -275,7 +275,7 @@ "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" }, "nan": { "version": "2.16.0", @@ -295,7 +295,7 @@ "pushdata-bitcoin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", - "integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=", + "integrity": "sha512-hw7rcYTJRAl4olM8Owe8x0fBuJJ+WGbMhQuLWOXEMN3PxPCKQHRkhfL+XG0+iXUmSHjkMmb3Ba55Mt21cZc9kQ==", "requires": { "bitcoin-ops": "^1.3.0" } @@ -379,7 +379,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "varuint-bitcoin": { "version": "1.1.2", @@ -392,7 +392,7 @@ "wif": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", - "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", + "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==", "requires": { "bs58check": "<3.0.0" }