outcometick 1.4.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 +21 -0
- package/README.md +88 -0
- package/api/lib/backtest-contract.mjs +318 -0
- package/api/lib/backtest-datasets.mjs +225 -0
- package/api/lib/backtest-manifest.mjs +345 -0
- package/api/lib/coverage-window.mjs +42 -0
- package/api/lib/data-taxonomy.mjs +175 -0
- package/api/lib/venue-path.mjs +16 -0
- package/bin/ot.mjs +4 -0
- package/cli/api-client.mjs +71 -0
- package/cli/commands/fetch.mjs +43 -0
- package/cli/commands/run.mjs +269 -0
- package/cli/commands/status.mjs +102 -0
- package/cli/commands/submit.mjs +77 -0
- package/cli/local-data.mjs +177 -0
- package/cli/ot.mjs +223 -0
- package/index.d.ts +195 -0
- package/index.mjs +2 -0
- package/package.json +58 -0
- package/runner/analyze/index.mjs +40 -0
- package/runner/analyze/javascript.mjs +380 -0
- package/runner/analyze/python.mjs +85 -0
- package/runner/analyze/python_analyze.py +320 -0
- package/runner/archive.mjs +185 -0
- package/runner/engine/book.mjs +226 -0
- package/runner/engine/portfolio.mjs +292 -0
- package/runner/engine/replay.mjs +496 -0
- package/runner/engine/report.mjs +417 -0
- package/runner/events.mjs +190 -0
- package/runner/harness/node/harness.mjs +467 -0
- package/runner/harness/node/sdk/index.d.ts +195 -0
- package/runner/harness/node/sdk/index.mjs +71 -0
- package/runner/harness/node/sdk/package.json +8 -0
- package/runner/harness/protocol.mjs +255 -0
- package/runner/harness/python/harness.py +374 -0
- package/runner/harness/python/otengine.py +523 -0
- package/runner/harness/python/otreplay.py +409 -0
- package/runner/harness/python/outcometick.py +67 -0
package/cli/ot.mjs
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `ot` — the command line the SDK docs tell customers to use.
|
|
3
|
+
//
|
|
4
|
+
// ot check . validate, free, no data
|
|
5
|
+
// ot run . --data ./polymarket-data-samples --date … replay locally
|
|
6
|
+
// ot submit . --assets btc,eth --from … --to … send it to the queue
|
|
7
|
+
//
|
|
8
|
+
// The one thing this file must get right is that `ot check` runs the SAME
|
|
9
|
+
// validator the queue runs. The docs promise "if it passes locally it will not
|
|
10
|
+
// be rejected on submit", and that promise only survives if there is exactly
|
|
11
|
+
// one implementation — so check() calls the very modules api/lib/backtest-
|
|
12
|
+
// routes.mjs calls, rather than reimplementing any of it.
|
|
13
|
+
//
|
|
14
|
+
// Written in Node because everything it needs already is: the contract, the
|
|
15
|
+
// validator, both analysers, the engine, the report and the archive writer. A
|
|
16
|
+
// Python submission is still run by the Python harness — the CLI spawns it the
|
|
17
|
+
// same way the worker does.
|
|
18
|
+
|
|
19
|
+
import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { pathToFileURL } from 'node:url';
|
|
22
|
+
import process from 'node:process';
|
|
23
|
+
|
|
24
|
+
import { SDK_VERSION, LIMITS, BacktestRejection } from '../api/lib/backtest-contract.mjs';
|
|
25
|
+
import { checkSubmission, MANIFEST_NAME } from '../api/lib/backtest-manifest.mjs';
|
|
26
|
+
import { analyzeSource } from '../runner/analyze/index.mjs';
|
|
27
|
+
|
|
28
|
+
const USAGE = `ot ${SDK_VERSION} — outcometick strategy tools
|
|
29
|
+
|
|
30
|
+
ot check <dir>
|
|
31
|
+
Validate the manifest, the entry point, the hook signatures and every
|
|
32
|
+
import. Touches no data and costs nothing. This is the exact validator
|
|
33
|
+
the queue runs.
|
|
34
|
+
|
|
35
|
+
ot run <dir> --data <archive> [--date <YYYY-MM-DD>] [--out <file>]
|
|
36
|
+
Replay locally against a cloned sample archive, using the same engine
|
|
37
|
+
the queue uses. Writes a report archive.
|
|
38
|
+
|
|
39
|
+
ot submit <dir> --assets btc,eth --from <day> --to <day> [--venue polymarket]
|
|
40
|
+
Send it to the queue. Needs OT_BACKTEST_KEY.
|
|
41
|
+
|
|
42
|
+
ot status <run_id>
|
|
43
|
+
Where a submitted run got to, and what it cost. Needs OT_BACKTEST_KEY.
|
|
44
|
+
|
|
45
|
+
ot fetch <run_id> [--out <file>]
|
|
46
|
+
Download a finished run's archive. Needs OT_BACKTEST_KEY.
|
|
47
|
+
|
|
48
|
+
Common:
|
|
49
|
+
--json machine-readable output
|
|
50
|
+
--api <url> API base (default https://outcometick.com)
|
|
51
|
+
|
|
52
|
+
Free sample data:
|
|
53
|
+
git clone https://github.com/Ligengxin96/polymarket-data-samples
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
/** Parse argv into {command, dir, flags}. */
|
|
57
|
+
export function parseArgs(argv) {
|
|
58
|
+
const [command, ...rest] = argv;
|
|
59
|
+
const flags = {};
|
|
60
|
+
const positional = [];
|
|
61
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
62
|
+
const a = rest[i];
|
|
63
|
+
if (!a.startsWith('--')) { positional.push(a); continue; }
|
|
64
|
+
const key = a.slice(2);
|
|
65
|
+
// Boolean flags take no value; everything else consumes the next token.
|
|
66
|
+
if (key === 'json') { flags.json = true; continue; }
|
|
67
|
+
const value = rest[i + 1];
|
|
68
|
+
if (value == null || value.startsWith('--')) {
|
|
69
|
+
throw new Error(`--${key} needs a value`);
|
|
70
|
+
}
|
|
71
|
+
flags[key] = value;
|
|
72
|
+
i += 1;
|
|
73
|
+
}
|
|
74
|
+
return { command, dir: positional[0] ?? '.', flags };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read a submission directory.
|
|
79
|
+
*
|
|
80
|
+
* Only files the manifest could plausibly reference, and only from the top two
|
|
81
|
+
* levels — a strategy directory that happens to contain a virtualenv or a
|
|
82
|
+
* .git should not turn into a 400 MB submission attempt. The limits are
|
|
83
|
+
* enforced properly by the validator; this is about not reading the disk.
|
|
84
|
+
*/
|
|
85
|
+
export async function readSubmission(dir) {
|
|
86
|
+
const out = [];
|
|
87
|
+
const walk = async (rel, depth) => {
|
|
88
|
+
const entries = await readdir(path.join(dir, rel || '.'), { withFileTypes: true });
|
|
89
|
+
for (const e of entries) {
|
|
90
|
+
const name = rel ? `${rel}/${e.name}` : e.name;
|
|
91
|
+
if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === '__pycache__') continue;
|
|
92
|
+
if (e.isDirectory()) {
|
|
93
|
+
if (depth > 0) await walk(name, depth - 1);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!/\.(py|mjs|js|json|csv)$/.test(e.name)) continue;
|
|
97
|
+
const full = path.join(dir, name);
|
|
98
|
+
const s = await stat(full);
|
|
99
|
+
if (s.size > LIMITS.maxTotalSourceBytes) {
|
|
100
|
+
throw new Error(`${name} is ${s.size} bytes, over the ${LIMITS.maxTotalSourceBytes} byte submission limit`);
|
|
101
|
+
}
|
|
102
|
+
out.push({ name, content: await readFile(full, 'utf8') });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
await walk('', 1);
|
|
106
|
+
if (!out.some((f) => f.name === MANIFEST_NAME)) {
|
|
107
|
+
throw new Error(`no ${MANIFEST_NAME} in ${path.resolve(dir)}`);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate a submission exactly as the queue would.
|
|
114
|
+
*
|
|
115
|
+
* `scope` is optional: coverage can only be checked against a real range, and
|
|
116
|
+
* `ot check` on its own is about the manifest and the source. `ot submit`
|
|
117
|
+
* passes one, and so does the API.
|
|
118
|
+
*/
|
|
119
|
+
export async function validate(files, scope = null) {
|
|
120
|
+
const checked = checkSubmission({ files, scope });
|
|
121
|
+
// analyzeSource, not a dispatch of our own: see runner/analyze/index.mjs for
|
|
122
|
+
// why the two sides sharing this exact function is the whole promise.
|
|
123
|
+
const analysis = await analyzeSource(checked);
|
|
124
|
+
return { ...checked, analysis };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Print a rejection the way the docs describe the codes. */
|
|
128
|
+
function reportRejection(err, json) {
|
|
129
|
+
if (json) {
|
|
130
|
+
process.stdout.write(`${JSON.stringify({ ok: false, ...(err.toJSON?.() ?? { code: 'E_RUNTIME', detail: err.message }) })}\n`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const code = err.code ?? 'E_RUNTIME';
|
|
134
|
+
const detail = err.detail ?? err.message;
|
|
135
|
+
process.stderr.write(`\n ${code}\n ${detail}\n\n`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function cmdCheck({ dir, flags }) {
|
|
139
|
+
const files = await readSubmission(dir);
|
|
140
|
+
const res = await validate(files);
|
|
141
|
+
if (flags.json) {
|
|
142
|
+
process.stdout.write(`${JSON.stringify({
|
|
143
|
+
ok: true,
|
|
144
|
+
manifest: res.manifest,
|
|
145
|
+
hooks: res.hookNames,
|
|
146
|
+
files: res.files.map((f) => ({ name: f.name, bytes: f.bytes })),
|
|
147
|
+
total_bytes: res.totalBytes,
|
|
148
|
+
imports: res.analysis.imports,
|
|
149
|
+
})}\n`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
const { manifest } = res;
|
|
153
|
+
process.stdout.write(`\n ok — ${manifest.language}, entry ${manifest.entry.file}:${manifest.entry.className}\n`);
|
|
154
|
+
process.stdout.write(` hooks ${Object.entries(res.hookNames).map(([k, v]) => `${k} → ${v}`).join(', ')}\n`);
|
|
155
|
+
process.stdout.write(` datasets ${manifest.datasets.join(', ')}\n`);
|
|
156
|
+
if (manifest.reference.length) process.stdout.write(` reference ${manifest.reference.join(', ')}\n`);
|
|
157
|
+
process.stdout.write(` files ${res.files.length} / ${LIMITS.maxFiles} · ${(res.totalBytes / 1024).toFixed(1)} / ${LIMITS.maxTotalSourceBytes / 1024} KB\n`);
|
|
158
|
+
if (manifest.mode === 'session') {
|
|
159
|
+
process.stdout.write(' mode session — bills at 3× the market-day rate\n');
|
|
160
|
+
}
|
|
161
|
+
process.stdout.write('\n');
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function main(argv) {
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
parsed = parseArgs(argv);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
process.stderr.write(`${err.message}\n`);
|
|
171
|
+
return 2;
|
|
172
|
+
}
|
|
173
|
+
const { command, flags } = parsed;
|
|
174
|
+
|
|
175
|
+
if (!command || command === 'help' || flags.help) {
|
|
176
|
+
process.stdout.write(USAGE);
|
|
177
|
+
return command ? 0 : 2;
|
|
178
|
+
}
|
|
179
|
+
if (command === 'version') {
|
|
180
|
+
process.stdout.write(`${SDK_VERSION}\n`);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
if (command === 'check') return await cmdCheck(parsed);
|
|
186
|
+
if (command === 'run') {
|
|
187
|
+
const { cmdRun } = await import('./commands/run.mjs');
|
|
188
|
+
return await cmdRun(parsed);
|
|
189
|
+
}
|
|
190
|
+
if (command === 'submit') {
|
|
191
|
+
const { cmdSubmit } = await import('./commands/submit.mjs');
|
|
192
|
+
return await cmdSubmit(parsed);
|
|
193
|
+
}
|
|
194
|
+
if (command === 'status') {
|
|
195
|
+
const { cmdStatus } = await import('./commands/status.mjs');
|
|
196
|
+
return await cmdStatus(parsed);
|
|
197
|
+
}
|
|
198
|
+
if (command === 'fetch') {
|
|
199
|
+
const { cmdFetch } = await import('./commands/fetch.mjs');
|
|
200
|
+
return await cmdFetch(parsed);
|
|
201
|
+
}
|
|
202
|
+
process.stderr.write(`unknown command ${JSON.stringify(command)}\n\n${USAGE}`);
|
|
203
|
+
return 2;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (err instanceof BacktestRejection) {
|
|
206
|
+
reportRejection(err, flags.json);
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
process.stderr.write(`${err.message}\n`);
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Exact identity, not a suffix match. The npm package's bin shim is also called
|
|
215
|
+
// ot.mjs, so `endsWith('/ot.mjs')` was true when it merely IMPORTED this file —
|
|
216
|
+
// and the CLI ran twice, once from the shim and once from here.
|
|
217
|
+
const invokedDirectly = process.argv[1]
|
|
218
|
+
&& pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
219
|
+
if (invokedDirectly) {
|
|
220
|
+
main(process.argv.slice(2)).then((code) => process.exit(code));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export { main, USAGE };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Type declarations for the `outcometick` strategy SDK.
|
|
2
|
+
//
|
|
3
|
+
// Hand-written against runner/engine/replay.mjs rather than generated, because
|
|
4
|
+
// the runtime is plain ESM. The value here is that a strategy which
|
|
5
|
+
// type-checks is a strategy the validator will accept: the hook names, the
|
|
6
|
+
// hook arities and the shape of `ctx` are all things the queue rejects on, and
|
|
7
|
+
// finding out at compile time is free while finding out after queueing is not.
|
|
8
|
+
//
|
|
9
|
+
// Anything not declared here does not exist at runtime either. `ctx` is frozen
|
|
10
|
+
// and the SDK deliberately exposes no way to reach the network, the clock or
|
|
11
|
+
// the filesystem — see the docs' "Not supported" list.
|
|
12
|
+
|
|
13
|
+
export type Side = 'UP' | 'DOWN';
|
|
14
|
+
|
|
15
|
+
export declare const SIDES: readonly ['UP', 'DOWN'];
|
|
16
|
+
|
|
17
|
+
/** One level of resting depth: [price, size]. */
|
|
18
|
+
export type Level = [number, number];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The book as of one millisecond, frozen.
|
|
22
|
+
*
|
|
23
|
+
* A read-only facade over the engine's live book — mutating what you get back
|
|
24
|
+
* reaches nothing, and there is no way to see a later state through it.
|
|
25
|
+
*/
|
|
26
|
+
export interface BookView {
|
|
27
|
+
readonly marketId: string;
|
|
28
|
+
readonly ts: number;
|
|
29
|
+
/** Best ask for `side` — what you pay to open. */
|
|
30
|
+
best(side: Side): number | null;
|
|
31
|
+
bestBid(side: Side): number | null;
|
|
32
|
+
best_bid(side: Side): number | null;
|
|
33
|
+
/** Size available at or better than `bound` (all of it when omitted). */
|
|
34
|
+
depth(side: Side, bound?: number | null): number;
|
|
35
|
+
bidDepth(side: Side, bound?: number | null): number;
|
|
36
|
+
bid_depth(side: Side, bound?: number | null): number;
|
|
37
|
+
levels(side: Side, n?: number): Level[];
|
|
38
|
+
bidLevels(side: Side, n?: number): Level[];
|
|
39
|
+
bid_levels(side: Side, n?: number): Level[];
|
|
40
|
+
mid(side: Side): number | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A settlement-stream observation. */
|
|
44
|
+
export interface Tick {
|
|
45
|
+
ts_ms: number;
|
|
46
|
+
value: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A market, as a hook sees it.
|
|
51
|
+
*
|
|
52
|
+
* `outcome` is absent everywhere except `onSettle`. That is not an oversight:
|
|
53
|
+
* before settlement the official label does not exist yet from the strategy's
|
|
54
|
+
* point of view, and handing it over early is look-ahead.
|
|
55
|
+
*/
|
|
56
|
+
export interface Market {
|
|
57
|
+
market_id: string;
|
|
58
|
+
asset: string;
|
|
59
|
+
interval: string;
|
|
60
|
+
strike: number | null;
|
|
61
|
+
open_ts_ms: number;
|
|
62
|
+
close_ts_ms: number | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The current position in this market, marked against the real book. */
|
|
66
|
+
export interface Position {
|
|
67
|
+
side: Side | null;
|
|
68
|
+
size: number;
|
|
69
|
+
avg_price: number | null;
|
|
70
|
+
unrealized: number | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A declared reference feed or external series, clamped to `ctx.now`. */
|
|
74
|
+
export interface FeedView<T = Record<string, number>> {
|
|
75
|
+
/** The most recent row stamped at or before ctx.now, or null. */
|
|
76
|
+
readonly last: T | null;
|
|
77
|
+
/** The last `n` rows at or before ctx.now, oldest first. */
|
|
78
|
+
window(n: number): T[];
|
|
79
|
+
/** The row in effect at `ts`, which may not be later than ctx.now. */
|
|
80
|
+
at(ts: number): T | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Everything a strategy can do.
|
|
85
|
+
*
|
|
86
|
+
* Constructed by the runner and frozen. Every accessor is clamped to the
|
|
87
|
+
* current event time, so none of it can see the future by construction rather
|
|
88
|
+
* than by convention.
|
|
89
|
+
*/
|
|
90
|
+
export interface Ctx<P = Record<string, unknown>> {
|
|
91
|
+
/** Params from the manifest, injected before the first hook. */
|
|
92
|
+
readonly p: P;
|
|
93
|
+
/** Current event time in epoch ms. Not the wall clock — there isn't one. */
|
|
94
|
+
readonly now: number;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The book as of this millisecond.
|
|
98
|
+
*
|
|
99
|
+
* Passing another market's id throws unless the manifest declared session
|
|
100
|
+
* mode; cross-market reads are what that mode is for.
|
|
101
|
+
*/
|
|
102
|
+
book(id?: string | null): BookView;
|
|
103
|
+
|
|
104
|
+
/** The last `n` ticks already seen, oldest first. Always a copy. */
|
|
105
|
+
history(n?: number): Tick[];
|
|
106
|
+
|
|
107
|
+
position(): Position;
|
|
108
|
+
|
|
109
|
+
/** Appended to logs.txt in the archive. Truncated past the log limit. */
|
|
110
|
+
log(msg: unknown): void;
|
|
111
|
+
|
|
112
|
+
/** The only randomness available, seeded and recorded in the report. */
|
|
113
|
+
random(seed?: number | null): number;
|
|
114
|
+
|
|
115
|
+
/** A reference feed declared in the manifest. Throws if undeclared. */
|
|
116
|
+
ref(name: string): FeedView;
|
|
117
|
+
|
|
118
|
+
/** An external series declared in the manifest. Throws if undeclared. */
|
|
119
|
+
ext(name: string): FeedView;
|
|
120
|
+
|
|
121
|
+
/** Rolling helpers over the tick history. Identical across languages. */
|
|
122
|
+
zscore(value: number, opts?: { window?: number }): number;
|
|
123
|
+
sma(window?: number): number | null;
|
|
124
|
+
stdev(window?: number): number;
|
|
125
|
+
ema(window?: number): number | null;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Record the strategy's own recompute of the outcome against the official
|
|
129
|
+
* one. Recorded for the cross-check panel, never enforced — a mismatch is
|
|
130
|
+
* information, not a failed run.
|
|
131
|
+
*/
|
|
132
|
+
assert_outcome(market: unknown, outcome: Side): void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface OrderInit {
|
|
136
|
+
side: Side;
|
|
137
|
+
/** Contracts. Must be positive. */
|
|
138
|
+
size: number;
|
|
139
|
+
/**
|
|
140
|
+
* A bound in whichever direction protects you: a ceiling when opening, a
|
|
141
|
+
* floor when reducing. Must be within [0, 1] — a binary outcome token
|
|
142
|
+
* trades nowhere else.
|
|
143
|
+
*/
|
|
144
|
+
limit?: number | null;
|
|
145
|
+
holdS?: number | null;
|
|
146
|
+
hold_s?: number | null;
|
|
147
|
+
reduceOnly?: boolean;
|
|
148
|
+
reduce_only?: boolean;
|
|
149
|
+
/** Only 'ioc' is modelled; anything else is rejected at construction. */
|
|
150
|
+
tif?: 'ioc';
|
|
151
|
+
tag?: string | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* An order a hook returns.
|
|
156
|
+
*
|
|
157
|
+
* Never sent — returned, and matched by the runner against the depth that was
|
|
158
|
+
* actually resting at that millisecond.
|
|
159
|
+
*/
|
|
160
|
+
export declare class Order {
|
|
161
|
+
constructor(init: OrderInit);
|
|
162
|
+
readonly side: Side;
|
|
163
|
+
readonly size: number;
|
|
164
|
+
readonly limit: number | null;
|
|
165
|
+
readonly hold_s: number | null;
|
|
166
|
+
readonly reduce_only: boolean;
|
|
167
|
+
readonly tif: 'ioc';
|
|
168
|
+
readonly tag: string | null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Base class for a submitted strategy.
|
|
173
|
+
*
|
|
174
|
+
* The hooks are intentionally not declared as members: implementing one you
|
|
175
|
+
* did not list in the manifest does nothing, and listing one you did not
|
|
176
|
+
* implement is a rejection. Declare them in `outcometick.json` and write them
|
|
177
|
+
* with these signatures.
|
|
178
|
+
*
|
|
179
|
+
* onMarketOpen(ctx: Ctx, market: Market): void
|
|
180
|
+
* onTick(ctx: Ctx, tick: Tick): Order | null
|
|
181
|
+
* onBook(ctx: Ctx, book: BookView): Order | null
|
|
182
|
+
* onTrade(ctx: Ctx, trade: Tick): Order | null
|
|
183
|
+
* onSettle(ctx: Ctx, market: Market, outcome: Side): void
|
|
184
|
+
*/
|
|
185
|
+
export declare class Strategy<P = Record<string, unknown>> {
|
|
186
|
+
/** Params from the manifest, injected by the runner before the first hook. */
|
|
187
|
+
p: P;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
declare const _default: {
|
|
191
|
+
Strategy: typeof Strategy;
|
|
192
|
+
Order: typeof Order;
|
|
193
|
+
SIDES: typeof SIDES;
|
|
194
|
+
};
|
|
195
|
+
export default _default;
|
package/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "outcometick",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Strategy SDK and CLI for outcometick prediction-market backtests",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://outcometick.com/docs/sdk",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/outcometick/outcometick-sdk-ts.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/outcometick/outcometick-sdk-ts/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"ot": "./bin/ot.mjs"
|
|
17
|
+
},
|
|
18
|
+
"types": "./index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./index.d.ts",
|
|
22
|
+
"default": "./index.mjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=24"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"api",
|
|
30
|
+
"runner",
|
|
31
|
+
"cli",
|
|
32
|
+
"bin",
|
|
33
|
+
"index.mjs",
|
|
34
|
+
"index.d.ts",
|
|
35
|
+
"!**/*.test.mjs",
|
|
36
|
+
"!**/*.test-d.ts"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node --test runner/analyze/*.test.mjs runner/engine/*.test.mjs runner/harness/node/*.test.mjs cli/*.test.mjs"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"acorn": "^8.18.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"typescript": "^5.6.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"danfojs-node": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"mathjs": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"decimal.js": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Choosing the analyser for a submission's language.
|
|
2
|
+
//
|
|
3
|
+
// This exists as a shared function, in a file both sides can import, because
|
|
4
|
+
// the docs make a promise that only holds if there is ONE of it:
|
|
5
|
+
//
|
|
6
|
+
// ot check runs the exact validator the queue runs. If it passes locally
|
|
7
|
+
// it will not be rejected on submit.
|
|
8
|
+
//
|
|
9
|
+
// api/lib/backtest-routes.mjs and cli/ot.mjs both call this. They used to each
|
|
10
|
+
// dispatch on languageId themselves, and the two dispatches were not the same:
|
|
11
|
+
// the server threw for an unrecognised language while the CLI fell through to
|
|
12
|
+
// the JavaScript analyser. With only nodejs and python that difference was
|
|
13
|
+
// invisible, but it would have surfaced the day a third language was added —
|
|
14
|
+
// as `ot check` cheerfully passing a Go strategy it had analysed as JavaScript,
|
|
15
|
+
// followed by a rejection after queueing. Which is precisely the experience the
|
|
16
|
+
// promise above exists to prevent.
|
|
17
|
+
|
|
18
|
+
import { analyzeJavaScriptSubmission } from './javascript.mjs';
|
|
19
|
+
import { analyzePythonSubmission } from './python.mjs';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Statically analyse a checked submission.
|
|
23
|
+
*
|
|
24
|
+
* @param checked The result of checkSubmission — already validated, so the
|
|
25
|
+
* language is known to be one the contract lists.
|
|
26
|
+
*/
|
|
27
|
+
export async function analyzeSource(checked) {
|
|
28
|
+
const { languageId, deps } = checked.manifest;
|
|
29
|
+
if (languageId === 'nodejs') {
|
|
30
|
+
return analyzeJavaScriptSubmission(checked.files, { deps });
|
|
31
|
+
}
|
|
32
|
+
if (languageId === 'python') {
|
|
33
|
+
return analyzePythonSubmission(checked.files, { deps });
|
|
34
|
+
}
|
|
35
|
+
// Fail closed. A language the contract accepts but no analyser covers is a
|
|
36
|
+
// gap on our side, and running it unanalysed is not the safe reading of it.
|
|
37
|
+
const err = new Error(`no analyser for ${languageId}`);
|
|
38
|
+
err.status = 503;
|
|
39
|
+
throw err;
|
|
40
|
+
}
|