-
Notifications
You must be signed in to change notification settings - Fork 212
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: a call spread option contract and tests.
Implementation of a fully collateralized call spread option, following Joe Clark's description. This is a combination of a bought call option and a sold call option at a higher strike price. The contracts are sold in pairs, and the buyers of the two positions together invest the entire amount that will be paid out. This option is settled financially. Neither party is expected to have ownership of the underlying asset at the start, and neither expects to take delivery at closing. zoe.startInstance() takes an issuerKeywordRecord that specifies the issuers for the keywords Underlying, Strike, and Collateral. The payout uses Collateral. The price oracle quotes the value of the Underlying in the same units as the Strike prices. creatorFacet has a method makeInvitationPair(), that takes terms that specifies { expiration, underlyingAmount, priceAuthority, strikePrice1, strikePrice2, settlementAmount, buyPercent }. ownerFacet.makeInvitationPair() returns two invitations, which can be sold separately. They settle when the priceAuthority announces the settlement amout as of it's pre-programmed closing time. closes: #1829
- Loading branch information
1 parent
224b39a
commit a99f2c7
Showing
3 changed files
with
888 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
// @ts-check | ||
import '../../exported'; | ||
|
||
import { assert, details } from '@agoric/assert'; | ||
|
||
// Eventually will be importable from '@agoric/zoe-contract-support' | ||
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 | ||
* 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 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 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. | ||
* | ||
* creatorInvitation has terms that include the amounts of the two options as | ||
* longOption and shortOption. When the creatorInvitation is exercised, the | ||
* payout includes the two option positions, which are themselves invitations | ||
* which can be exercised for free, and are valuable for their 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.) | ||
* + exit the contract when both seats have been paid. | ||
* | ||
* @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`, | ||
); | ||
|
||
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 | ||
// resolve the payments when those promises resolve. | ||
const seatPromiseKits = {}; | ||
|
||
seatPromiseKits[Position.LONG] = makePromiseKit(); | ||
seatPromiseKits[Position.SHORT] = makePromiseKit(); | ||
|
||
function reallocateToSeat(position, sharePercent) { | ||
seatPromiseKits[position].promise.then(seat => { | ||
const currentCollateral = collateralSeat.getCurrentAllocation() | ||
.Collateral; | ||
const collateral = terms.settlementAmount; | ||
const collateralShare = floorDivide( | ||
multiply(collateral.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(); | ||
}); | ||
} | ||
|
||
function calculateLongShare(price) { | ||
// longShare is the value of the underlying at close of the strikePrice | ||
// percentage (base:100) computed from strikePrice | ||
// scale that will be used to calculate the portion of collateral | ||
// allocated to each party. | ||
|
||
if (strikeMath.isGTE(terms.strikePrice1, price)) { | ||
return 0; | ||
} else if (strikeMath.isGTE(price, terms.strikePrice2)) { | ||
return 100; | ||
} | ||
|
||
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. | ||
const longShare = calculateLongShare(price); | ||
reallocateToSeat(Position.LONG, longShare, terms); | ||
reallocateToSeat(Position.SHORT, inverse(longShare), terms); | ||
} | ||
|
||
function schedulePayoffs() { | ||
terms.priceAuthority | ||
.priceAtTime(terms.expiration, terms.underlyingAmount) | ||
.then(price => payoffOptions(price)); | ||
} | ||
|
||
function makeOptionInvitation(dir) { | ||
const optionsTerms = harden({ | ||
...terms, | ||
position: dir, | ||
}); | ||
return zcf.makeInvitation( | ||
seat => seatPromiseKits[dir].resolve(seat), | ||
`collect ${dir} payout`, | ||
optionsTerms, | ||
); | ||
} | ||
|
||
async function makeOptionPair() { | ||
return { | ||
longInvitation: makeOptionInvitation(Position.LONG), | ||
shortInvitation: makeOptionInvitation(Position.SHORT), | ||
}; | ||
} | ||
|
||
async function makeInvitationToBuy() { | ||
const { longInvitation, shortInvitation } = await makeOptionPair(); | ||
const invitationIssuer = zcf.getInvitationIssuer(); | ||
const longAmount = await E(invitationIssuer).getAmountOf(longInvitation); | ||
const shortAmount = await E(invitationIssuer).getAmountOf(shortInvitation); | ||
depositToSeat( | ||
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 => { | ||
assertProposalShape(longSeat, { | ||
give: { Collateral: null }, | ||
want: { LongOption: null, ShortOption: null }, | ||
}); | ||
|
||
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, | ||
}); | ||
return zcf.makeInvitation(pairBuyerPosition, `call spread pair`, longTerms); | ||
} | ||
|
||
return harden({ creatorInvitation: makeInvitationToBuy() }); | ||
}; | ||
|
||
harden(start); | ||
export { start }; |
Oops, something went wrong.