-
Notifications
You must be signed in to change notification settings - Fork 217
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
Changes from 1 commit
3bc0b70
a0dd761
f900449
779b33b
b92306b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||||||||||
* option sold at a higher price. The contracts are sold in pairs, and the | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I don't think we want to call the invitations contracts. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we mean There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, what makes this difficult? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you do my rewrite of 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');
}
}; There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`, | ||||||||||
); | ||||||||||
|
||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh! good idea. we have |
||||||||||
// 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you make a comment of the formula in mathematical notation? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a common function or did we come up with this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's correct. |
||||||||||
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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, if we are doing the reallocations only after the seat resolves ( There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
} | ||||||||||
|
||||||||||
function makeOptionInvitation(dir) { | ||||||||||
const optionsTerms = harden({ | ||||||||||
...terms, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||||||||||
const { longInvitation, shortInvitation } = await makeOptionPair(); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,
); There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should await this. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 => { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.