Skip to content

Commit

Permalink
feat: 2 parties can buy callSpread positions separately (#2019)
Browse files Browse the repository at this point in the history
This is a separate Zoe contract from the previous call spread, which
is renamed to fundedCallSpread (because the first party funds the
entire payout). The new one is pricedCallSpread (the first party
decides the prices, and gets two invitations to buy each position
separately.)

Common code is mostly factored out. There are 30 lines of boilerplate
at the beginning of each contract that seems better visible there
rather than hidden behind function calls.

I made a start at generating typescript declarations, but don't see
what eslint doesn't like about the current state. (The error messages
just say "syntax error", which doesn't help.

Also there's a weirdness in fundedCallSpread (marked with a TODO). I
want to replace sequential await calls with a single Promise.all(),
but the replacement code doesn't work as I expect it to. If anyone can
spot my mistake, I'd appreciate it.
  • Loading branch information
Chris-Hibbert authored Nov 19, 2020
1 parent eea0e01 commit 2b19988
Show file tree
Hide file tree
Showing 9 changed files with 662 additions and 271 deletions.
227 changes: 0 additions & 227 deletions packages/zoe/src/contracts/callSpread.js

This file was deleted.

31 changes: 31 additions & 0 deletions packages/zoe/src/contracts/callSpread/calculateShares.js
Original file line number Diff line number Diff line change
@@ -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 };
136 changes: 136 additions & 0 deletions packages/zoe/src/contracts/callSpread/fundedCallSpread.js
Original file line number Diff line number Diff line change
@@ -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<PositionKind,PromiseRecord<ZCFSeat>>} */
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 };
Loading

0 comments on commit 2b19988

Please sign in to comment.