@overcast-xyz/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +272 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +200 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.js +18 -0
- package/dist/commands/accept.d.ts +11 -0
- package/dist/commands/accept.js +112 -0
- package/dist/commands/assets.d.ts +9 -0
- package/dist/commands/assets.js +80 -0
- package/dist/commands/balance.d.ts +9 -0
- package/dist/commands/balance.js +74 -0
- package/dist/commands/deposit.d.ts +7 -0
- package/dist/commands/deposit.js +67 -0
- package/dist/commands/exercise.d.ts +14 -0
- package/dist/commands/exercise.js +88 -0
- package/dist/commands/key.d.ts +15 -0
- package/dist/commands/key.js +98 -0
- package/dist/commands/mm-webhook.d.ts +36 -0
- package/dist/commands/mm-webhook.js +152 -0
- package/dist/commands/mm.d.ts +37 -0
- package/dist/commands/mm.js +345 -0
- package/dist/commands/offer.d.ts +29 -0
- package/dist/commands/offer.js +149 -0
- package/dist/commands/option.d.ts +28 -0
- package/dist/commands/option.js +138 -0
- package/dist/commands/quote-webhook.d.ts +36 -0
- package/dist/commands/quote-webhook.js +152 -0
- package/dist/commands/quote.d.ts +37 -0
- package/dist/commands/quote.js +345 -0
- package/dist/commands/redeem.d.ts +14 -0
- package/dist/commands/redeem.js +88 -0
- package/dist/commands/rfq-view.d.ts +25 -0
- package/dist/commands/rfq-view.js +172 -0
- package/dist/commands/rfq.d.ts +4 -0
- package/dist/commands/rfq.js +280 -0
- package/dist/commands/shared.d.ts +81 -0
- package/dist/commands/shared.js +296 -0
- package/dist/commands/withdraw.d.ts +7 -0
- package/dist/commands/withdraw.js +67 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.js +58 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +50 -0
- package/package.json +38 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NewRfqPayload } from "@overcast/core";
|
|
2
|
+
import { GlobalFlags } from "../config";
|
|
3
|
+
/** A market maker's priced premium for an RFQ: asset address + native amount. */
|
|
4
|
+
export interface Premium {
|
|
5
|
+
premiumAsset: string;
|
|
6
|
+
premiumAmount: bigint;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Prices an incoming RFQ. Returns the premium to quote, or `null` to skip the
|
|
10
|
+
* RFQ. May be async so pricers can consult external data (e.g. a spot oracle).
|
|
11
|
+
*/
|
|
12
|
+
export type QuotePricer = (rfq: NewRfqPayload) => Promise<Premium | null>;
|
|
13
|
+
export interface MmOptions extends GlobalFlags {
|
|
14
|
+
/** MM edge added on top of the Black-Scholes fair value (fraction). */
|
|
15
|
+
spread?: string;
|
|
16
|
+
/** Annualized implied volatility, e.g. `0.8` for 80%. */
|
|
17
|
+
vol?: string;
|
|
18
|
+
/** Annualized risk-free rate, e.g. `0.05` for 5%. */
|
|
19
|
+
rate?: string;
|
|
20
|
+
/** Endpoint each RFQ event is POSTed to; its reply drives the quote. */
|
|
21
|
+
webhookUrl?: string;
|
|
22
|
+
/** Per-event webhook timeout, in milliseconds. */
|
|
23
|
+
webhookTimeout?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `overcast mm` — run a market maker that auto-quotes every incoming RFQ using
|
|
27
|
+
* a Black-Scholes price.
|
|
28
|
+
*
|
|
29
|
+
* A cash-secured put and a covered call are the same instrument — an option to
|
|
30
|
+
* swap `settlementAmount` of one asset for `collateralAmount` of the other —
|
|
31
|
+
* differing only in which leg is collateral. We identify the stablecoin (cash)
|
|
32
|
+
* leg and the volatile (underlying) leg, then price:
|
|
33
|
+
* - settlement is cash → collateral is the underlying → **covered call**
|
|
34
|
+
* - collateral is cash → settlement is the underlying → **cash-secured put**
|
|
35
|
+
* See {@link blackScholesPricer}.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runMarketMaker(opts: MmOptions): Promise<void>;
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.runMarketMaker = runMarketMaker;
|
|
40
|
+
const p = __importStar(require("@clack/prompts"));
|
|
41
|
+
const core_1 = require("@overcast/core");
|
|
42
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
43
|
+
const client_1 = require("../client");
|
|
44
|
+
const config_1 = require("../config");
|
|
45
|
+
const mm_webhook_1 = require("./mm-webhook");
|
|
46
|
+
/** MM edge as a fraction of fair value (2%). */
|
|
47
|
+
const DEFAULT_SPREAD = 0.02;
|
|
48
|
+
/** Annualized implied volatility used for every quote (80%). */
|
|
49
|
+
const DEFAULT_VOL = 0.8;
|
|
50
|
+
/** Annualized risk-free rate (crypto convention: assume 0). */
|
|
51
|
+
const DEFAULT_RATE = 0;
|
|
52
|
+
/** Per-event webhook timeout when `--webhook-timeout` is omitted. */
|
|
53
|
+
const DEFAULT_WEBHOOK_TIMEOUT_MS = 5000;
|
|
54
|
+
const SECONDS_PER_YEAR = 365 * 24 * 60 * 60;
|
|
55
|
+
/**
|
|
56
|
+
* Symbols treated as the cash / numeraire leg. A quote needs exactly one cash
|
|
57
|
+
* leg and one volatile-underlying leg; the cash side is assumed to be ~$1.
|
|
58
|
+
*/
|
|
59
|
+
const STABLE_SYMBOLS = new Set([
|
|
60
|
+
"USDC",
|
|
61
|
+
"USDT",
|
|
62
|
+
"USDH",
|
|
63
|
+
"PYUSD",
|
|
64
|
+
"DAI",
|
|
65
|
+
"USDP",
|
|
66
|
+
"TUSD",
|
|
67
|
+
"USD",
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* `overcast mm` — run a market maker that auto-quotes every incoming RFQ using
|
|
71
|
+
* a Black-Scholes price.
|
|
72
|
+
*
|
|
73
|
+
* A cash-secured put and a covered call are the same instrument — an option to
|
|
74
|
+
* swap `settlementAmount` of one asset for `collateralAmount` of the other —
|
|
75
|
+
* differing only in which leg is collateral. We identify the stablecoin (cash)
|
|
76
|
+
* leg and the volatile (underlying) leg, then price:
|
|
77
|
+
* - settlement is cash → collateral is the underlying → **covered call**
|
|
78
|
+
* - collateral is cash → settlement is the underlying → **cash-secured put**
|
|
79
|
+
* See {@link blackScholesPricer}.
|
|
80
|
+
*/
|
|
81
|
+
async function runMarketMaker(opts) {
|
|
82
|
+
const config = (0, config_1.resolveConfig)(opts);
|
|
83
|
+
const app = await (0, client_1.createClient)(config);
|
|
84
|
+
const spread = opts.spread ? Number(opts.spread) : DEFAULT_SPREAD;
|
|
85
|
+
const vol = opts.vol ? Number(opts.vol) : DEFAULT_VOL;
|
|
86
|
+
const rate = opts.rate ? Number(opts.rate) : DEFAULT_RATE;
|
|
87
|
+
const rfq = app.rfq;
|
|
88
|
+
// Webhook mode: delegate pricing (and forward every lifecycle event) to a
|
|
89
|
+
// developer-supplied local endpoint. Falls back to the built-in Black-Scholes
|
|
90
|
+
// pricer when no `--webhook-url` (or `OVERCAST_MM_WEBHOOK_URL`) is set.
|
|
91
|
+
const webhookUrl = opts.webhookUrl ?? process.env.OVERCAST_MM_WEBHOOK_URL;
|
|
92
|
+
const webhook = webhookUrl
|
|
93
|
+
? {
|
|
94
|
+
url: webhookUrl,
|
|
95
|
+
timeoutMs: Number(opts.webhookTimeout) || DEFAULT_WEBHOOK_TIMEOUT_MS,
|
|
96
|
+
}
|
|
97
|
+
: null;
|
|
98
|
+
const pricer = webhook
|
|
99
|
+
? (0, mm_webhook_1.webhookPricer)(webhook, app.publicKey)
|
|
100
|
+
: blackScholesPricer({ spread, vol, rate });
|
|
101
|
+
p.intro(picocolors_1.default.magenta("Overcast Market Maker"));
|
|
102
|
+
// The market maker takes the option (pays the premium), so it acts on the
|
|
103
|
+
// TAKER side — the opposite of the RFQ creator's MAKER side.
|
|
104
|
+
await rfq.registerRole("TAKER");
|
|
105
|
+
p.log.info(`Maker ${picocolors_1.default.bold(app.publicKey)}`);
|
|
106
|
+
p.log.info(picocolors_1.default.dim(webhook
|
|
107
|
+
? `Webhook — ${webhook.url} (timeout ${webhook.timeoutMs}ms)`
|
|
108
|
+
: `Black-Scholes — vol ${vol * 100}%, rate ${rate * 100}%, edge ${spread * 100}%`));
|
|
109
|
+
p.log.info(picocolors_1.default.dim("Listening for RFQs. Press Ctrl-C to stop."));
|
|
110
|
+
rfq.onQuoteAccepted = (a) => {
|
|
111
|
+
p.log.success(`Quote ${short(a.quoteId)} accepted — option ${picocolors_1.default.bold(a.optionId)} (RFQ ${short(a.rfqId)})`);
|
|
112
|
+
if (webhook)
|
|
113
|
+
(0, mm_webhook_1.postEvent)(webhook, "quoteAccepted", a, app.publicKey);
|
|
114
|
+
};
|
|
115
|
+
rfq.onError = (e) => {
|
|
116
|
+
p.log.error(`RFQ error: ${e.message}`);
|
|
117
|
+
if (webhook)
|
|
118
|
+
(0, mm_webhook_1.postEvent)(webhook, "error", e, app.publicKey);
|
|
119
|
+
};
|
|
120
|
+
// In webhook mode, forward the remaining lifecycle events too so the endpoint
|
|
121
|
+
// has full visibility (these are fire-and-forget — their replies are ignored).
|
|
122
|
+
if (webhook) {
|
|
123
|
+
rfq.onNewQuote = (q) => (0, mm_webhook_1.postEvent)(webhook, "newQuote", q, app.publicKey);
|
|
124
|
+
rfq.onRfqCancelled = (c) => (0, mm_webhook_1.postEvent)(webhook, "rfqCancelled", c, app.publicKey);
|
|
125
|
+
rfq.onRfqExpired = (e) => (0, mm_webhook_1.postEvent)(webhook, "rfqExpired", e, app.publicKey);
|
|
126
|
+
}
|
|
127
|
+
// Auto-quote every incoming RFQ (Black-Scholes, or the webhook's reply).
|
|
128
|
+
rfq.onNewRfq = (incoming) => {
|
|
129
|
+
void quoteRfq(rfq, app.publicKey, incoming, pricer);
|
|
130
|
+
};
|
|
131
|
+
// Run until interrupted.
|
|
132
|
+
await new Promise((resolve) => {
|
|
133
|
+
process.on("SIGINT", () => {
|
|
134
|
+
p.log.warn("Shutting down…");
|
|
135
|
+
rfq.disconnect();
|
|
136
|
+
resolve();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
p.outro(picocolors_1.default.magenta("Market maker stopped."));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Price an incoming RFQ and submit a quote over the full option terms — the
|
|
143
|
+
* RFQ's legs completed with the maker's premium — on the TAKER side. The client
|
|
144
|
+
* authors the settlement offer and signs it with the protocol signer, so no
|
|
145
|
+
* signature is passed here.
|
|
146
|
+
*/
|
|
147
|
+
async function quoteRfq(rfq, creator, incoming, pricer) {
|
|
148
|
+
try {
|
|
149
|
+
const premium = await pricer(incoming);
|
|
150
|
+
if (!premium)
|
|
151
|
+
return; // the pricer already logged why it skipped
|
|
152
|
+
const details = (0, core_1.completeOptionDetails)(incoming.details, premium);
|
|
153
|
+
if (!details) {
|
|
154
|
+
skip(incoming.rfqId, "RFQ is missing option terms");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const { quoteId } = await rfq.submitQuote({
|
|
158
|
+
rfqId: incoming.rfqId,
|
|
159
|
+
details,
|
|
160
|
+
creator,
|
|
161
|
+
side: "TAKER",
|
|
162
|
+
});
|
|
163
|
+
p.log.success(`Quoted ${premium.premiumAmount} ${short(premium.premiumAsset)} on RFQ ${short(incoming.rfqId)} (quote ${short(quoteId)})`);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
p.log.error(`Failed to quote RFQ ${short(incoming.rfqId)}: ${err.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Black-Scholes pricer. Maps an RFQ onto European-option terms and quotes the
|
|
171
|
+
* fair premium plus a `spread` edge, denominated in the cash (stablecoin) leg.
|
|
172
|
+
*
|
|
173
|
+
* The contract lets the holder swap the underlying for cash at the implied
|
|
174
|
+
* strike `K = cashAmount / underlyingAmount` (USD per underlying unit), with
|
|
175
|
+
* `underlyingAmount` as the contract size.
|
|
176
|
+
*
|
|
177
|
+
* Spot comes from a live CoinGecko lookup, falling back to the asset's
|
|
178
|
+
* backend-supplied {@link Asset.lastKnownPriceUsd} (the only source for RWAs,
|
|
179
|
+
* which have no `coinGeckoId`). Volatility uses the asset's own
|
|
180
|
+
* {@link Asset.volatility} when present, else the configured default. Returns
|
|
181
|
+
* `null` (skips the RFQ) when the legs can't be priced — no single cash leg, no
|
|
182
|
+
* usable spot price, or the option has already expired.
|
|
183
|
+
*/
|
|
184
|
+
function blackScholesPricer(params) {
|
|
185
|
+
const { spread, vol, rate } = params;
|
|
186
|
+
return async (rfq) => {
|
|
187
|
+
const Sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
188
|
+
await Sleep(Math.random() * 1000);
|
|
189
|
+
// `d` is the curated (wire) shape — assets resolved, amounts as decimal
|
|
190
|
+
// strings — but only *partially* filled: the RFQ may omit some terms. We can
|
|
191
|
+
// only price a fully-specified pair of legs, so bail otherwise.
|
|
192
|
+
const d = rfq.details;
|
|
193
|
+
if (d.endTimestamp == null ||
|
|
194
|
+
!d.collateralAsset ||
|
|
195
|
+
d.collateralAmount == null ||
|
|
196
|
+
!d.settlementAsset ||
|
|
197
|
+
d.settlementAmount == null) {
|
|
198
|
+
skip(rfq.rfqId, "RFQ does not specify both option legs");
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
// Capture the now-narrowed terms before any further `await` widens them back.
|
|
202
|
+
const endTimestamp = d.endTimestamp;
|
|
203
|
+
const collateral = {
|
|
204
|
+
asset: d.collateralAsset,
|
|
205
|
+
amount: (0, core_1.toDisplayAmount)((0, core_1.amountFromWire)(d.collateralAmount), d.collateralAsset),
|
|
206
|
+
};
|
|
207
|
+
const settlement = {
|
|
208
|
+
asset: d.settlementAsset,
|
|
209
|
+
amount: (0, core_1.toDisplayAmount)((0, core_1.amountFromWire)(d.settlementAmount), d.settlementAsset),
|
|
210
|
+
};
|
|
211
|
+
// Need exactly one cash leg and one volatile leg to define an option.
|
|
212
|
+
const collateralIsCash = isStable(collateral.asset);
|
|
213
|
+
const settlementIsCash = isStable(settlement.asset);
|
|
214
|
+
if (collateralIsCash === settlementIsCash) {
|
|
215
|
+
skip(rfq.rfqId, `need exactly one stablecoin leg (got ${symbol(collateral.asset)}/${symbol(settlement.asset)})`);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
// settlement = cash → covered call; collateral = cash → cash-secured put.
|
|
219
|
+
const isCall = settlementIsCash;
|
|
220
|
+
const cash = isCall ? settlement : collateral;
|
|
221
|
+
const underlying = isCall ? collateral : settlement;
|
|
222
|
+
// Prefer a live CoinGecko spot, falling back to the backend-supplied last
|
|
223
|
+
// known price. For RWAs (no `coinGeckoId`) the last known price is the only
|
|
224
|
+
// source — they're never auto-refreshed against a live feed.
|
|
225
|
+
const liveSpot = underlying.asset.coinGeckoId
|
|
226
|
+
? await fetchSpotUsd(underlying.asset.coinGeckoId)
|
|
227
|
+
: null;
|
|
228
|
+
const spot = liveSpot ?? underlying.asset.lastKnownPriceUsd ?? null;
|
|
229
|
+
if (spot == null) {
|
|
230
|
+
skip(rfq.rfqId, `no spot or last known price for ${symbol(underlying.asset)}`);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
// Use the asset's own volatility when the backend provides one; otherwise
|
|
234
|
+
// fall back to the configured default.
|
|
235
|
+
const sigma = underlying.asset.volatility ?? vol;
|
|
236
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
237
|
+
const years = (endTimestamp - nowSec) / SECONDS_PER_YEAR;
|
|
238
|
+
if (years <= 0) {
|
|
239
|
+
skip(rfq.rfqId, "option has already expired");
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const strike = Number(cash.amount) / Number(underlying.amount); // USD per underlying unit
|
|
243
|
+
const perUnit = blackScholes(isCall, spot, strike, years, rate, sigma);
|
|
244
|
+
const fairCash = perUnit * Number(underlying.amount); // total premium in cash units
|
|
245
|
+
const quotedCash = fairCash * (1 + spread) * (Math.random() * 0.05 + 0.95); // add the MM's edge
|
|
246
|
+
// Pricing is done in floats (display units); the quoted premium becomes an
|
|
247
|
+
// exact native `bigint`. The backend requires a positive premium, so clamp
|
|
248
|
+
// to at least one native unit.
|
|
249
|
+
const decimals = cash.asset.decimals ?? 0;
|
|
250
|
+
const native = (0, core_1.parseUnits)(quotedCash.toFixed(decimals), decimals);
|
|
251
|
+
const premiumAmount = native < 1n ? 1n : native;
|
|
252
|
+
return { premiumAsset: cash.asset.address, premiumAmount };
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Black-Scholes price of one European option, in the same currency as `S`/`K`.
|
|
257
|
+
*
|
|
258
|
+
* @param isCall true for a call, false for a put
|
|
259
|
+
* @param S spot price of the underlying
|
|
260
|
+
* @param K strike price
|
|
261
|
+
* @param T time to expiry, in years
|
|
262
|
+
* @param r annualized risk-free rate
|
|
263
|
+
* @param sigma annualized volatility
|
|
264
|
+
*/
|
|
265
|
+
function blackScholes(isCall, S, K, T, r, sigma) {
|
|
266
|
+
// Degenerate inputs: the option is worth its (undiscounted) intrinsic value.
|
|
267
|
+
if (sigma <= 0 || T <= 0) {
|
|
268
|
+
return Math.max(0, isCall ? S - K : K - S);
|
|
269
|
+
}
|
|
270
|
+
const sqrtT = Math.sqrt(T);
|
|
271
|
+
const d1 = (Math.log(S / K) + (r + (sigma * sigma) / 2) * T) / (sigma * sqrtT);
|
|
272
|
+
const d2 = d1 - sigma * sqrtT;
|
|
273
|
+
const disc = Math.exp(-r * T);
|
|
274
|
+
return isCall
|
|
275
|
+
? S * normCdf(d1) - K * disc * normCdf(d2)
|
|
276
|
+
: K * disc * normCdf(-d2) - S * normCdf(-d1);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Standard normal CDF via the Zelen & Severo rational approximation
|
|
280
|
+
* (Abramowitz & Stegun 26.2.17), accurate to ~7.5e-8.
|
|
281
|
+
*/
|
|
282
|
+
function normCdf(x) {
|
|
283
|
+
if (x < 0)
|
|
284
|
+
return 1 - normCdf(-x);
|
|
285
|
+
const t = 1 / (1 + 0.2316419 * x);
|
|
286
|
+
const poly = t *
|
|
287
|
+
(0.31938153 +
|
|
288
|
+
t *
|
|
289
|
+
(-0.356563782 +
|
|
290
|
+
t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
|
|
291
|
+
const pdf = 0.3989422804014327 * Math.exp((-x * x) / 2);
|
|
292
|
+
return 1 - pdf * poly;
|
|
293
|
+
}
|
|
294
|
+
// --- spot prices -------------------------------------------------------------
|
|
295
|
+
/** Cached CoinGecko USD prices, refreshed on a short TTL to avoid rate limits. */
|
|
296
|
+
const SPOT_TTL_MS = 60_000;
|
|
297
|
+
const spotCache = new Map();
|
|
298
|
+
/**
|
|
299
|
+
* Fetch the USD spot price for a CoinGecko id, served from a short-lived cache.
|
|
300
|
+
* Returns `null` if the lookup fails (network error or unknown id) so the
|
|
301
|
+
* pricer can skip the RFQ rather than quote on stale or missing data.
|
|
302
|
+
*/
|
|
303
|
+
async function fetchSpotUsd(coinGeckoId) {
|
|
304
|
+
const cached = spotCache.get(coinGeckoId);
|
|
305
|
+
if (cached && Date.now() - cached.at < SPOT_TTL_MS)
|
|
306
|
+
return cached.price;
|
|
307
|
+
try {
|
|
308
|
+
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(coinGeckoId)}&vs_currencies=usd`;
|
|
309
|
+
const res = await fetch(url);
|
|
310
|
+
if (!res.ok)
|
|
311
|
+
return null;
|
|
312
|
+
const body = (await res.json());
|
|
313
|
+
const price = body[coinGeckoId]?.usd;
|
|
314
|
+
if (typeof price !== "number" || !(price > 0))
|
|
315
|
+
return null;
|
|
316
|
+
spotCache.set(coinGeckoId, { price, at: Date.now() });
|
|
317
|
+
return price;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// --- helpers -----------------------------------------------------------------
|
|
324
|
+
/**
|
|
325
|
+
* Whether an asset is treated as the cash / numeraire leg. The backend's own
|
|
326
|
+
* {@link Asset.cashLike} classification wins when present; otherwise fall back to
|
|
327
|
+
* the stablecoin-symbol heuristic for assets that predate the flag.
|
|
328
|
+
*/
|
|
329
|
+
function isStable(asset) {
|
|
330
|
+
if (asset.cashLike != null)
|
|
331
|
+
return asset.cashLike;
|
|
332
|
+
return asset.symbol ? STABLE_SYMBOLS.has(asset.symbol.toUpperCase()) : false;
|
|
333
|
+
}
|
|
334
|
+
/** A display label for an asset (symbol, falling back to a short address). */
|
|
335
|
+
function symbol(asset) {
|
|
336
|
+
return asset.symbol ?? short(asset.address);
|
|
337
|
+
}
|
|
338
|
+
/** Log why an RFQ was skipped without quoting. */
|
|
339
|
+
function skip(rfqId, reason) {
|
|
340
|
+
p.log.warn(`Skipping RFQ ${short(rfqId)}: ${reason}`);
|
|
341
|
+
}
|
|
342
|
+
/** Shorten a base58 id for log lines. */
|
|
343
|
+
function short(id) {
|
|
344
|
+
return id.length <= 12 ? id : `${id.slice(0, 6)}…${id.slice(-4)}`;
|
|
345
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { GlobalFlags } from "../config";
|
|
2
|
+
export interface OfferShowOptions extends GlobalFlags {
|
|
3
|
+
/** Offer id to look up; prompts when omitted. */
|
|
4
|
+
id?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `overcast offer show` — look up an open offer by its content-address id.
|
|
8
|
+
*
|
|
9
|
+
* Collateral (maker) and settlement (taker) offers now share a single
|
|
10
|
+
* content-addressed namespace; the offer's own `side` says which it is.
|
|
11
|
+
*/
|
|
12
|
+
export declare function offerShow(opts: OfferShowOptions): Promise<void>;
|
|
13
|
+
export interface OfferListOptions extends GlobalFlags {
|
|
14
|
+
/** Which side to list: `collateral` (maker), `settlement` (taker), or `both` (default). */
|
|
15
|
+
kind?: string;
|
|
16
|
+
/** Filter to the offer's counterparty (its creator). */
|
|
17
|
+
party?: string;
|
|
18
|
+
/** Filter to offers touching this asset. */
|
|
19
|
+
asset?: string;
|
|
20
|
+
/** Max rows. */
|
|
21
|
+
limit?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `overcast offer list` — browse open offers via the off-chain
|
|
25
|
+
* {@link OvercastView}. Read-only. Offers now live in a single namespace;
|
|
26
|
+
* `--kind` narrows the listing to one side (by the offer's `side`), and with
|
|
27
|
+
* none given both sides are shown.
|
|
28
|
+
*/
|
|
29
|
+
export declare function offerList(opts: OfferListOptions): Promise<void>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.offerShow = offerShow;
|
|
40
|
+
exports.offerList = offerList;
|
|
41
|
+
const p = __importStar(require("@clack/prompts"));
|
|
42
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
43
|
+
const client_1 = require("../client");
|
|
44
|
+
const config_1 = require("../config");
|
|
45
|
+
const shared_1 = require("./shared");
|
|
46
|
+
/**
|
|
47
|
+
* `overcast offer show` — look up an open offer by its content-address id.
|
|
48
|
+
*
|
|
49
|
+
* Collateral (maker) and settlement (taker) offers now share a single
|
|
50
|
+
* content-addressed namespace; the offer's own `side` says which it is.
|
|
51
|
+
*/
|
|
52
|
+
async function offerShow(opts) {
|
|
53
|
+
const config = (0, config_1.resolveConfig)(opts, { keypairOptional: true });
|
|
54
|
+
const app = await (0, client_1.createClient)(config);
|
|
55
|
+
const registry = await (0, shared_1.loadRegistry)(app.api);
|
|
56
|
+
if (opts.id && (0, shared_1.validateAddress)(opts.id)) {
|
|
57
|
+
throw new Error(`invalid offer id: ${opts.id}`);
|
|
58
|
+
}
|
|
59
|
+
const id = opts.id ?? (await (0, shared_1.promptId)("Offer id"));
|
|
60
|
+
p.intro(picocolors_1.default.cyan("Overcast offer"));
|
|
61
|
+
const spinner = p.spinner();
|
|
62
|
+
spinner.start("Looking up offer");
|
|
63
|
+
try {
|
|
64
|
+
const offer = await app.chain.getOffer(id);
|
|
65
|
+
if (!offer) {
|
|
66
|
+
spinner.stop(picocolors_1.default.yellow(`No open offer found for ${(0, shared_1.short)(id)}.`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
spinner.stop(`${sideLabel(offer.side)} offer ${(0, shared_1.short)(id)}`);
|
|
70
|
+
p.note([
|
|
71
|
+
`Creator ${(0, shared_1.short)(offer.creator)}`,
|
|
72
|
+
"",
|
|
73
|
+
(0, shared_1.describeOptionDetails)(offer.details, registry),
|
|
74
|
+
].join("\n"), `${sideLabel(offer.side)} offer`);
|
|
75
|
+
p.outro(picocolors_1.default.green("Done."));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
spinner.stop(picocolors_1.default.red(`Lookup failed: ${err.message}`));
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `overcast offer list` — browse open offers via the off-chain
|
|
84
|
+
* {@link OvercastView}. Read-only. Offers now live in a single namespace;
|
|
85
|
+
* `--kind` narrows the listing to one side (by the offer's `side`), and with
|
|
86
|
+
* none given both sides are shown.
|
|
87
|
+
*/
|
|
88
|
+
async function offerList(opts) {
|
|
89
|
+
const kind = parseKind(opts.kind);
|
|
90
|
+
const config = (0, config_1.resolveConfig)(opts, { keypairOptional: true });
|
|
91
|
+
const app = await (0, client_1.createClient)(config);
|
|
92
|
+
const query = {
|
|
93
|
+
filter: {
|
|
94
|
+
party: (0, shared_1.optionalAddress)("--party", opts.party),
|
|
95
|
+
asset: (0, shared_1.optionalAddress)("--asset", opts.asset),
|
|
96
|
+
},
|
|
97
|
+
page: opts.limit ? { limit: (0, shared_1.parseLimit)(opts.limit) } : undefined,
|
|
98
|
+
};
|
|
99
|
+
p.intro(picocolors_1.default.cyan("Overcast offers"));
|
|
100
|
+
const spinner = p.spinner();
|
|
101
|
+
spinner.start("Loading offers");
|
|
102
|
+
try {
|
|
103
|
+
const { items } = await app.api.listOffers(query);
|
|
104
|
+
// A single namespace has no per-side query filter, so narrow by the offer's
|
|
105
|
+
// own `side` here when `--kind` asks for one.
|
|
106
|
+
const offers = kind === "both" ? items : items.filter((o) => o.side === kindSide(kind));
|
|
107
|
+
spinner.stop("Offers loaded.");
|
|
108
|
+
if (!renderOffers(offers)) {
|
|
109
|
+
p.outro(picocolors_1.default.yellow("No offers match."));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
p.outro(picocolors_1.default.green("Done."));
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
spinner.stop(picocolors_1.default.red(`List failed: ${err.message}`));
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Print the offers, one per line with side + creator; returns whether any were shown. */
|
|
120
|
+
function renderOffers(offers) {
|
|
121
|
+
if (offers.length === 0) {
|
|
122
|
+
p.log.info(`${picocolors_1.default.bold("Offers")} ${picocolors_1.default.dim("— none")}`);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
p.log.info(picocolors_1.default.bold(`Offers (${offers.length})`));
|
|
126
|
+
for (const offer of offers) {
|
|
127
|
+
p.log.message([
|
|
128
|
+
`${picocolors_1.default.bold((0, shared_1.short)(offer.id))} ${(0, shared_1.summarizeCuratedDetails)(offer.details)}`,
|
|
129
|
+
picocolors_1.default.dim(`${sideLabel(offer.side).toLowerCase()} ${(0, shared_1.short)(offer.creator)}`),
|
|
130
|
+
].join("\n"));
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/** Human-readable name for an offer's side. */
|
|
135
|
+
function sideLabel(side) {
|
|
136
|
+
return side === "MAKER" ? "Collateral" : "Settlement";
|
|
137
|
+
}
|
|
138
|
+
/** The {@link OptionSide} a non-`both` `--kind` selects. */
|
|
139
|
+
function kindSide(kind) {
|
|
140
|
+
return kind === "collateral" ? "MAKER" : "TAKER";
|
|
141
|
+
}
|
|
142
|
+
/** Validate the `--kind` flag, defaulting to listing both sides. */
|
|
143
|
+
function parseKind(value) {
|
|
144
|
+
const kind = (value ?? "both").toLowerCase();
|
|
145
|
+
if (kind !== "collateral" && kind !== "settlement" && kind !== "both") {
|
|
146
|
+
throw new Error(`invalid --kind: ${value} (expected collateral, settlement, or both)`);
|
|
147
|
+
}
|
|
148
|
+
return kind;
|
|
149
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GlobalFlags } from "../config";
|
|
2
|
+
export interface OptionShowOptions extends GlobalFlags {
|
|
3
|
+
collateralOffer?: string;
|
|
4
|
+
settlementOffer?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `overcast option show` — resolve a market option from its
|
|
8
|
+
* `(collateralOffer, settlementOffer)` id pair and print its terms alongside
|
|
9
|
+
* the live claim state: the outstanding exercise / redeem claim supplies and,
|
|
10
|
+
* when a wallet is supplied, that wallet's own holdings of each.
|
|
11
|
+
*/
|
|
12
|
+
export declare function optionShow(opts: OptionShowOptions): Promise<void>;
|
|
13
|
+
export interface OptionListOptions extends GlobalFlags {
|
|
14
|
+
/** Filter to options where this address is the maker. */
|
|
15
|
+
maker?: string;
|
|
16
|
+
/** Filter to options where this address is the taker. */
|
|
17
|
+
taker?: string;
|
|
18
|
+
/** Filter to options touching this asset (premium, collateral, or settlement). */
|
|
19
|
+
asset?: string;
|
|
20
|
+
/** Max rows to return. */
|
|
21
|
+
limit?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `overcast option list` — browse the backend's curated market options via the
|
|
25
|
+
* off-chain {@link OvercastView}. Read-only: no wallet required. Filters map
|
|
26
|
+
* straight onto the view's {@link OptionFilter}; results are newest-first.
|
|
27
|
+
*/
|
|
28
|
+
export declare function optionList(opts: OptionListOptions): Promise<void>;
|