outcometick 1.4.1 → 1.5.1
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 +32 -1
- package/api/lib/backtest-contract.mjs +1 -1
- package/client/data.d.ts +157 -0
- package/client/data.mjs +263 -0
- package/package.json +10 -5
- package/runner/analyze/python_analyze.py +20 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
scripts/publish-sdk-repos.mjs and overwritten wholesale on each publish.
|
|
6
6
|
An edit made here survives until the next publish and then disappears.
|
|
7
7
|
|
|
8
|
-
Generated from monorepo revision
|
|
8
|
+
Generated from monorepo revision 3652fc71edb3360170ce324e623b19d0ff994844.
|
|
9
9
|
-->
|
|
10
10
|
|
|
11
11
|
# outcometick
|
|
@@ -79,6 +79,37 @@ npm install && npm test
|
|
|
79
79
|
Requires `python3` on PATH: one of the two static analysers is written in
|
|
80
80
|
Python, and the CLI drives it the same way the API does.
|
|
81
81
|
|
|
82
|
+
## Downloading data
|
|
83
|
+
|
|
84
|
+
The other half of the package: a client for the data subscription, on a
|
|
85
|
+
separate import because it has nothing to do with writing a strategy.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { DataClient, NO_VALUE } from "outcometick/data";
|
|
89
|
+
|
|
90
|
+
const ot = new DataClient(); // key from OT_KEY
|
|
91
|
+
|
|
92
|
+
const meta = await ot.meta(); // what can this key see?
|
|
93
|
+
|
|
94
|
+
const { files } = await ot.files({
|
|
95
|
+
from: "2026-08-01", to: "2026-08-12", // or date: "2026-08-12"
|
|
96
|
+
asset: ["BTCUSD", "ETHUSD"], // an array means "any of these"
|
|
97
|
+
dataset: "prices",
|
|
98
|
+
interval: ["5m", NO_VALUE], // "5m" alone EXCLUDES the
|
|
99
|
+
}); // period-less settlement streams
|
|
100
|
+
|
|
101
|
+
await ot.download(files[0], { saveTo: files[0].name }); // checksum verified
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`meta()` reports the dimensions this key can actually reach. Note that
|
|
105
|
+
`intervals` holds real durations only — the `none` sentinel is reported
|
|
106
|
+
separately under `filterTokens`, so a caller that builds an enum from it or
|
|
107
|
+
parses the values as durations never meets a token.
|
|
108
|
+
|
|
109
|
+
Downloads are checksum-verified: `/v1/dl` redirects to storage with the sha256
|
|
110
|
+
in a header, and the client follows that redirect itself so the checksum is not
|
|
111
|
+
thrown away.
|
|
112
|
+
|
|
82
113
|
---
|
|
83
114
|
|
|
84
115
|
Writing your strategy in Python instead? The SDK for it is
|
|
@@ -15,7 +15,7 @@ import { FIRST_COMPLETE_DAY } from './coverage-window.mjs';
|
|
|
15
15
|
export const SCHEMA_VERSION = 1;
|
|
16
16
|
|
|
17
17
|
/** SDK version reported by the docs page and stamped into every report. */
|
|
18
|
-
export const SDK_VERSION = '1.
|
|
18
|
+
export const SDK_VERSION = '1.5.1';
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// Languages
|
package/client/data.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Types for the data-subscription client, `outcometick/data`.
|
|
2
|
+
//
|
|
3
|
+
// Hand-written against api/subscription-api.mjs, like the strategy SDK's
|
|
4
|
+
// declarations, and guarded the same way — see client/data-types.test.mjs.
|
|
5
|
+
|
|
6
|
+
/** A filter value: one alternative, or several meaning "any of these". */
|
|
7
|
+
export type Filter = string | readonly string[];
|
|
8
|
+
|
|
9
|
+
/** The sentinel naming files that have no value for a dimension. */
|
|
10
|
+
export declare const NO_VALUE: 'none';
|
|
11
|
+
|
|
12
|
+
export declare const DEFAULT_BASE_URL: string;
|
|
13
|
+
|
|
14
|
+
/** An error carrying the status and whatever the API said alongside it. */
|
|
15
|
+
export declare class OutcometickError extends Error {
|
|
16
|
+
readonly status: number;
|
|
17
|
+
/** The API's `error` string. */
|
|
18
|
+
readonly detail: string;
|
|
19
|
+
/**
|
|
20
|
+
* The parsed response. A 403 on a date outside coverage also carries `floor`
|
|
21
|
+
* and `ceiling` — the difference between "no" and "no, but here is the range
|
|
22
|
+
* you do have".
|
|
23
|
+
*/
|
|
24
|
+
readonly body: unknown;
|
|
25
|
+
readonly url: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One archive file, as returned by `files()`. */
|
|
29
|
+
export interface FileRow {
|
|
30
|
+
date: string;
|
|
31
|
+
name: string;
|
|
32
|
+
venue: string;
|
|
33
|
+
dataset: string;
|
|
34
|
+
/** null for datasets that are not per-asset. */
|
|
35
|
+
asset: string | null;
|
|
36
|
+
/** null for streams with no period — the settlement feeds. */
|
|
37
|
+
interval: string | null;
|
|
38
|
+
bytes: number;
|
|
39
|
+
sha256: string;
|
|
40
|
+
/** Direct download; needs the key, as a header or `?api_key=`. */
|
|
41
|
+
url: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface FilesResult {
|
|
45
|
+
from: string;
|
|
46
|
+
to: string;
|
|
47
|
+
/** Days actually in scope within [from, to], not the span. */
|
|
48
|
+
days: number;
|
|
49
|
+
count: number;
|
|
50
|
+
/** Total size of the matched files. */
|
|
51
|
+
bytes: number;
|
|
52
|
+
files: FileRow[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface FilesQuery {
|
|
56
|
+
/** One day. Cannot be combined with from/to. */
|
|
57
|
+
date?: string;
|
|
58
|
+
/** Inclusive start. Defaults to the newest day in scope. */
|
|
59
|
+
from?: string;
|
|
60
|
+
/** Inclusive end. Defaults to `from`. */
|
|
61
|
+
to?: string;
|
|
62
|
+
venue?: Filter;
|
|
63
|
+
dataset?: Filter;
|
|
64
|
+
asset?: Filter;
|
|
65
|
+
/** `"none"` selects the streams that have no period at all. */
|
|
66
|
+
interval?: Filter;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface MetaResult {
|
|
70
|
+
firstDay?: string;
|
|
71
|
+
lastDay?: string;
|
|
72
|
+
/** Number of days in scope, not a span. */
|
|
73
|
+
days: number;
|
|
74
|
+
venues: string[];
|
|
75
|
+
assets: string[];
|
|
76
|
+
/** Real durations only — the sentinel lives in `filterTokens`. */
|
|
77
|
+
intervals: string[];
|
|
78
|
+
/** dataset name -> human description. */
|
|
79
|
+
datasets: Record<string, string>;
|
|
80
|
+
filterTokens: {
|
|
81
|
+
noValue: 'none';
|
|
82
|
+
/** Which dimensions some file leaves empty. */
|
|
83
|
+
appliesTo: string[];
|
|
84
|
+
};
|
|
85
|
+
scope?: unknown;
|
|
86
|
+
sampledFrom?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface DaysResult {
|
|
90
|
+
days: string[];
|
|
91
|
+
/** Earliest downloadable day, or null when unbounded. */
|
|
92
|
+
floor: string | null;
|
|
93
|
+
/** Latest downloadable day, or null when unbounded. */
|
|
94
|
+
ceiling: string | null;
|
|
95
|
+
scope?: unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SignedUrl {
|
|
99
|
+
url: string;
|
|
100
|
+
name: string;
|
|
101
|
+
bytes: number;
|
|
102
|
+
sha256: string;
|
|
103
|
+
expiresInSec: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface DownloadResult {
|
|
107
|
+
bytes: Uint8Array;
|
|
108
|
+
/** The checksum that was verified, when one was available. */
|
|
109
|
+
sha256: string | null;
|
|
110
|
+
name: string;
|
|
111
|
+
date: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface DownloadOptions {
|
|
115
|
+
/** Verify the sha256. Default true. */
|
|
116
|
+
verify?: boolean;
|
|
117
|
+
/** Also write the bytes to this path. */
|
|
118
|
+
saveTo?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface DataClientOptions {
|
|
122
|
+
/** Defaults to process.env.OT_KEY. */
|
|
123
|
+
key?: string | null;
|
|
124
|
+
/** Defaults to https://outcometick.com. */
|
|
125
|
+
baseUrl?: string;
|
|
126
|
+
/** Injectable for tests. */
|
|
127
|
+
fetch?: typeof globalThis.fetch;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export declare class DataClient {
|
|
131
|
+
constructor(options?: DataClientOptions);
|
|
132
|
+
readonly key: string | null;
|
|
133
|
+
readonly baseUrl: string;
|
|
134
|
+
|
|
135
|
+
/** The date window and every dimension value in it. */
|
|
136
|
+
meta(): Promise<MetaResult>;
|
|
137
|
+
|
|
138
|
+
/** The days this key may download, with the window bounds. */
|
|
139
|
+
days(): Promise<DaysResult>;
|
|
140
|
+
|
|
141
|
+
/** Search across a date range. Filters accept a string or an array. */
|
|
142
|
+
files(query?: FilesQuery): Promise<FilesResult>;
|
|
143
|
+
|
|
144
|
+
/** A presigned URL, without fetching the bytes. Short-lived. */
|
|
145
|
+
signUrl(date: string, name: string, options?: { expiresIn?: number }): Promise<SignedUrl>;
|
|
146
|
+
|
|
147
|
+
/** Download one file, verifying its checksum. */
|
|
148
|
+
download(file: FileRow, options?: DownloadOptions): Promise<DownloadResult>;
|
|
149
|
+
download(date: string, name: string, options?: DownloadOptions): Promise<DownloadResult>;
|
|
150
|
+
|
|
151
|
+
/** Public — no key required. */
|
|
152
|
+
coverage(): Promise<unknown>;
|
|
153
|
+
plans(): Promise<unknown>;
|
|
154
|
+
health(): Promise<unknown>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default DataClient;
|
package/client/data.mjs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// The data-subscription client: `outcometick/data`.
|
|
2
|
+
//
|
|
3
|
+
// import { DataClient } from "outcometick/data";
|
|
4
|
+
// const ot = new DataClient(); // key from OT_KEY
|
|
5
|
+
// const { files } = await ot.files({ asset: ["btc", "eth"], dataset: "prices" });
|
|
6
|
+
// await ot.download(files[0], "./btc.csv.gz"); // verifies the checksum
|
|
7
|
+
//
|
|
8
|
+
// Deliberately a SUBPATH, not part of the package root. The root exports the
|
|
9
|
+
// strategy SDK — `Strategy` and `Order` — which is what a backtest imports, and
|
|
10
|
+
// that code runs in a container with no network at all. Putting an HTTP client
|
|
11
|
+
// on the same import would invite a strategy to reach for it, type-check
|
|
12
|
+
// locally, and then fail inside the sandbox. Here it cannot be reached by
|
|
13
|
+
// accident, and the submission analyser rejects the import outright.
|
|
14
|
+
//
|
|
15
|
+
// Everything this file knows about the API's shape came from reading
|
|
16
|
+
// api/subscription-api.mjs, not from the docs page. The two had drifted: the
|
|
17
|
+
// published curl example shows only `asset` and `dataset`, while /v1/files also
|
|
18
|
+
// takes a date RANGE and a venue and an interval, and every filter accepts
|
|
19
|
+
// comma-separated alternatives plus a `none` sentinel.
|
|
20
|
+
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { writeFile } from 'node:fs/promises';
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_BASE_URL = 'https://outcometick.com';
|
|
25
|
+
|
|
26
|
+
/** The sentinel that names files with no value for a dimension. */
|
|
27
|
+
export const NO_VALUE = 'none';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* An API error, carrying whatever the server said alongside the status.
|
|
31
|
+
*
|
|
32
|
+
* The subscription API answers 403 on a date outside coverage with the actual
|
|
33
|
+
* `floor` and `ceiling`, which is the difference between "you cannot have this"
|
|
34
|
+
* and "you cannot have this, here is what you can have". Flattening that into a
|
|
35
|
+
* message string would throw the useful half away.
|
|
36
|
+
*/
|
|
37
|
+
export class OutcometickError extends Error {
|
|
38
|
+
constructor(status, body, url) {
|
|
39
|
+
const detail = body?.error ?? (typeof body === 'string' ? body.slice(0, 200) : 'request failed');
|
|
40
|
+
super(`${status} ${detail}`);
|
|
41
|
+
this.name = 'OutcometickError';
|
|
42
|
+
this.status = status;
|
|
43
|
+
this.detail = detail;
|
|
44
|
+
this.body = body;
|
|
45
|
+
this.url = url;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Render one filter value.
|
|
51
|
+
*
|
|
52
|
+
* Arrays join with commas because that is exactly what the API means by
|
|
53
|
+
* `asset=btc,eth` — alternatives, not a nested structure. Passing an array is
|
|
54
|
+
* the friendlier spelling of the same request, so both work.
|
|
55
|
+
*/
|
|
56
|
+
function filterValue(v) {
|
|
57
|
+
if (v == null) return null;
|
|
58
|
+
const parts = (Array.isArray(v) ? v : [v])
|
|
59
|
+
.map((x) => String(x).trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
return parts.length ? parts.join(',') : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class DataClient {
|
|
65
|
+
/**
|
|
66
|
+
* @param opts.key API key. Defaults to process.env.OT_KEY.
|
|
67
|
+
* @param opts.baseUrl API origin. Defaults to https://outcometick.com.
|
|
68
|
+
* @param opts.fetch Injectable for tests.
|
|
69
|
+
*/
|
|
70
|
+
constructor({ key = null, baseUrl = DEFAULT_BASE_URL, fetch: fetchImpl = null } = {}) {
|
|
71
|
+
// Read at construction so the failure is "you have not set a key", raised
|
|
72
|
+
// once and early, rather than a 401 from whichever call happened to be
|
|
73
|
+
// first.
|
|
74
|
+
this.key = key ?? process.env.OT_KEY ?? null;
|
|
75
|
+
this.baseUrl = String(baseUrl).replace(/\/+$/, '');
|
|
76
|
+
this._fetch = fetchImpl ?? globalThis.fetch;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The key, or a readable explanation of its absence. */
|
|
80
|
+
_requireKey() {
|
|
81
|
+
if (!this.key) {
|
|
82
|
+
throw new Error('no API key.\n'
|
|
83
|
+
+ ' Pass one as new DataClient({ key }), or set OT_KEY:\n'
|
|
84
|
+
+ ' export OT_KEY="ck_…"');
|
|
85
|
+
}
|
|
86
|
+
return this.key;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async _get(path, { query = null, auth = true, redirect = 'follow' } = {}) {
|
|
90
|
+
const url = new URL(this.baseUrl + path);
|
|
91
|
+
for (const [k, v] of Object.entries(query ?? {})) {
|
|
92
|
+
const value = filterValue(v);
|
|
93
|
+
if (value !== null) url.searchParams.set(k, value);
|
|
94
|
+
}
|
|
95
|
+
const headers = auth ? { authorization: `Bearer ${this._requireKey()}` } : {};
|
|
96
|
+
|
|
97
|
+
let res;
|
|
98
|
+
try {
|
|
99
|
+
res = await this._fetch(url, { headers, redirect });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw new Error(`could not reach ${this.baseUrl}: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
return { res, url: url.toString() };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async _json(path, opts = {}) {
|
|
107
|
+
const { res, url } = await this._get(path, opts);
|
|
108
|
+
const text = await res.text();
|
|
109
|
+
let body;
|
|
110
|
+
try { body = JSON.parse(text); } catch { body = text; }
|
|
111
|
+
if (!res.ok) throw new OutcometickError(res.status, body, url);
|
|
112
|
+
return body;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------- discovery -------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* What this key can see: the date window, and every dimension value in it.
|
|
119
|
+
*
|
|
120
|
+
* `assets` and `intervals` hold real symbols and real durations only. The
|
|
121
|
+
* `none` sentinel is reported separately under `filterTokens`, because a
|
|
122
|
+
* client that builds an enum from `intervals` or parses them as durations
|
|
123
|
+
* must not meet a token.
|
|
124
|
+
*/
|
|
125
|
+
async meta() {
|
|
126
|
+
return this._json('/v1/meta');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The days this key may download, with the window's floor and ceiling. */
|
|
130
|
+
async days() {
|
|
131
|
+
return this._json('/v1/mirror/days');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Search for files across a date range.
|
|
136
|
+
*
|
|
137
|
+
* @param q.date one day — sugar for from === to. Cannot be combined
|
|
138
|
+
* with from/to.
|
|
139
|
+
* @param q.from inclusive start; defaults to the newest day in scope.
|
|
140
|
+
* @param q.to inclusive end; defaults to `from`.
|
|
141
|
+
* @param q.venue polymarket | predict-fun
|
|
142
|
+
* @param q.dataset prices | twap60s | book | klines | … (see meta())
|
|
143
|
+
* @param q.asset BTCUSD, ETHUSD, …
|
|
144
|
+
* @param q.interval 5m, 1h, … or "none" for the streams that have no period
|
|
145
|
+
*
|
|
146
|
+
* Every filter accepts a string or an array; an array is joined with commas
|
|
147
|
+
* and means "any of these". `interval: ["5m", NO_VALUE]` is how you ask for
|
|
148
|
+
* 5-minute files AND the period-less settlement streams — asking for "5m"
|
|
149
|
+
* alone deliberately excludes them.
|
|
150
|
+
*
|
|
151
|
+
* The server caps the range (92 days by default) and answers 400 past it.
|
|
152
|
+
*/
|
|
153
|
+
async files(q = {}) {
|
|
154
|
+
if (q.date && (q.from || q.to)) {
|
|
155
|
+
// The server rejects this too; catching it here saves a round trip and
|
|
156
|
+
// says the same thing, so the two cannot describe it differently.
|
|
157
|
+
throw new Error('use either date, or from/to — not both');
|
|
158
|
+
}
|
|
159
|
+
return this._json('/v1/files', {
|
|
160
|
+
query: {
|
|
161
|
+
date: q.date, from: q.from, to: q.to,
|
|
162
|
+
venue: q.venue, dataset: q.dataset, asset: q.asset, interval: q.interval,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------- download --------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A presigned URL for one file, without fetching it.
|
|
171
|
+
*
|
|
172
|
+
* Useful when something else does the fetching — a data frame library, a job
|
|
173
|
+
* runner, a browser. The URL is short-lived; hold the `{date, name}` pair and
|
|
174
|
+
* ask again rather than storing it.
|
|
175
|
+
*/
|
|
176
|
+
async signUrl(date, name, { expiresIn = null } = {}) {
|
|
177
|
+
return this._json('/v1/mirror/download', {
|
|
178
|
+
query: { date, name, ...(expiresIn ? { expiresIn } : {}) },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Download one file.
|
|
184
|
+
*
|
|
185
|
+
* Accepts either a row from files() or an explicit (date, name).
|
|
186
|
+
*
|
|
187
|
+
* The checksum is verified by default. /v1/dl answers 302 with the sha256 in
|
|
188
|
+
* a header and the bytes come from R2 behind the redirect, so the redirect is
|
|
189
|
+
* followed MANUALLY: letting fetch follow it would discard the header and
|
|
190
|
+
* with it the only checksum available without a second API call. A row from
|
|
191
|
+
* files() carries its own sha256, which is used when present.
|
|
192
|
+
*
|
|
193
|
+
* @returns {Promise<{bytes: Uint8Array, sha256: string|null, name: string, date: string}>}
|
|
194
|
+
*/
|
|
195
|
+
async download(fileOrDate, nameOrOpts = null, maybeOpts = null) {
|
|
196
|
+
const isRow = fileOrDate && typeof fileOrDate === 'object';
|
|
197
|
+
const date = isRow ? fileOrDate.date : fileOrDate;
|
|
198
|
+
const name = isRow ? fileOrDate.name : nameOrOpts;
|
|
199
|
+
const opts = (isRow ? nameOrOpts : maybeOpts) ?? {};
|
|
200
|
+
const { verify = true, saveTo = null } = opts;
|
|
201
|
+
|
|
202
|
+
if (!date || !name) throw new Error('download needs a file row, or a date and a name');
|
|
203
|
+
|
|
204
|
+
const { res, url } = await this._get(
|
|
205
|
+
`/v1/dl/${encodeURIComponent(date)}/${encodeURIComponent(name)}`,
|
|
206
|
+
{ redirect: 'manual' },
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
let expected = isRow ? (fileOrDate.sha256 ?? null) : null;
|
|
210
|
+
let bytesRes = res;
|
|
211
|
+
|
|
212
|
+
if (res.status >= 300 && res.status < 400) {
|
|
213
|
+
expected = expected
|
|
214
|
+
?? res.headers.get('x-outcometick-sha256')
|
|
215
|
+
?? res.headers.get('x-amz-meta-sha256')
|
|
216
|
+
?? null;
|
|
217
|
+
const location = res.headers.get('location');
|
|
218
|
+
if (!location) throw new OutcometickError(res.status, { error: 'redirect with no location' }, url);
|
|
219
|
+
// The signed URL carries its own auth; sending ours to R2 as well would
|
|
220
|
+
// leak the key to a host that has no use for it.
|
|
221
|
+
bytesRes = await this._fetch(location, { redirect: 'follow' });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!bytesRes.ok) {
|
|
225
|
+
const text = await bytesRes.text();
|
|
226
|
+
let body;
|
|
227
|
+
try { body = JSON.parse(text); } catch { body = text; }
|
|
228
|
+
throw new OutcometickError(bytesRes.status, body, url);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const bytes = new Uint8Array(await bytesRes.arrayBuffer());
|
|
232
|
+
|
|
233
|
+
if (verify && expected) {
|
|
234
|
+
const got = createHash('sha256').update(bytes).digest('hex');
|
|
235
|
+
if (got !== expected) {
|
|
236
|
+
throw new Error(`checksum mismatch for ${date}/${name}\n`
|
|
237
|
+
+ ` expected ${expected}\n got ${got}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (saveTo) await writeFile(saveTo, bytes);
|
|
242
|
+
return { bytes, sha256: expected, name, date };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------- public, no key needed -------------------------------------
|
|
246
|
+
|
|
247
|
+
/** Coverage across all venues. Public — works without a key. */
|
|
248
|
+
async coverage() {
|
|
249
|
+
return this._json('/v1/public/coverage', { auth: false });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Plans and live prices. Public. */
|
|
253
|
+
async plans() {
|
|
254
|
+
return this._json('/v1/public/plans', { auth: false });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Liveness. Public. */
|
|
258
|
+
async health() {
|
|
259
|
+
return this._json('/v1/health', { auth: false });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export default DataClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "outcometick",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Strategy SDK and CLI for outcometick prediction-market backtests",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
".": {
|
|
21
21
|
"types": "./index.d.ts",
|
|
22
22
|
"default": "./index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./data": {
|
|
25
|
+
"types": "./client/data.d.ts",
|
|
26
|
+
"default": "./client/data.mjs"
|
|
23
27
|
}
|
|
24
28
|
},
|
|
25
29
|
"engines": {
|
|
@@ -27,16 +31,17 @@
|
|
|
27
31
|
},
|
|
28
32
|
"files": [
|
|
29
33
|
"api",
|
|
30
|
-
"runner",
|
|
31
|
-
"cli",
|
|
32
34
|
"bin",
|
|
33
|
-
"
|
|
35
|
+
"cli",
|
|
36
|
+
"client",
|
|
34
37
|
"index.d.ts",
|
|
38
|
+
"index.mjs",
|
|
39
|
+
"runner",
|
|
35
40
|
"!**/*.test.mjs",
|
|
36
41
|
"!**/*.test-d.ts"
|
|
37
42
|
],
|
|
38
43
|
"scripts": {
|
|
39
|
-
"test": "node --test runner/analyze/*.test.mjs runner/engine/*.test.mjs runner/harness/node/*.test.mjs cli/*.test.mjs"
|
|
44
|
+
"test": "node --test runner/analyze/*.test.mjs runner/engine/*.test.mjs runner/harness/node/*.test.mjs cli/*.test.mjs client/*.test.mjs"
|
|
40
45
|
},
|
|
41
46
|
"dependencies": {
|
|
42
47
|
"acorn": "^8.18.0"
|
|
@@ -145,6 +145,26 @@ def check_import(module, name, line, deps, relative_ok):
|
|
|
145
145
|
else "E_FORBIDDEN"
|
|
146
146
|
)
|
|
147
147
|
raise Reject(code, f"{name}:{line}: import {root} — {FORBIDDEN_IMPORTS[root]}", name, line)
|
|
148
|
+
# `outcometick` is allowed as the SDK module and nothing else. In the
|
|
149
|
+
# sandbox it is a single flat module -- Strategy and Order -- so
|
|
150
|
+
# `outcometick.data` resolves to nothing there, even though pip installs it
|
|
151
|
+
# as a real submodule. Allowing it because the ROOT matches would mean
|
|
152
|
+
# `ot check` passing a strategy that dies on import after being queued,
|
|
153
|
+
# which is the exact failure the "if it passes locally it will not be
|
|
154
|
+
# rejected on submit" promise exists to prevent.
|
|
155
|
+
#
|
|
156
|
+
# The JavaScript analyser has always compared the whole specifier, so
|
|
157
|
+
# `outcometick/data` was already refused there. This is the Python half of
|
|
158
|
+
# the same rule.
|
|
159
|
+
if root == "outcometick" and module != "outcometick":
|
|
160
|
+
raise Reject(
|
|
161
|
+
"E_IMPORT",
|
|
162
|
+
f"{name}:{line}: import of {module!r} — only 'outcometick' itself is "
|
|
163
|
+
"available to a strategy; the sandbox has no network and no submodules",
|
|
164
|
+
name,
|
|
165
|
+
line,
|
|
166
|
+
specifier=module,
|
|
167
|
+
)
|
|
148
168
|
if root in ALWAYS_ALLOWED or root in deps:
|
|
149
169
|
return
|
|
150
170
|
raise Reject(
|