everyblock-mcp 0.2.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/LICENSE.md +75 -0
- package/README.md +92 -0
- package/dist/abi.js +1720 -0
- package/dist/chain.js +202 -0
- package/dist/main.js +18 -0
- package/dist/orders.js +178 -0
- package/dist/server.js +638 -0
- package/package.json +49 -0
package/dist/chain.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Connection, configuration and the read/write plumbing the tools share. Secrets are read from the environment,
|
|
2
|
+
// never printed, logged or written anywhere.
|
|
3
|
+
import { BaseError, ContractFunctionRevertedError, createPublicClient, createWalletClient, defineChain, http, parseAbi, parseEventLogs, parseUnits, } from "viem";
|
|
4
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
5
|
+
import { everyblockAbi, testUsdAbi } from "./abi.js";
|
|
6
|
+
export const FEED_NAMES = {
|
|
7
|
+
"0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43": "BTC/USD",
|
|
8
|
+
"0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace": "ETH/USD",
|
|
9
|
+
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d": "SOL/USD",
|
|
10
|
+
"0x31491744e2dbf6df7fcf4ac0820d18a609b49076d45066d3568424e62f686cd1": "MON/USD",
|
|
11
|
+
};
|
|
12
|
+
export const DEFAULT_FEEDS = Object.keys(FEED_NAMES);
|
|
13
|
+
export const MONAD_MAINNET = 143;
|
|
14
|
+
export const MONAD_TESTNET = 10143;
|
|
15
|
+
/** The live Monad testnet deployment (docs/SUBMISSION.md, "Deployed contracts"). */
|
|
16
|
+
export const TESTNET_RPC = "https://testnet-rpc.monad.xyz";
|
|
17
|
+
export const TESTNET_EVERYBLOCK = "0x034ebcA29FcAdDF0F434Dd63C75e719b1dC62dB5";
|
|
18
|
+
/** The hosted web app's proof endpoint for the testnet deployment. */
|
|
19
|
+
export const TESTNET_PROOF_URL = "https://everyblock-gray.vercel.app/api/proof";
|
|
20
|
+
export const ANVIL = 31337;
|
|
21
|
+
/** Monad charges the gas limit, not gas used: estimate, then add 15% (as the keeper does). */
|
|
22
|
+
const GAS_MARGIN_PCT = 115n;
|
|
23
|
+
/** The fee function another stream adds; absent on older deployments, which then charge no fee. */
|
|
24
|
+
const feeAbi = parseAbi(["function marketFeeBps(uint256 marketId) view returns (uint256)"]);
|
|
25
|
+
export const isLocalRpc = (url) => /^https?:\/\/(127\.0\.0\.1|localhost)(:|\/|$)/.test(url);
|
|
26
|
+
/**
|
|
27
|
+
* Environment:
|
|
28
|
+
* RPC_URL, EVERYBLOCK the chain and contract (default: both unset = the Monad testnet deployment)
|
|
29
|
+
* MCP_PRIVATE_KEY the trading wallet's key (writes); never printed
|
|
30
|
+
* MCP_UNLOCKED_ACCOUNT instead of a key, an unlocked node account (localhost only, e.g. anvil)
|
|
31
|
+
* MCP_ALLOWED_CHAINS chain ids writes may run on (default 10143,31337: Monad testnet and anvil)
|
|
32
|
+
* MCP_MAX_ORDER_SIZE shares per order (default 100)
|
|
33
|
+
* MCP_MAX_SPEND_USD collateral one write call may commit, in dollars (default 50)
|
|
34
|
+
* FEEDS comma-separated Pyth feed ids (default BTC, ETH, SOL, MON)
|
|
35
|
+
* MCP_LOG_RANGE, MCP_LOG_LOOKBACK blocks per eth_getLogs call (100) and how far back to look (2000)
|
|
36
|
+
* EVERYBLOCK_PROOF_URL the web app's /api/proof, where `settle` gets signed Pyth prices without a Pyth
|
|
37
|
+
* key of its own (default on Monad testnet: the hosted app; set "" to disable)
|
|
38
|
+
*/
|
|
39
|
+
export function configFromEnv(env = process.env) {
|
|
40
|
+
const need = (name) => {
|
|
41
|
+
const v = env[name];
|
|
42
|
+
if (!v)
|
|
43
|
+
throw new Error(`set ${name}`);
|
|
44
|
+
return v;
|
|
45
|
+
};
|
|
46
|
+
const positive = (name, fallback) => {
|
|
47
|
+
const v = env[name] ?? fallback;
|
|
48
|
+
if (!/^\d+(\.\d+)?$/.test(v) || Number(v) <= 0)
|
|
49
|
+
throw new Error(`${name} must be a positive number`);
|
|
50
|
+
return v;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
// With neither set, the server talks to the live Monad testnet deployment, so `npx everyblock-mcp` just works.
|
|
54
|
+
rpcUrl: env.RPC_URL || (env.EVERYBLOCK ? need("RPC_URL") : TESTNET_RPC),
|
|
55
|
+
everyblock: (env.EVERYBLOCK || (env.RPC_URL ? need("EVERYBLOCK") : TESTNET_EVERYBLOCK)),
|
|
56
|
+
feeds: (env.FEEDS ? env.FEEDS.split(",").map((s) => s.trim()) : DEFAULT_FEEDS),
|
|
57
|
+
allowedChains: (env.MCP_ALLOWED_CHAINS ?? `${MONAD_TESTNET},${ANVIL}`)
|
|
58
|
+
.split(",")
|
|
59
|
+
.filter((s) => s.trim() !== "")
|
|
60
|
+
.map((s) => Number(s.trim())),
|
|
61
|
+
maxOrderSize: BigInt(positive("MCP_MAX_ORDER_SIZE", "100").split(".")[0]),
|
|
62
|
+
maxSpendUsd: positive("MCP_MAX_SPEND_USD", "50"),
|
|
63
|
+
logRange: BigInt(positive("MCP_LOG_RANGE", "100").split(".")[0]),
|
|
64
|
+
logLookback: BigInt(positive("MCP_LOG_LOOKBACK", "2000").split(".")[0]),
|
|
65
|
+
privateKey: env.MCP_PRIVATE_KEY ? env.MCP_PRIVATE_KEY : undefined,
|
|
66
|
+
unlockedAccount: env.MCP_UNLOCKED_ACCOUNT ? env.MCP_UNLOCKED_ACCOUNT : undefined,
|
|
67
|
+
proofUrl: env.EVERYBLOCK_PROOF_URL,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function connect(cfg) {
|
|
71
|
+
// viem polls for receipts every 4 s by default; Monad makes a block in well under a second.
|
|
72
|
+
const pub = createPublicClient({ transport: http(cfg.rpcUrl), pollingInterval: 250 });
|
|
73
|
+
const chainId = await pub.getChainId();
|
|
74
|
+
const chain = defineChain({
|
|
75
|
+
id: chainId,
|
|
76
|
+
name: `chain-${chainId}`,
|
|
77
|
+
nativeCurrency: { name: "MON", symbol: "MON", decimals: 18 },
|
|
78
|
+
rpcUrls: { default: { http: [cfg.rpcUrl] } },
|
|
79
|
+
});
|
|
80
|
+
let account = null;
|
|
81
|
+
if (cfg.privateKey) {
|
|
82
|
+
account = privateKeyToAccount(cfg.privateKey);
|
|
83
|
+
}
|
|
84
|
+
else if (cfg.unlockedAccount) {
|
|
85
|
+
if (!isLocalRpc(cfg.rpcUrl))
|
|
86
|
+
throw new Error("MCP_UNLOCKED_ACCOUNT is only for a local node (localhost RPC_URL)");
|
|
87
|
+
account = cfg.unlockedAccount;
|
|
88
|
+
}
|
|
89
|
+
const wallet = account ? createWalletClient({ transport: http(cfg.rpcUrl) }) : null;
|
|
90
|
+
const [collateral, cent] = await Promise.all([
|
|
91
|
+
pub.readContract({ address: cfg.everyblock, abi: everyblockAbi, functionName: "collateral" }),
|
|
92
|
+
pub.readContract({ address: cfg.everyblock, abi: everyblockAbi, functionName: "CENT" }),
|
|
93
|
+
]);
|
|
94
|
+
const share = 100n * cent;
|
|
95
|
+
const digits = String(share).length - 1;
|
|
96
|
+
const maxSpend = 10n ** BigInt(digits) === share
|
|
97
|
+
? parseUnits(cfg.maxSpendUsd, digits)
|
|
98
|
+
: BigInt(Math.floor(Number(cfg.maxSpendUsd) * Number(share)));
|
|
99
|
+
return {
|
|
100
|
+
pub,
|
|
101
|
+
wallet,
|
|
102
|
+
account,
|
|
103
|
+
address: account === null ? null : typeof account === "string" ? account : account.address,
|
|
104
|
+
chain,
|
|
105
|
+
chainId,
|
|
106
|
+
everyblock: cfg.everyblock,
|
|
107
|
+
collateral,
|
|
108
|
+
cent,
|
|
109
|
+
feeds: cfg.feeds,
|
|
110
|
+
allowedChains: cfg.allowedChains,
|
|
111
|
+
maxOrderSize: cfg.maxOrderSize,
|
|
112
|
+
maxSpend,
|
|
113
|
+
logRange: cfg.logRange,
|
|
114
|
+
logLookback: cfg.logLookback,
|
|
115
|
+
proofUrl: cfg.proofUrl === undefined ? (chainId === MONAD_TESTNET ? TESTNET_PROOF_URL : undefined) : cfg.proofUrl || undefined,
|
|
116
|
+
queue: Promise.resolve(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// ------------------------------------------------------------------------------------------------
|
|
120
|
+
// Reads
|
|
121
|
+
// ------------------------------------------------------------------------------------------------
|
|
122
|
+
/** Reads Everyblock. The function name is dynamic, so the call is typed loosely; the ABI still encodes it. */
|
|
123
|
+
export function read(ctx, functionName, args = []) {
|
|
124
|
+
return ctx.pub.readContract({ address: ctx.everyblock, abi: everyblockAbi, functionName, args });
|
|
125
|
+
}
|
|
126
|
+
export function readToken(ctx, functionName, args = []) {
|
|
127
|
+
return ctx.pub.readContract({ address: ctx.collateral, abi: testUsdAbi, functionName, args });
|
|
128
|
+
}
|
|
129
|
+
/** marketFeeBps(marketId) where the deployment has it, else 0. */
|
|
130
|
+
export async function feeBps(ctx, marketId) {
|
|
131
|
+
try {
|
|
132
|
+
const bps = await ctx.pub.readContract({
|
|
133
|
+
address: ctx.everyblock,
|
|
134
|
+
abi: feeAbi,
|
|
135
|
+
functionName: "marketFeeBps",
|
|
136
|
+
args: [marketId],
|
|
137
|
+
});
|
|
138
|
+
return { bps: Number(bps), source: "marketFeeBps" };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { bps: 0, source: "none (this deployment charges no fee)" };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export async function now(ctx) {
|
|
145
|
+
const b = await ctx.pub.getBlock();
|
|
146
|
+
return { timestamp: b.timestamp, number: b.number };
|
|
147
|
+
}
|
|
148
|
+
/** Why writes are refused on this server, or null if they are allowed. */
|
|
149
|
+
export function writeBlocker(ctx) {
|
|
150
|
+
if (!ctx.wallet || !ctx.account) {
|
|
151
|
+
return "this server is read-only: set MCP_PRIVATE_KEY (or MCP_UNLOCKED_ACCOUNT on a local node) to trade";
|
|
152
|
+
}
|
|
153
|
+
if (!ctx.allowedChains.includes(ctx.chainId)) {
|
|
154
|
+
return `writes are not allowed on chain ${ctx.chainId} (MCP_ALLOWED_CHAINS=${ctx.allowedChains.join(",")})`;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
/** Runs `fn` after every earlier write of this server has finished. */
|
|
159
|
+
export function serialize(ctx, fn) {
|
|
160
|
+
const run = ctx.queue.then(fn, fn);
|
|
161
|
+
ctx.queue = run.catch(() => undefined);
|
|
162
|
+
return run;
|
|
163
|
+
}
|
|
164
|
+
/** Sends one transaction and waits for it: gas estimate + 15%, receipt checked, events decoded. */
|
|
165
|
+
export async function send(ctx, target, functionName, args, value) {
|
|
166
|
+
const blocked = writeBlocker(ctx);
|
|
167
|
+
if (blocked)
|
|
168
|
+
throw new Error(blocked);
|
|
169
|
+
const abi = (target === "everyblock" ? everyblockAbi : testUsdAbi);
|
|
170
|
+
const address = target === "everyblock" ? ctx.everyblock : ctx.collateral;
|
|
171
|
+
const call = { address, abi, functionName, args, account: ctx.account, ...(value ? { value } : {}) };
|
|
172
|
+
let gas;
|
|
173
|
+
try {
|
|
174
|
+
gas = ((await ctx.pub.estimateContractGas(call)) * GAS_MARGIN_PCT) / 100n;
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
throw new Error(`${functionName} would revert: ${reason(e)}`);
|
|
178
|
+
}
|
|
179
|
+
const hash = await ctx.wallet.writeContract({ ...call, chain: ctx.chain, gas });
|
|
180
|
+
const receipt = await ctx.pub.waitForTransactionReceipt({ hash });
|
|
181
|
+
if (receipt.status !== "success")
|
|
182
|
+
throw new Error(`${functionName} reverted in ${hash}`);
|
|
183
|
+
const logs = parseEventLogs({ abi: [...everyblockAbi, ...testUsdAbi], logs: receipt.logs });
|
|
184
|
+
return {
|
|
185
|
+
txHash: hash,
|
|
186
|
+
blockNumber: receipt.blockNumber.toString(),
|
|
187
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
188
|
+
events: logs.map((l) => ({ name: l.eventName, args: (l.args ?? {}) })),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** A revert's error name (e.g. MarketNotTrading) when the ABI knows it, else viem's short message. */
|
|
192
|
+
export function reason(e) {
|
|
193
|
+
if (e instanceof BaseError) {
|
|
194
|
+
const revert = e.walk((x) => x instanceof ContractFunctionRevertedError);
|
|
195
|
+
if (revert instanceof ContractFunctionRevertedError && revert.data?.errorName) {
|
|
196
|
+
const args = revert.data.args?.length ? `(${revert.data.args.map(String).join(", ")})` : "";
|
|
197
|
+
return `${revert.data.errorName}${args}`;
|
|
198
|
+
}
|
|
199
|
+
return e.shortMessage.split("\n")[0];
|
|
200
|
+
}
|
|
201
|
+
return e instanceof Error ? e.message.split("\n")[0] : String(e);
|
|
202
|
+
}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Everyblock MCP server over stdio. Configuration comes from the environment (see chain.ts configFromEnv);
|
|
3
|
+
// secrets are read, never printed. stdout carries the protocol, so diagnostics go to stderr.
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { configFromEnv, connect } from "./chain.js";
|
|
6
|
+
import { createServer } from "./server.js";
|
|
7
|
+
async function main() {
|
|
8
|
+
const ctx = await connect(configFromEnv());
|
|
9
|
+
const server = createServer(ctx);
|
|
10
|
+
await server.connect(new StdioServerTransport());
|
|
11
|
+
const mode = ctx.address ? `wallet ${ctx.address}` : "read-only";
|
|
12
|
+
const writes = ctx.allowedChains.includes(ctx.chainId) ? "writes allowed" : "writes refused on this chain";
|
|
13
|
+
console.error(`everyblock-mcp: chain ${ctx.chainId}, Everyblock ${ctx.everyblock}, ${mode}, ${writes}`);
|
|
14
|
+
}
|
|
15
|
+
main().catch((e) => {
|
|
16
|
+
console.error(e instanceof Error ? e.message : e);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
package/dist/orders.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Order math, mirrored from Everyblock.sol. Pure functions, so they can be tested without a chain.
|
|
2
|
+
import { formatUnits } from "viem";
|
|
3
|
+
/** Everyblock.Kind, in the contract's order. */
|
|
4
|
+
export const KINDS = ["BuyUp", "SellDown", "SellUp", "BuyDown"];
|
|
5
|
+
export const BUY_UP = 0;
|
|
6
|
+
export const SELL_DOWN = 1;
|
|
7
|
+
export const SELL_UP = 2;
|
|
8
|
+
export const BUY_DOWN = 3;
|
|
9
|
+
export const OUTCOMES = ["pending", "up", "down", "void"];
|
|
10
|
+
export function kindOf(side, outcome) {
|
|
11
|
+
if (side === "buy")
|
|
12
|
+
return outcome === "up" ? BUY_UP : BUY_DOWN;
|
|
13
|
+
return outcome === "up" ? SELL_UP : SELL_DOWN;
|
|
14
|
+
}
|
|
15
|
+
export function describeKind(kind) {
|
|
16
|
+
return [
|
|
17
|
+
{ side: "buy", outcome: "up" },
|
|
18
|
+
{ side: "sell", outcome: "down" },
|
|
19
|
+
{ side: "sell", outcome: "up" },
|
|
20
|
+
{ side: "buy", outcome: "down" },
|
|
21
|
+
][kind];
|
|
22
|
+
}
|
|
23
|
+
/** Buy UP and Sell DOWN are bids in the UP book; Sell UP and Buy DOWN are asks. */
|
|
24
|
+
export const isBid = (kind) => kind === BUY_UP || kind === SELL_DOWN;
|
|
25
|
+
/** The UP-book tick an order rests at: its own price for UP orders, 100 - price for DOWN orders. */
|
|
26
|
+
export const tickOf = (kind, price) => (kind === BUY_UP || kind === SELL_UP ? price : 100 - price);
|
|
27
|
+
/** An order's own limit, back from its tick. */
|
|
28
|
+
export const priceOf = (kind, tick) => tickOf(kind, tick);
|
|
29
|
+
/** The price, in cents of this order's own outcome, that a clearing tick c (cents of UP) means. */
|
|
30
|
+
export const ownPriceAt = (kind, c) => (kind === BUY_UP || kind === SELL_UP ? c : 100 - c);
|
|
31
|
+
/** The fee on a notional at `bps` (the market's marketFeeBps), rounded down like Everyblock._fee. */
|
|
32
|
+
export const feeOn = (notional, bps) => (notional * BigInt(bps)) / 10000n;
|
|
33
|
+
/**
|
|
34
|
+
* What placeOrder takes from the trader: buys escrow price * size cents plus the fee on it (Everyblock._withFee;
|
|
35
|
+
* the unused part comes back on claim), sells escrow the shares.
|
|
36
|
+
*/
|
|
37
|
+
export function escrowOf(kind, price, size, cent, feeBps = 0) {
|
|
38
|
+
if (kind === SELL_UP)
|
|
39
|
+
return { collateral: 0n, up: size, down: 0n };
|
|
40
|
+
if (kind === SELL_DOWN)
|
|
41
|
+
return { collateral: 0n, up: 0n, down: size };
|
|
42
|
+
const notional = size * BigInt(price) * cent;
|
|
43
|
+
return { collateral: notional + feeOn(notional, feeBps), up: 0n, down: 0n };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The worst and best cases of a full fill at the limit, net of the fee (a fill at a better clearing price only
|
|
47
|
+
* improves them): a buy pays at most price per share plus the fee and each share pays 100 cents if its outcome
|
|
48
|
+
* wins; a sell receives at least price per share less the fee and gives up shares that would pay 100 cents if
|
|
49
|
+
* they win.
|
|
50
|
+
*/
|
|
51
|
+
export function scenarios(kind, price, size, cent, feeBps = 0) {
|
|
52
|
+
const share = 100n * cent;
|
|
53
|
+
const atLimit = size * BigInt(price) * cent;
|
|
54
|
+
const fee = feeOn(atLimit, feeBps);
|
|
55
|
+
if (kind === BUY_UP || kind === BUY_DOWN) {
|
|
56
|
+
const maxCost = atLimit + fee;
|
|
57
|
+
return { maxCost, payoutIfWins: size * share, maxProfit: size * share - maxCost, maxLoss: maxCost };
|
|
58
|
+
}
|
|
59
|
+
const minProceeds = atLimit - fee;
|
|
60
|
+
return { minProceeds, maxProfit: minProceeds, maxLoss: size * share - minProceeds };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Everyblock's clearing rule on a batch's levels, as `_clear` runs it: the tick that maximises matched volume,
|
|
64
|
+
* then minimises the surplus, then the midpoint of a zero-surplus range. Tick 0 means no trade. Used to show an
|
|
65
|
+
* indicative price for a batch that is still open.
|
|
66
|
+
*/
|
|
67
|
+
export function clearBatch(levels) {
|
|
68
|
+
const none = { tick: 0, volume: 0n, bidFillAtTick: 0n, askFillAtTick: 0n };
|
|
69
|
+
const bids = new Map();
|
|
70
|
+
const asks = new Map();
|
|
71
|
+
for (const l of levels) {
|
|
72
|
+
if (l.bid > 0n)
|
|
73
|
+
bids.set(l.tick, (bids.get(l.tick) ?? 0n) + l.bid);
|
|
74
|
+
if (l.ask > 0n)
|
|
75
|
+
asks.set(l.tick, (asks.get(l.tick) ?? 0n) + l.ask);
|
|
76
|
+
}
|
|
77
|
+
if (bids.size === 0 || asks.size === 0)
|
|
78
|
+
return none;
|
|
79
|
+
const lo = Math.min(...asks.keys());
|
|
80
|
+
const hi = Math.max(...bids.keys());
|
|
81
|
+
if (hi < lo)
|
|
82
|
+
return none;
|
|
83
|
+
const n = hi - lo + 1;
|
|
84
|
+
const b = [];
|
|
85
|
+
const a = [];
|
|
86
|
+
let demand = 0n;
|
|
87
|
+
for (let i = 0; i < n; i++) {
|
|
88
|
+
b.push(bids.get(lo + i) ?? 0n);
|
|
89
|
+
a.push(asks.get(lo + i) ?? 0n);
|
|
90
|
+
demand += b[i];
|
|
91
|
+
}
|
|
92
|
+
let volume = 0n;
|
|
93
|
+
let eqLo = 0;
|
|
94
|
+
let eqHi = 0;
|
|
95
|
+
let lastPos = 0;
|
|
96
|
+
let posSurplus = 0n;
|
|
97
|
+
let firstNeg = 0;
|
|
98
|
+
let negSurplus = 0n;
|
|
99
|
+
let supply = 0n;
|
|
100
|
+
for (let i = 0; i < n; i++) {
|
|
101
|
+
supply += a[i];
|
|
102
|
+
const v = demand < supply ? demand : supply;
|
|
103
|
+
if (v > volume) {
|
|
104
|
+
volume = v;
|
|
105
|
+
eqLo = eqHi = lastPos = firstNeg = 0;
|
|
106
|
+
}
|
|
107
|
+
if (v === volume) {
|
|
108
|
+
if (demand === supply) {
|
|
109
|
+
if (eqLo === 0)
|
|
110
|
+
eqLo = lo + i;
|
|
111
|
+
eqHi = lo + i;
|
|
112
|
+
}
|
|
113
|
+
else if (demand > supply) {
|
|
114
|
+
lastPos = lo + i;
|
|
115
|
+
posSurplus = demand - supply;
|
|
116
|
+
}
|
|
117
|
+
else if (firstNeg === 0) {
|
|
118
|
+
firstNeg = lo + i;
|
|
119
|
+
negSurplus = supply - demand;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
demand -= b[i];
|
|
123
|
+
}
|
|
124
|
+
if (eqLo !== 0) {
|
|
125
|
+
const c = Math.floor((eqLo + eqHi) / 2);
|
|
126
|
+
return { tick: c, volume, bidFillAtTick: b[c - lo], askFillAtTick: a[c - lo] };
|
|
127
|
+
}
|
|
128
|
+
if (firstNeg === 0 || (lastPos !== 0 && posSurplus <= negSurplus)) {
|
|
129
|
+
return { tick: lastPos, volume, bidFillAtTick: b[lastPos - lo] - posSurplus, askFillAtTick: a[lastPos - lo] };
|
|
130
|
+
}
|
|
131
|
+
return { tick: firstNeg, volume, bidFillAtTick: b[firstNeg - lo], askFillAtTick: a[firstNeg - lo] - negSurplus };
|
|
132
|
+
}
|
|
133
|
+
// ------------------------------------------------------------------------------------------------
|
|
134
|
+
// Markets
|
|
135
|
+
// ------------------------------------------------------------------------------------------------
|
|
136
|
+
export const SAMPLES = 12;
|
|
137
|
+
export const WINDOW = 60n;
|
|
138
|
+
export const GRACE = 3600n;
|
|
139
|
+
/** Where a market is in its life. `outcome` is Everyblock.outcome(), which reads Void after the deadline. */
|
|
140
|
+
export function marketStatus(now, openTime, closeTime, outcome) {
|
|
141
|
+
if (outcome === 3)
|
|
142
|
+
return "void";
|
|
143
|
+
if (outcome !== 0)
|
|
144
|
+
return "resolved";
|
|
145
|
+
if (now < openTime)
|
|
146
|
+
return "upcoming";
|
|
147
|
+
if (now < closeTime)
|
|
148
|
+
return "trading";
|
|
149
|
+
return "closed";
|
|
150
|
+
}
|
|
151
|
+
/** A settlement sum of 12 prices as a mean price, e.g. 65012.5, or null if unset. */
|
|
152
|
+
export function meanPrice(sum, expo, set) {
|
|
153
|
+
if (!set)
|
|
154
|
+
return null;
|
|
155
|
+
const decimals = Math.max(0, -expo);
|
|
156
|
+
const scaled = sum * 10n ** BigInt(Math.max(0, expo)); // positive exponents (never seen) scale up
|
|
157
|
+
const mean = (scaled * 1000n) / BigInt(SAMPLES); // 3 extra digits so the mean does not round away
|
|
158
|
+
return trimZeros(formatUnits(mean, decimals + 3));
|
|
159
|
+
}
|
|
160
|
+
/** Collateral units as dollars of shares (one share pays 100 * CENT units = $1.00). */
|
|
161
|
+
export function usd(units, cent) {
|
|
162
|
+
const share = 100n * cent;
|
|
163
|
+
const digits = String(share).length - 1;
|
|
164
|
+
if (10n ** BigInt(digits) === share)
|
|
165
|
+
return trimZeros(formatUnits(units, digits), 2);
|
|
166
|
+
return (Number(units) / Number(share)).toFixed(6);
|
|
167
|
+
}
|
|
168
|
+
/** Cents as a dollar price, 55 -> "0.55". */
|
|
169
|
+
export const centsToUsd = (cents) => (cents / 100).toFixed(2);
|
|
170
|
+
function trimZeros(s, minDecimals = 0) {
|
|
171
|
+
if (!s.includes("."))
|
|
172
|
+
return minDecimals ? `${s}.${"0".repeat(minDecimals)}` : s;
|
|
173
|
+
let [i, f] = s.split(".");
|
|
174
|
+
f = f.replace(/0+$/, "");
|
|
175
|
+
if (f.length < minDecimals)
|
|
176
|
+
f = f.padEnd(minDecimals, "0");
|
|
177
|
+
return f ? `${i}.${f}` : i;
|
|
178
|
+
}
|