diff --git a/packages/zoe/src/contracts/callSpread.js b/packages/zoe/src/contracts/callSpread.js deleted file mode 100644 index 5c73270adca..00000000000 --- a/packages/zoe/src/contracts/callSpread.js +++ /dev/null @@ -1,227 +0,0 @@ -// @ts-check -import '../../exported'; - -import { assert, details } from '@agoric/assert'; -import { makePromiseKit } from '@agoric/promise-kit'; -import { E } from '@agoric/eventual-send'; -import { - assertProposalShape, - depositToSeat, - natSafeMath, - trade, - assertUsesNatMath, -} from '../contractSupport'; - -const { subtract, multiply, floorDivide } = natSafeMath; - -/** - * This contract implements a fully collateralized call spread. This is a - * combination of a call option bought at one strike price and a second call - * option sold at a higher price. The invitations are produced in pairs, and the - * purchaser pays the entire amount that will be paid out. The individual - * options are ERTP invitations that are suitable for resale. - * - * This option is settled financially. There is no requirement that the original - * purchaser have ownership of the underlying asset at the start, and the - * beneficiaries shouldn't expect to take delivery at closing. - * - * The issuerKeywordRecord specifies the issuers for four keywords: Underlying, - * Strike, and Collateral. The payout is in Collateral. Strike amounts are used - * for the price oracle's quotes as to the value of the Underlying, as well as - * the strike prices in the terms. The terms include { timer, underlyingAmount, - * expiration, priceAuthority, strikePrice1, strikePrice2, settlementAmount }. - * The timer must be recognized by the priceAuthority. expiration is a time - * recognized by the timer. underlyingAmount is passed to the priceAuthority, - * so it could be an NFT or a fungible amount. strikePrice2 must be greater than - * strikePrice1. settlementAmount uses Collateral. - * - * The creatorInvitation has customProperties that include the amounts of the - * two options as longAmount and shortAmount. When the creatorInvitation is - * exercised, the payout includes the two option positions, which are themselves - * invitations which can be exercised for free, and provide the option payouts - * with the keyword Collateral. - * - * Future enhancements: - * + issue multiple option pairs with the same expiration from a single instance - * + create separate invitations to purchase the pieces of the option pair. - * (This would remove the current requirement that an intermediary have the - * total collateral available before the option descriptions have been - * created.) - * + increase the precision of the calculations. (change PERCENT_BASE to 10000) - */ - -/** - * Constants for long and short positions. - * - * @type {{ LONG: 'long', SHORT: 'short' }} - */ -const Position = { - LONG: 'long', - SHORT: 'short', -}; - -const PERCENT_BASE = 100; -const inverse = percent => subtract(PERCENT_BASE, percent); - -/** - * calculate the portion (as a percentage) of the collateral that should be - * allocated to the long side. - * - * @param {AmountMath} strikeMath the math to use - * @param {Amount} price the value of the underlying asset at closing that - * determines the payouts to the parties - * @param {Amount} strikePrice1 the lower strike price - * @param {Amount} strikePrice2 the upper strike price - * - * if price <= strikePrice1, return 0 - * if price >= strikePrice2, return 100. - * Otherwise return a number between 1 and 99 reflecting the position of price - * in the range from strikePrice1 to strikePrice2. - */ -function calculateLongShare(strikeMath, price, strikePrice1, strikePrice2) { - if (strikeMath.isGTE(strikePrice1, price)) { - return 0; - } else if (strikeMath.isGTE(price, strikePrice2)) { - return PERCENT_BASE; - } - - const denominator = strikeMath.subtract(strikePrice2, strikePrice1).value; - const numerator = strikeMath.subtract(price, strikePrice1).value; - return floorDivide(multiply(PERCENT_BASE, numerator), denominator); -} - -/** - * @type {ContractStartFn} - */ -const start = zcf => { - // terms: underlyingAmount, priceAuthority, strikePrice1, strikePrice2, - // settlementAmount, expiration, timer - - const terms = zcf.getTerms(); - const { - maths: { Collateral: collateralMath, Strike: strikeMath, Quote: quoteMath }, - brands: { Strike: strikeBrand }, - } = terms; - assertUsesNatMath(zcf, collateralMath.getBrand()); - assertUsesNatMath(zcf, strikeMath.getBrand()); - // notice that we don't assert that the Underlying is fungible. - - assert( - strikeMath.isGTE(terms.strikePrice2, terms.strikePrice1), - details`strikePrice2 must be greater than strikePrice1`, - ); - - zcf.saveIssuer(zcf.getInvitationIssuer(), 'Options'); - - // Create the two options immediately and allocate them to this seat. - const { zcfSeat: collateralSeat } = zcf.makeEmptySeatKit(); - - // Since the seats for the payout of the settlement aren't created until the - // invitations for the options themselves are exercised, we don't have those - // seats at the time of creation of the options, so we use Promises, and - // allocate the payouts when those promises resolve. - const seatPromiseKits = {}; - - seatPromiseKits[Position.LONG] = makePromiseKit(); - seatPromiseKits[Position.SHORT] = makePromiseKit(); - let seatsExited = 0; - - function reallocateToSeat(position, sharePercent) { - seatPromiseKits[position].promise.then(seat => { - const totalCollateral = terms.settlementAmount; - const collateralShare = floorDivide( - multiply(totalCollateral.value, sharePercent), - PERCENT_BASE, - ); - const seatPortion = collateralMath.make(collateralShare); - trade( - zcf, - { seat, gains: { Collateral: seatPortion } }, - { seat: collateralSeat, gains: {} }, - ); - seat.exit(); - seatsExited += 1; - const remainder = collateralSeat.getAmountAllocated('Collateral'); - if (collateralMath.isEmpty(remainder) && seatsExited === 2) { - zcf.shutdown('contract has been settled'); - } - }); - } - - function payoffOptions(priceQuoteAmount) { - const { amountOut } = quoteMath.getValue(priceQuoteAmount)[0]; - const strike1 = terms.strikePrice1; - const strike2 = terms.strikePrice2; - const longShare = calculateLongShare( - strikeMath, - amountOut, - strike1, - strike2, - ); - // either offer might be exercised late, so we pay the two seats separately. - reallocateToSeat(Position.LONG, longShare); - reallocateToSeat(Position.SHORT, inverse(longShare)); - } - - function schedulePayoffs() { - E(terms.priceAuthority) - .quoteAtTime(terms.expiration, terms.underlyingAmount, strikeBrand) - .then(priceQuote => payoffOptions(priceQuote.quoteAmount)); - } - - function makeOptionInvitation(dir) { - // All we do at time of exercise is resolve the promise. - return zcf.makeInvitation( - seat => seatPromiseKits[dir].resolve(seat), - `collect ${dir} payout`, - { position: dir }, - ); - } - - async function makeCreatorInvitation() { - const pair = { - LongOption: makeOptionInvitation(Position.LONG), - ShortOption: makeOptionInvitation(Position.SHORT), - }; - const invitationIssuer = zcf.getInvitationIssuer(); - const longAmount = await E(invitationIssuer).getAmountOf(pair.LongOption); - const shortAmount = await E(invitationIssuer).getAmountOf(pair.ShortOption); - const amounts = { LongOption: longAmount, ShortOption: shortAmount }; - await depositToSeat(zcf, collateralSeat, amounts, pair); - - // transfer collateral from creator to collateralSeat, then return a pair - // of callSpread options - /** @type {OfferHandler} */ - const createOptionsHandler = creatorSeat => { - assertProposalShape(creatorSeat, { - give: { Collateral: null }, - want: { LongOption: null, ShortOption: null }, - }); - - trade( - zcf, - { - seat: collateralSeat, - gains: { Collateral: terms.settlementAmount }, - }, - { - seat: creatorSeat, - gains: { LongOption: longAmount, ShortOption: shortAmount }, - }, - ); - schedulePayoffs(); - creatorSeat.exit(); - }; - - const custom = harden({ - longAmount, - shortAmount, - }); - return zcf.makeInvitation(createOptionsHandler, `call spread pair`, custom); - } - - return harden({ creatorInvitation: makeCreatorInvitation() }); -}; - -harden(start); -export { start, calculateLongShare }; diff --git a/packages/zoe/src/contracts/callSpread/calculateShares.js b/packages/zoe/src/contracts/callSpread/calculateShares.js new file mode 100644 index 00000000000..7b097a8c648 --- /dev/null +++ b/packages/zoe/src/contracts/callSpread/calculateShares.js @@ -0,0 +1,31 @@ +// @ts-check +import '../../../exported'; +import './types'; + +import { natSafeMath } from '../../contractSupport'; + +const { subtract, multiply, floorDivide } = natSafeMath; + +const PERCENT_BASE = 100; + +/** + * Calculate the portion (as a percentage) of the collateral that should be + * allocated to the long side of a call spread contract. price gives the value + * of the underlying asset at closing that determines the payouts to the parties + * + * @type {CalculateShares} */ +function calculateShares(strikeMath, price, strikePrice1, strikePrice2) { + if (strikeMath.isGTE(strikePrice1, price)) { + return { longShare: 0, shortShare: PERCENT_BASE }; + } else if (strikeMath.isGTE(price, strikePrice2)) { + return { longShare: PERCENT_BASE, shortShare: 0 }; + } + + const denominator = strikeMath.subtract(strikePrice2, strikePrice1).value; + const numerator = strikeMath.subtract(price, strikePrice1).value; + const longShare = floorDivide(multiply(PERCENT_BASE, numerator), denominator); + return { longShare, shortShare: subtract(PERCENT_BASE, longShare) }; +} + +harden(calculateShares); +export { calculateShares }; diff --git a/packages/zoe/src/contracts/callSpread/fundedCallSpread.js b/packages/zoe/src/contracts/callSpread/fundedCallSpread.js new file mode 100644 index 00000000000..10c03c48da3 --- /dev/null +++ b/packages/zoe/src/contracts/callSpread/fundedCallSpread.js @@ -0,0 +1,136 @@ +// @ts-check +import '../../../exported'; +import './types'; + +import { assert, details } from '@agoric/assert'; +import { makePromiseKit } from '@agoric/promise-kit'; +import { E } from '@agoric/eventual-send'; +import { + assertProposalShape, + depositToSeat, + trade, + assertUsesNatMath, +} from '../../contractSupport'; +import { makePayoffHandler } from './payoffHandler'; +import { Position } from './position'; + +/** + * This contract implements a fully collateralized call spread. This is a + * combination of a call option bought at one strike price and a second call + * option sold at a higher price. The invitations are produced in pairs, and the + * instance creator escrows upfront all collateral that will be paid out. The + * individual options are ERTP invitations that are suitable for resale. + * + * This option contract is settled financially. There is no requirement that the + * creator have ownership of the underlying asset at the start, and + * the beneficiaries shouldn't expect to take delivery at closing. + * + * The issuerKeywordRecord specifies the issuers for three keywords: Underlying, + * Strike, and Collateral. The payout is in Collateral. Strike amounts are used + * for the price oracle's quotes as to the value of the Underlying, as well as + * the strike prices in the terms. + * + * The creatorInvitation has customProperties that include the amounts of the + * two options as longAmount and shortAmount. When the creatorInvitation is + * exercised, the payout includes the two option positions, which are themselves + * invitations which can be exercised for free, and provide the option payouts + * with the keyword Collateral. + * + * terms include: + * `timer` is a timer, and must be recognized by `priceAuthority`. + * `expiration` is a time recognized by the `timer`. + * `underlyingAmount` is passed to `priceAuthority`. It could be an NFT or a + * fungible amount. + * `strikePrice2` must be greater than `strikePrice1`. + * `settlementAmount` is the amount deposited by the funder and split between + * the holders of the options. It uses Collateral. + * `priceAuthority` is an oracle that has a timer so it can respond to requests + * for prices as of a stated time. After the deadline, it will issue a + * PriceQuote giving the value of the underlying asset in the strike currency. + * + * Future enhancements: + * + issue multiple option pairs with the same expiration from a single instance + * + increase the precision of the calculations. (change PERCENT_BASE to 10000) + */ + +/** @type {ContractStartFn} */ +const start = async zcf => { + const terms = zcf.getTerms(); + const { + maths: { Collateral: collateralMath, Strike: strikeMath }, + } = terms; + assertUsesNatMath(zcf, collateralMath.getBrand()); + assertUsesNatMath(zcf, strikeMath.getBrand()); + // notice that we don't assert that the Underlying is fungible. + + assert( + strikeMath.isGTE(terms.strikePrice2, terms.strikePrice1), + details`strikePrice2 must be greater than strikePrice1`, + ); + + await zcf.saveIssuer(zcf.getInvitationIssuer(), 'Options'); + + // We will create the two options early and allocate them to this seat. + const { zcfSeat: collateralSeat } = zcf.makeEmptySeatKit(); + + // Since the seats for the payout of the settlement aren't created until the + // invitations for the options themselves are exercised, we don't have those + // seats at the time of creation of the options, so we use Promises, and + // allocate the payouts when those promises resolve. + /** @type {Record>} */ + const seatPromiseKits = { + [Position.LONG]: makePromiseKit(), + [Position.SHORT]: makePromiseKit(), + }; + + /** @type {PayoffHandler} */ + const payoffHandler = makePayoffHandler(zcf, seatPromiseKits, collateralSeat); + + async function makeFundedPairInvitation() { + const pair = { + LongOption: payoffHandler.makeOptionInvitation(Position.LONG), + ShortOption: payoffHandler.makeOptionInvitation(Position.SHORT), + }; + const invitationIssuer = zcf.getInvitationIssuer(); + const [longAmount, shortAmount] = await Promise.all([ + E(invitationIssuer).getAmountOf(pair.LongOption), + E(invitationIssuer).getAmountOf(pair.ShortOption), + ]); + const amounts = { LongOption: longAmount, ShortOption: shortAmount }; + await depositToSeat(zcf, collateralSeat, amounts, pair); + // AWAIT //// + + /** @type {OfferHandler} */ + const createOptionsHandler = creatorSeat => { + assertProposalShape(creatorSeat, { + give: { Collateral: null }, + want: { LongOption: null, ShortOption: null }, + }); + + trade( + zcf, + { + seat: collateralSeat, + gains: { Collateral: terms.settlementAmount }, + }, + { + seat: creatorSeat, + gains: { LongOption: longAmount, ShortOption: shortAmount }, + }, + ); + payoffHandler.schedulePayoffs(); + creatorSeat.exit(); + }; + + const custom = harden({ + longAmount, + shortAmount, + }); + return zcf.makeInvitation(createOptionsHandler, `call spread pair`, custom); + } + + return harden({ creatorInvitation: makeFundedPairInvitation() }); +}; + +harden(start); +export { start }; diff --git a/packages/zoe/src/contracts/callSpread/payoffHandler.js b/packages/zoe/src/contracts/callSpread/payoffHandler.js new file mode 100644 index 00000000000..6893e6225ce --- /dev/null +++ b/packages/zoe/src/contracts/callSpread/payoffHandler.js @@ -0,0 +1,93 @@ +// @ts-check +import '../../../exported'; +import './types'; + +import { E } from '@agoric/eventual-send'; +import { trade, natSafeMath } from '../../contractSupport'; +import { Position } from './position'; +import { calculateShares } from './calculateShares'; + +const { multiply, floorDivide } = natSafeMath; + +const PERCENT_BASE = 100; + +/** + * makePayoffHandler returns an object with methods that are useful for + * callSpread contracts. + * + * @type {MakePayoffHandler} + */ +function makePayoffHandler(zcf, seatPromiseKits, collateralSeat) { + const terms = zcf.getTerms(); + const { + maths: { Collateral: collateralMath, Strike: strikeMath }, + brands: { Strike: strikeBrand }, + } = terms; + let seatsExited = 0; + + /** @type {MakeOptionInvitation} */ + function makeOptionInvitation(dir) { + return zcf.makeInvitation( + // All we do at the time of exercise is resolve the promise. + seat => seatPromiseKits[dir].resolve(seat), + `collect ${dir} payout`, + { + position: dir, + }, + ); + } + + function reallocateToSeat(seatPromise, sharePercent) { + seatPromise.then(seat => { + const totalCollateral = terms.settlementAmount; + const collateralShare = floorDivide( + multiply(totalCollateral.value, sharePercent), + PERCENT_BASE, + ); + const seatPortion = collateralMath.make(collateralShare); + trade( + zcf, + { seat, gains: { Collateral: seatPortion } }, + { seat: collateralSeat, gains: {} }, + ); + seat.exit(); + seatsExited += 1; + const remainder = collateralSeat.getAmountAllocated('Collateral'); + if (collateralMath.isEmpty(remainder) && seatsExited === 2) { + zcf.shutdown('contract has been settled'); + } + }); + } + + function payoffOptions(quoteAmount) { + const strike1 = terms.strikePrice1; + const strike2 = terms.strikePrice2; + const { longShare, shortShare } = calculateShares( + strikeMath, + quoteAmount, + strike1, + strike2, + ); + // either offer might be exercised late, so we pay the two seats separately. + reallocateToSeat(seatPromiseKits[Position.LONG].promise, longShare); + reallocateToSeat(seatPromiseKits[Position.SHORT].promise, shortShare); + } + + function schedulePayoffs() { + E(terms.priceAuthority) + .quoteAtTime(terms.expiration, terms.underlyingAmount, strikeBrand) + .then(priceQuote => + payoffOptions(priceQuote.quoteAmount.value[0].amountOut), + ); + } + + /** @type {PayoffHandler} */ + const handler = harden({ + schedulePayoffs, + makeOptionInvitation, + }); + return handler; +} + +harden(makePayoffHandler); +export { makePayoffHandler }; diff --git a/packages/zoe/src/contracts/callSpread/position.js b/packages/zoe/src/contracts/callSpread/position.js new file mode 100644 index 00000000000..94f5c5f24e1 --- /dev/null +++ b/packages/zoe/src/contracts/callSpread/position.js @@ -0,0 +1,9 @@ +/** + * Constants for long and short positions. + * + * @type {{ LONG: 'long', SHORT: 'short' }} + */ +export const Position = { + LONG: 'long', + SHORT: 'short', +}; diff --git a/packages/zoe/src/contracts/callSpread/pricedCallSpread.js b/packages/zoe/src/contracts/callSpread/pricedCallSpread.js new file mode 100644 index 00000000000..0187dc7b65d --- /dev/null +++ b/packages/zoe/src/contracts/callSpread/pricedCallSpread.js @@ -0,0 +1,180 @@ +// @ts-check +import '../../../exported'; +import './types'; + +import { assert, details } from '@agoric/assert'; +import { makePromiseKit } from '@agoric/promise-kit'; +import { E } from '@agoric/eventual-send'; +import { + assertProposalShape, + depositToSeat, + trade, + assertUsesNatMath, + natSafeMath, +} from '../../contractSupport'; +import { makePayoffHandler } from './payoffHandler'; +import { Position } from './position'; + +const { subtract, multiply, floorDivide } = natSafeMath; + +/** + * This contract implements a fully collateralized call spread. This is a + * combination of a call option bought at one strike price and a second call + * option sold at a higher price. The invitations are produced in pairs. The + * creatorFacet has a method makeInvitationPair(longCollateralShare) whose + * argument must be a number between 0 and 100. makeInvitationPair() returns two + * invitations which require depositing longCollateralShare and + * (100 - longCollateralShare) to exercise the respective options/invitations. + * (They are returned under the Keyword 'Option'.) The + * options are ERTP invitations that are suitable for resale. + * + * This option contract is settled financially. There is no requirement that the + * creator have ownership of the underlying asset at the start, and + * the beneficiaries shouldn't expect to take delivery at closing. + * + * The issuerKeywordRecord specifies the issuers for three keywords: Underlying, + * Strike, and Collateral. The payout is in Collateral. Strike amounts are used + * for the price oracle's quotes as to the value of the Underlying, as well as + * the strike prices in the terms. + * + * terms include: + * `timer` is a timer, and must be recognized by `priceAuthority`. + * `expiration` is a time recognized by the `timer`. + * `underlyingAmount` is passed to `priceAuthority`. It could be an NFT or a + * fungible amount. + * `strikePrice2` must be greater than `strikePrice1`. + * `settlementAmount` is the amount deposited by the funder and split between + * the holders of the options. It uses Collateral. + * `priceAuthority` is an oracle that has a timer so it can respond to requests + * for prices as of a stated time. After the deadline, it will issue a + * PriceQuote giving the value of the underlying asset in the strike currency. + * + * Future enhancements: + * + issue multiple option pairs with the same expiration from a single instance + * + increase the precision of the calculations. (change PERCENT_BASE to 10000) + */ + +const PERCENT_BASE = 100; +const inverse = percent => subtract(PERCENT_BASE, percent); + +/** @type {ContractStartFn} */ +const start = zcf => { + const terms = zcf.getTerms(); + const { + maths: { Collateral: collateralMath, Strike: strikeMath }, + } = terms; + assertUsesNatMath(zcf, collateralMath.getBrand()); + assertUsesNatMath(zcf, strikeMath.getBrand()); + // notice that we don't assert that the Underlying is fungible. + + assert( + strikeMath.isGTE(terms.strikePrice2, terms.strikePrice1), + details`strikePrice2 must be greater than strikePrice1`, + ); + + zcf.saveIssuer(zcf.getInvitationIssuer(), 'Options'); + + // We will create the two options early and allocate them to this seat. + const { zcfSeat: collateralSeat } = zcf.makeEmptySeatKit(); + + // Since the seats for the payout of the settlement aren't created until the + // invitations for the options themselves are exercised, we don't have those + // seats at the time of creation of the options, so we use Promises, and + // allocate the payouts when those promises resolve. + /** @type {Record>} */ + const seatPromiseKits = { + [Position.LONG]: makePromiseKit(), + [Position.SHORT]: makePromiseKit(), + }; + + /** @type {PayoffHandler} */ + const payoffHandler = makePayoffHandler(zcf, seatPromiseKits, collateralSeat); + + async function makeOptionInvitation(dir, longShare) { + const option = payoffHandler.makeOptionInvitation(dir); + const invitationIssuer = zcf.getInvitationIssuer(); + const payment = harden({ Option: option }); + const spreadAmount = harden({ + Option: await E(invitationIssuer).getAmountOf(option), + }); + // AWAIT //// + + await depositToSeat(zcf, collateralSeat, spreadAmount, payment); + // AWAIT //// + + const numerator = (dir === Position.LONG) ? longShare : inverse(longShare); + const required = floorDivide( + multiply(terms.settlementAmount.value, numerator), + 100, + ); + + /** @type {OfferHandler} */ + const optionPosition = depositSeat => { + assertProposalShape(depositSeat, { + give: { Collateral: null }, + want: { Option: null }, + exit: { onDemand: null }, + }); + + const { + give: { Collateral: newCollateral }, + want: { Option: desiredOption }, + } = depositSeat.getProposal(); + + // assert that the allocation includes the amount of collateral required + assert( + collateralMath.isEqual(newCollateral, collateralMath.make(required)), + details`Collateral required: ${required}`, + ); + + // assert that the requested option was the right one. + assert( + spreadAmount.Option.value[0].instance === + desiredOption.value[0].instance, + details`wanted option not a match`, + ); + + trade( + zcf, + { seat: depositSeat, gains: spreadAmount }, + { + seat: collateralSeat, + gains: { Collateral: newCollateral }, + losses: spreadAmount, + }, + ); + depositSeat.exit(); + }; + + return zcf.makeInvitation(optionPosition, `call spread ${dir}`, { + position: dir, + collateral: required, + option: spreadAmount.Option, + }); + } + + function makeInvitationPair(longCollateralShare) { + assert( + longCollateralShare >= 0 && longCollateralShare <= 100, + 'percentages must be between 0 and 100.', + ); + + const longInvitation = makeOptionInvitation( + Position.LONG, + longCollateralShare, + ); + const shortInvitation = makeOptionInvitation( + Position.SHORT, + longCollateralShare, + ); + // TODO: don't schedule maturity until both options are exercised + payoffHandler.schedulePayoffs(); + return { longInvitation, shortInvitation }; + } + + const creatorFacet = harden({ makeInvitationPair }); + return harden({ creatorFacet }); +}; + +harden(start); +export { start }; diff --git a/packages/zoe/src/contracts/callSpread/types.js b/packages/zoe/src/contracts/callSpread/types.js new file mode 100644 index 00000000000..bf1432351eb --- /dev/null +++ b/packages/zoe/src/contracts/callSpread/types.js @@ -0,0 +1,49 @@ +/** + * @typedef {'long' | 'short'} PositionKind + */ + +/** + * @callback MakeOptionInvitation + * @param {PositionKind} positionKind + * @returns {Promise} + */ + +/** + * @typedef {Object} PayoffHandler + * @property {() => void} schedulePayoffs + * @property {MakeOptionInvitation} makeOptionInvitation + */ + +/** + * @callback MakePayoffHandler + * @param {ContractFacet} zcf + * @param {Record>} seatPromiseKits + * @param {ZCFSeat} collateralSeat + * @returns {PayoffHandler} + */ + +/** + * @typedef {Object} CalculateSharesReturn + * Return value from calculateShares, which represents the portions assigned to + * the long and short side of a transaction. These will be two non-negative + * integers that sum to 100. + * @property {number} longShare + * @property {number} shortShare + */ + +/** + * @callback CalculateShares + * calculate the portion (as a percentage) of the collateral that should be + * allocated to the long side of a call spread contract. price gives the value + * of the underlying asset at closing that determines the payouts to the parties + * + * if price <= strikePrice1, return 0 + * if price >= strikePrice2, return 100. + * Otherwise return a number between 1 and 99 reflecting the position of price + * in the range from strikePrice1 to strikePrice2. + * @param {AmountMath} strikeMath + * @param {Amount} price + * @param {Amount} strikePrice1 + * @param {Amount} strikePrice2 + * @returns {CalculateSharesReturn } + */ diff --git a/packages/zoe/test/unitTests/contracts/test-callSpread-calculation.js b/packages/zoe/test/unitTests/contracts/test-callSpread-calculation.js index e4f19f39a1a..bd74a04c225 100644 --- a/packages/zoe/test/unitTests/contracts/test-callSpread-calculation.js +++ b/packages/zoe/test/unitTests/contracts/test-callSpread-calculation.js @@ -5,7 +5,7 @@ import test from 'ava'; import '../../../exported'; import { setup } from '../setupBasicMints'; -import { calculateLongShare } from '../../../src/contracts/callSpread'; +import { calculateShares } from '../../../src/contracts/callSpread/calculateShares'; test('callSpread-calculation, at lower bound', async t => { const { moola, amountMaths } = setup(); @@ -14,7 +14,10 @@ test('callSpread-calculation, at lower bound', async t => { const strike1 = moola(20); const strike2 = moola(70); const price = moola(20); - t.is(0, calculateLongShare(moolaMath, price, strike1, strike2)); + t.deepEqual( + { longShare: 0, shortShare: 100 }, + calculateShares(moolaMath, price, strike1, strike2), + ); }); test('callSpread-calculation, at upper bound', async t => { @@ -24,7 +27,10 @@ test('callSpread-calculation, at upper bound', async t => { const strike1 = moola(20); const strike2 = moola(55); const price = moola(55); - t.is(100, calculateLongShare(moolaMath, price, strike1, strike2)); + t.deepEqual( + { longShare: 100, shortShare: 0 }, + calculateShares(moolaMath, price, strike1, strike2), + ); }); test('callSpread-calculation, below lower bound', async t => { @@ -34,7 +40,10 @@ test('callSpread-calculation, below lower bound', async t => { const strike1 = moola(15); const strike2 = moola(55); const price = moola(0); - t.is(0, calculateLongShare(moolaMath, price, strike1, strike2)); + t.deepEqual( + { longShare: 0, shortShare: 100 }, + calculateShares(moolaMath, price, strike1, strike2), + ); }); test('callSpread-calculation, above upper bound', async t => { @@ -44,7 +53,10 @@ test('callSpread-calculation, above upper bound', async t => { const strike1 = moola(15); const strike2 = moola(55); const price = moola(60); - t.is(100, calculateLongShare(moolaMath, price, strike1, strike2)); + t.deepEqual( + { longShare: 100, shortShare: 0 }, + calculateShares(moolaMath, price, strike1, strike2), + ); }); test('callSpread-calculation, mid-way', async t => { @@ -54,7 +66,10 @@ test('callSpread-calculation, mid-way', async t => { const strike1 = moola(15); const strike2 = moola(45); const price = moola(40); - t.is(83, calculateLongShare(moolaMath, price, strike1, strike2)); + t.deepEqual( + { longShare: 83, shortShare: 17 }, + calculateShares(moolaMath, price, strike1, strike2), + ); }); test('callSpread-calculation, zero', async t => { @@ -64,7 +79,10 @@ test('callSpread-calculation, zero', async t => { const strike1 = moola(15); const strike2 = moola(45); const price = moola(0); - t.is(0, calculateLongShare(moolaMath, price, strike1, strike2)); + t.deepEqual( + { longShare: 0, shortShare: 100 }, + calculateShares(moolaMath, price, strike1, strike2), + ); }); test('callSpread-calculation, large', async t => { @@ -74,5 +92,8 @@ test('callSpread-calculation, large', async t => { const strike1 = moola(15); const strike2 = moola(45); const price = moola(10000000000); - t.is(100, calculateLongShare(moolaMath, price, strike1, strike2)); + t.deepEqual( + { longShare: 100, shortShare: 0 }, + calculateShares(moolaMath, price, strike1, strike2), + ); }); diff --git a/packages/zoe/test/unitTests/contracts/test-callSpread.js b/packages/zoe/test/unitTests/contracts/test-callSpread.js index b1f48b33d8c..585ab7e5bab 100644 --- a/packages/zoe/test/unitTests/contracts/test-callSpread.js +++ b/packages/zoe/test/unitTests/contracts/test-callSpread.js @@ -11,7 +11,8 @@ import { installationPFromSource } from '../installFromSource'; import { assertPayoutDeposit, assertPayoutAmount } from '../../zoeTestHelpers'; import { makeFakePriceAuthority } from '../../../tools/fakePriceAuthority'; -const callSpread = `${__dirname}/../../../src/contracts/callSpread`; +const fundedCallSpread = `${__dirname}/../../../src/contracts/callSpread/fundedCallSpread`; +const pricedCallSpread = `${__dirname}/../../../src/contracts/callSpread/pricedCallSpread`; const simpleExchange = `${__dirname}/../../../src/contracts/simpleExchange`; const makeTestPriceAuthority = (amountMaths, priceList, timer) => @@ -25,7 +26,7 @@ const makeTestPriceAuthority = (amountMaths, priceList, timer) => // Underlying is in Simoleans. Collateral, strikePrice and Payout are in bucks. // Value is in Moola. The price oracle takes an amount in Underlying, and // gives the value in Moola. -test('callSpread below Strike1', async t => { +test('fundedCallSpread below Strike1', async t => { const { moolaIssuer, simoleanIssuer, @@ -38,7 +39,7 @@ test('callSpread below Strike1', async t => { amountMaths, brands, } = setup(); - const installation = await installationPFromSource(zoe, callSpread); + const installation = await installationPFromSource(zoe, fundedCallSpread); // Alice will create and fund a call spread contract, and give the invitations // to Bob and Carol. Bob and Carol will promptly schedule collection of funds. @@ -68,7 +69,7 @@ test('callSpread below Strike1', async t => { timer: manualTimer, }); - // Alice creates a callSpread instance + // Alice creates a fundedCallSpread instance const issuerKeywordRecord = harden({ Underlying: simoleanIssuer, Collateral: bucksIssuer, @@ -124,7 +125,7 @@ test('callSpread below Strike1', async t => { // Underlying is in Simoleans. Collateral, strikePrice and Payout are in bucks. // Value is in Moola. -test('callSpread above Strike2', async t => { +test('fundedCallSpread above Strike2', async t => { const { moolaIssuer, simoleanIssuer, @@ -135,9 +136,8 @@ test('callSpread above Strike2', async t => { bucks, zoe, amountMaths, - brands, } = setup(); - const installation = await installationPFromSource(zoe, callSpread); + const installation = await installationPFromSource(zoe, fundedCallSpread); // Alice will create and fund a call spread contract, and give the invitations // to Bob and Carol. Bob and Carol will promptly schedule collection of funds. @@ -167,15 +167,11 @@ test('callSpread above Strike2', async t => { timer: manualTimer, }); - // Alice creates a callSpread instance + // Alice creates a fundedCallSpread instance const issuerKeywordRecord = harden({ Underlying: simoleanIssuer, Collateral: bucksIssuer, Strike: moolaIssuer, - Quote: await E(priceAuthority).getQuoteIssuer( - brands.get('simoleans'), - brands.get('moola'), - ), }); const { creatorInvitation } = await zoe.startInstance( @@ -229,7 +225,7 @@ test('callSpread above Strike2', async t => { // Underlying is in Simoleans. Collateral, strikePrice and Payout are in bucks. // Value is in Moola. -test('callSpread, mid-strike', async t => { +test('fundedCallSpread, mid-strike', async t => { const { moolaIssuer, simoleanIssuer, @@ -240,9 +236,8 @@ test('callSpread, mid-strike', async t => { bucks, zoe, amountMaths, - brands, } = setup(); - const installation = await installationPFromSource(zoe, callSpread); + const installation = await installationPFromSource(zoe, fundedCallSpread); // Alice will create and fund a call spread contract, and give the invitations // to Bob and Carol. Bob and Carol will promptly schedule collection of funds. @@ -271,15 +266,11 @@ test('callSpread, mid-strike', async t => { settlementAmount: bucks(300), timer: manualTimer, }); - // Alice creates a callSpread instance + // Alice creates a fundedCallSpread instance const issuerKeywordRecord = harden({ Underlying: simoleanIssuer, Collateral: bucksIssuer, Strike: moolaIssuer, - Quote: await E(priceAuthority).getQuoteIssuer( - brands.get('simoleans'), - brands.get('moola'), - ), }); const { creatorInvitation } = await zoe.startInstance( @@ -333,7 +324,7 @@ test('callSpread, mid-strike', async t => { // Underlying is in Simoleans. Collateral, strikePrice and Payout are in bucks. // Value is in Moola. Carol waits to collect until after settlement -test('callSpread, late exercise', async t => { +test('fundedCallSpread, late exercise', async t => { const { moolaIssuer, simoleanIssuer, @@ -344,9 +335,8 @@ test('callSpread, late exercise', async t => { bucks, zoe, amountMaths, - brands, } = setup(); - const installation = await installationPFromSource(zoe, callSpread); + const installation = await installationPFromSource(zoe, fundedCallSpread); // Alice will create and fund a call spread contract, and give the invitations // to Bob and Carol. Bob and Carol will promptly schedule collection of funds. @@ -376,15 +366,11 @@ test('callSpread, late exercise', async t => { timer: manualTimer, }); - // Alice creates a callSpread instance + // Alice creates a fundedCallSpread instance const issuerKeywordRecord = harden({ Underlying: simoleanIssuer, Collateral: bucksIssuer, Strike: moolaIssuer, - Quote: await E(priceAuthority).getQuoteIssuer( - brands.get('simoleans'), - brands.get('moola'), - ), }); const { creatorInvitation } = await zoe.startInstance( installation, @@ -437,7 +423,7 @@ test('callSpread, late exercise', async t => { await Promise.all([bobDeposit]); }); -test('callSpread, sell options', async t => { +test('fundedCallSpread, sell options', async t => { const { moolaIssuer, simoleanIssuer, @@ -448,9 +434,8 @@ test('callSpread, sell options', async t => { bucks, zoe, amountMaths, - brands, } = setup(); - const installation = await installationPFromSource(zoe, callSpread); + const installation = await installationPFromSource(zoe, fundedCallSpread); const invitationIssuer = await E(zoe).getInvitationIssuer(); // Alice will create and fund a call spread contract, and sell the invitations @@ -484,15 +469,11 @@ test('callSpread, sell options', async t => { timer: manualTimer, }); - // Alice creates a callSpread instance + // Alice creates a fundedCallSpread instance const issuerKeywordRecord = harden({ Underlying: simoleanIssuer, Collateral: bucksIssuer, Strike: moolaIssuer, - Quote: await E(priceAuthority).getQuoteIssuer( - brands.get('simoleans'), - brands.get('moola'), - ), }); const { creatorInvitation } = await zoe.startInstance( installation, @@ -620,3 +601,121 @@ test('callSpread, sell options', async t => { await E(manualTimer).tick(); await Promise.all([aliceLong, aliceShort, bobDeposit, carolDeposit]); }); + +test('pricedCallSpread, mid-strike', async t => { + const { + moolaIssuer, + simoleanIssuer, + moola, + simoleans, + bucksIssuer, + bucksMint, + bucks, + zoe, + amountMaths, + } = setup(); + const installation = await installationPFromSource(zoe, pricedCallSpread); + + // Alice will create a call spread contract, and give the invitations + // to Bob and Carol. Bob and Carol will fund and exercise, then promptly + // schedule collection of funds. The spread will then mature, and both will + // get paid. + + // Setup Bob + const bobBucksPurse = bucksIssuer.makeEmptyPurse(); + const bobBucksPayment = bucksMint.mintPayment(bucks(225)); + // Setup Carol + const carolBucksPurse = bucksIssuer.makeEmptyPurse(); + const carolBucksPayment = bucksMint.mintPayment(bucks(75)); + + const manualTimer = buildManualTimer(console.log, 0); + const priceAuthority = await makeTestPriceAuthority( + amountMaths, + [20, 45, 45, 45, 45, 45, 45], + manualTimer, + ); + // underlying is 2 Simoleans, strike range is 30-50 (doubled) + const terms = harden({ + expiration: 3, + underlyingAmount: simoleans(2), + priceAuthority, + strikePrice1: moola(60), + strikePrice2: moola(100), + settlementAmount: bucks(300), + timer: manualTimer, + }); + // Alice creates a pricedCallSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + }); + const { creatorFacet } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + const invitationPair = await E(creatorFacet).makeInvitationPair(75); + const { longInvitation, shortInvitation } = invitationPair; + + const invitationIssuer = await E(zoe).getInvitationIssuer(); + const longAmount = await E(invitationIssuer).getAmountOf(longInvitation); + const shortAmount = await E(invitationIssuer).getAmountOf(shortInvitation); + + const longOptionValue = longAmount.value[0]; + t.is('long', longOptionValue.position); + const longOption = longOptionValue.option; + + // Bob makes an offer for the long option + const bobProposal = harden({ + want: { Option: longOption }, + give: { Collateral: bucks(longOptionValue.collateral) }, + }); + const bobFundingSeat = await zoe.offer(await longInvitation, bobProposal, { + Collateral: bobBucksPayment, + }); + // bob gets an option, and exercises it for the payout + const bobOption = await bobFundingSeat.getPayout('Option'); + const bobOptionSeat = await zoe.offer(bobOption); + + const bobPayout = bobOptionSeat.getPayout('Collateral'); + const bobDeposit = assertPayoutDeposit( + t, + bobPayout, + bobBucksPurse, + bucks(225), + ); + + const shortOptionValue = shortAmount.value[0]; + t.is('short', shortOptionValue.position); + const shortOption = shortOptionValue.option; + + // carol makes an offer for the short option + const carolProposal = harden({ + want: { Option: shortOption }, + give: { Collateral: bucks(shortOptionValue.collateral) }, + }); + const carolFundingSeat = await zoe.offer( + await shortInvitation, + carolProposal, + { + Collateral: carolBucksPayment, + }, + ); + // carol gets an option, and exercises it for the payout + const carolOption = await carolFundingSeat.getPayout('Option'); + const carolOptionSeat = await zoe.offer(carolOption); + + const carolPayout = carolOptionSeat.getPayout('Collateral'); + const carolDeposit = assertPayoutDeposit( + t, + carolPayout, + carolBucksPurse, + bucks(75), + ); + + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await Promise.all([bobDeposit, carolDeposit]); +});