Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: a call spread option contract and tests. #1854

Merged
merged 5 commits into from
Oct 29, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions packages/zoe/src/contracts/callSpread.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// @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;

/**
* 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);

/**
* 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Joe describes this as "a call at a lower strike and a put at a higher strike". I think a sold call option is different than a put.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"One contract to rule them all" says "A more general structure is a call spread, which is a bought call at a lower strike and a sold call at a higher strike. This generalises calls, puts, binaries (including prediction markets), and futures." It later says (under "Fully collateralized call spread") "This is a call at a lower strike and a put at a higher strike."

I think the second is a mistake. It would produce the right outcomes for the long position, but the short position wouldn't have all the rights they should. I've asked for clarification in the doc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Joe responded that they are both calls. The earlier (in the document) statement was correct.

* option sold at a higher price. The contracts are sold in pairs, and the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* option sold at a higher price. The contracts are sold in pairs, and the
* option sold at a higher price. The invitations are sold in pairs, and the

I don't think we want to call the invitations contracts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this contract is not about selling invitations at all - that's a different level. This contract produces invitations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep. thanks for the correction.

* 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Options shouldn't need an issuer specified. It's a Zoe invitation, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, Zoe doesn't know about the invitationIssuer on a per contract level. Huh, wonder if we should pre-register that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could go either way on that. We haven't needed it for other contracts to date. But you're right, it's a general Zoe facility.

* Strike, Collateral and Options. 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. Options indicates the
* invitationIssuer, which is part of the amounts of the options. The terms
* include { expiration, underlyingAmount, priceAuthority, strikePrice1,
* strikePrice2, settlementAmount }. expiration is a time recognized by the
* priceAuthority. 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 terms that include the amounts of the two options
* as longOption and shortOption. When the creatorInvitation is exercised, the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we mean terms or customProperties?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right.

* payout includes the two option positions, which are themselves invitations
* which can be exercised for free, and provide the option payouts.
*
* 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.)
Comment on lines +46 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's no purchasing of the option pair happening in this contract. The creator isn't purchasing them, and if we split up that role, the two parties wouldn't be purchasing the options either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is currently no purchasing of the option pair in this contract.

This goes back to my previous approach. I think it would still be valuable, as it's required in order to not have an intermediary have to front the funds.

* + exit the contract when both seats have been paid.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, what makes this difficult?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do my rewrite of reallocateShare below, you could put a check to see if there any collateral left, and if not, shutdown:

    const reallocateShare = (seat, share) => {
      trade(
        zcf,
        { seat: collateralSeat, gains: {} },
        { seat, gains: { Collateral: share } },
      );
      seat.exit();

      if (
        collateralMath.isEmpty(collateralSeat.getAmountAllocated('Collateral'))
      ) {
        zcf.shutdown('contract has been financially settled');
      }
    };

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not hard, I just thought (lazily, I'll admit) that it would make a simple starter project for someone.

It wasn't sufficient to check for an empty seat, because the complementary seat might be getting zero, but still want to exit cleanly.

* + increase the precision of the calcluations. (change PERCENT_BASE to 10000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* + increase the precision of the calcluations. (change PERCENT_BASE to 10000)
* + increase the precision of the calculations. (change PERCENT_BASE to 10000)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh. yep.

*
* @type {ContractStartFn}
*/
const start = zcf => {
// terms: underlyingAmount, priceAuthority, strike1, strike2,
// settlementAmount, expiration

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`,
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add the Zoe invitation issuer here so the creator doesn't have to define it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh! good idea. we have .saveIssuer().

// 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 = {};
Comment on lines +119 to +123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!


seatPromiseKits[Position.LONG] = makePromiseKit();
seatPromiseKits[Position.SHORT] = makePromiseKit();

function reallocateToSeat(position, sharePercent) {
seatPromiseKits[position].promise.then(seat => {
const currentCollateral = collateralSeat.getCurrentAllocation()
.Collateral;
const totalCollateral = terms.settlementAmount;
const collateralShare = floorDivide(
multiply(totalCollateral.value, sharePercent),
PERCENT_BASE,
);
const seatPortion = collateralMath.make(collateralShare);
const collateralRemainder = collateralMath.subtract(
currentCollateral,
seatPortion,
);
zcf.reallocate(
seat.stage({ Collateral: seatPortion }),
collateralSeat.stage({ Collateral: collateralRemainder }),
);
seat.exit();
});
}

// calculate the portion (as a percentage) of the collateral that should be
// allocated to the long side.
function calculateLongShare(price) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make a comment of the formula in mathematical notation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding correctly, it's a step function where:

if price < strike1: short gets all collateral
if price > strike2: long gets all collateral
else: the collateral is allocated proportionally based on how close it is to strike1 or strike2

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a common function or did we come up with this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct.
We (including Joe) didn't come up with this. It's in all the diagrams explaining call spreads that I've found, and no one ever gives it a name.

if (strikeMath.isGTE(terms.strikePrice1, price)) {
return 0;
} else if (strikeMath.isGTE(price, terms.strikePrice2)) {
return PERCENT_BASE;
}

const denominator = strikeMath.subtract(
terms.strikePrice2,
terms.strikePrice1,
).value;
const numerator = strikeMath.subtract(price, terms.strikePrice1).value;
return floorDivide(multiply(PERCENT_BASE, numerator), denominator);
}

function payoffOptions(price) {
// either offer might be exercised late, so we pay the two seats separately.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, if we are doing the reallocations only after the seat resolves (seatPromiseKits[position].promise.then(seat => { on line 98, we don't need to do this entirely separately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify by doing the calculations as one step and then only having the reallocation occur after the seat resolves like:

function payoffOptions(price) {
    const longPercentShare = calculateLongShare(price);
    const totalCollateral = terms.settlementAmount;
    const longShare = collateralMath.make(
      floorDivide(
        multiply(totalCollateral.value, longPercentShare),
        PERCENT_BASE,
      ),
    );
    const shortShare = collateralMath.subtract(totalCollateral, longShare);

    const reallocateShare = (seat, share) => {
      trade(
        zcf,
        { seat: collateralSeat, gains: {} },
        { seat, gains: { Collateral: share } },
      );
      seat.exit();
    };

    seatPromiseKits[Position.LONG].promise.then(longSeat =>
      reallocateShare(longSeat, longShare),
    );
    seatPromiseKits[Position.SHORT].promise.then(shortSeat =>
      reallocateShare(shortSeat, shortShare),
    );
  }

  function schedulePayoffs() {
    terms.priceAuthority
      .priceAtTime(terms.expiration, terms.underlyingAmount)
      .then(payoffOptions);
  }

const longShare = calculateLongShare(price);
reallocateToSeat(Position.LONG, longShare);
reallocateToSeat(Position.SHORT, inverse(longShare));
}

function schedulePayoffs() {
terms.priceAuthority
.priceAtTime(terms.expiration, terms.underlyingAmount)
.then(price => payoffOptions(price));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.then(price => payoffOptions(price));
.then(payoffOptions);

}

function makeOptionInvitation(dir) {
const optionsTerms = harden({
...terms,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want put the terms in the invitation properties if the user can get them by looking up the instance?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need.

position: dir,
});
// All we do at time of exercise is resolve the promise.
return zcf.makeInvitation(
seat => seatPromiseKits[dir].resolve(seat),
`collect ${dir} payout`,
optionsTerms,
);
}

async function makeOptionPair() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be async

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return {
longInvitation: makeOptionInvitation(Position.LONG),
shortInvitation: makeOptionInvitation(Position.SHORT),
};
}

async function makeInvitationToBuy() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async function makeInvitationToBuy() {
async function makeCreatorInvitation() {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const { longInvitation, shortInvitation } = await makeOptionPair();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to await this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not return the paymentKeywordRecord that you will use in line 178 instead of creating new names for the properties?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like:

const makeOptionPair = () =>
    harden({
      Long: makeOptionInvitation(Position.LONG),
      Short: makeOptionInvitation(Position.SHORT),
    });

  async function makeCreatorInvitation() {
    const optionPair = makeOptionPair();
    const invitationIssuer = zcf.getInvitationIssuer();
    const longAmount = await E(invitationIssuer).getAmountOf(optionPair.Long);
    const shortAmount = await E(invitationIssuer).getAmountOf(optionPair.Short);
    await depositToSeat(
      zcf,
      collateralSeat,
      { LongOption: longAmount, ShortOption: shortAmount },
      optionPair,
    );

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good suggestions.

const invitationIssuer = zcf.getInvitationIssuer();
const longAmount = await E(invitationIssuer).getAmountOf(longInvitation);
const shortAmount = await E(invitationIssuer).getAmountOf(shortInvitation);
depositToSeat(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should await this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Maybe we should have a naming convention?

zcf,
collateralSeat,
{ LongOption: longAmount, ShortOption: shortAmount },
{ LongOption: longInvitation, ShortOption: shortInvitation },
);

// transfer collateral from longSeat to collateralSeat, then return a pair
// of callSpread invitations
/** @type {OfferHandler} */
const pairBuyerPosition = longSeat => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few naming changes - this is the handler for the creator to produce the two invitations, right? So there is no purchase or buying going on. I think this should be named something else. Also, this is not the long position in the contract. It's the creatorSeat.

Suggested change
const pairBuyerPosition = longSeat => {
const produceInvitations = creatorSeat => {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep.

assertProposalShape(longSeat, {
give: { Collateral: null },
want: { LongOption: null, ShortOption: null },
});

katelynsills marked this conversation as resolved.
Show resolved Hide resolved
trade(
zcf,
{
seat: collateralSeat,
gains: { Collateral: terms.settlementAmount },
},
{
seat: longSeat,
gains: { LongOption: longAmount, ShortOption: shortAmount },
},
);
schedulePayoffs();
longSeat.exit();
};

const longTerms = harden({
...terms,
LongOption: longAmount,
ShortOption: shortAmount,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use capitalization in the customProperties. Can we call this

Suggested change
LongOption: longAmount,
ShortOption: shortAmount,
longAmount,
shortAmount,

instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

});
return zcf.makeInvitation(pairBuyerPosition, `call spread pair`, longTerms);
}

return harden({ creatorInvitation: makeInvitationToBuy() });
};

harden(start);
export { start };
Loading