briskapi 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- {briskapi-0.3.0 → briskapi-0.3.1}/PKG-INFO +1 -1
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/__init__.py +1 -1
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/decoder/decoder.cjs +79 -13
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/decoder/engineio.cjs +26 -2
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/decoder/sbi.cjs +45 -8
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi.egg-info/PKG-INFO +1 -1
- {briskapi-0.3.0 → briskapi-0.3.1}/pyproject.toml +1 -1
- {briskapi-0.3.0 → briskapi-0.3.1}/LICENSE +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/README.md +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/LICENSE-pybrisk.txt +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/__main__.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/_archive.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/_live.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/_market.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/_recording.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/archive.json +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/cli.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/decoder/assets.json +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/decoder/web.cjs +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/references/historical_mock.json +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/sbi.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/schema.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi/timing.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi.egg-info/SOURCES.txt +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi.egg-info/dependency_links.txt +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi.egg-info/entry_points.txt +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi.egg-info/requires.txt +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/briskapi.egg-info/top_level.txt +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/setup.cfg +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/tests/test_api.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/tests/test_archive.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/tests/test_package.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/tests/test_sbi.py +0 -0
- {briskapi-0.3.0 → briskapi-0.3.1}/tests/test_timing.py +0 -0
|
@@ -18,7 +18,7 @@ from briskapi._live import Feed, connect as _connect, record as _record, stream
|
|
|
18
18
|
from briskapi._market import Market, Ticker
|
|
19
19
|
from briskapi._recording import JST, BriskError, NotFoundError, Recording, Table
|
|
20
20
|
|
|
21
|
-
__version__ = '0.3.
|
|
21
|
+
__version__ = '0.3.1'
|
|
22
22
|
__all__ = ['JST', 'Archive', 'BriskError', 'Feed', 'Market', 'NotFoundError', 'Recording', 'Table', 'Ticker',
|
|
23
23
|
'connect', 'consent', 'current', 'load', 'pull', 'record', 'recordings', 'stream']
|
|
24
24
|
|
|
@@ -52,10 +52,37 @@ function loadAssets(cache) {
|
|
|
52
52
|
return assets;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Position of the stock view's fields, relative to a version's base. Protocol 16000 (the 2021
|
|
56
|
+
// demo, and the vendor client's own parse of it) puts a header word first. 18000 (SBI's current
|
|
57
|
+
// build, as an independent parser of it reads the same view) drops that word, so every index is
|
|
58
|
+
// one lower. The fields and their order are otherwise identical. Any other version is refused
|
|
59
|
+
// rather than read at a guessed offset.
|
|
60
|
+
const STOCK_VIEW_SHIFT = { 16000: 0, 18000: -1 };
|
|
61
|
+
|
|
62
|
+
function stockViewShift(protocolVersion) {
|
|
63
|
+
if (!(protocolVersion in STOCK_VIEW_SHIFT)) {
|
|
64
|
+
throw new Error(`Stock view layout for protocol ${protocolVersion} is not known (known: ${Object.keys(STOCK_VIEW_SHIFT).join(', ')})`);
|
|
65
|
+
}
|
|
66
|
+
return STOCK_VIEW_SHIFT[protocolVersion];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// `words` is the raw _getStockView buffer; `ohlc` the number of OHLC bars ahead of the quote fields.
|
|
70
|
+
function parseStockView(words, ohlc, shift) {
|
|
71
|
+
const a = words.subarray(6 * ohlc + shift);
|
|
72
|
+
return { frame: a[19], max_frame: a[20], source_time_us: u64(a, 17), last_price10: words[1 + shift],
|
|
73
|
+
open_price10: words[2 + shift], bid_price10: a[7], ask_price10: a[8], indicative_price10: a[9],
|
|
74
|
+
indicative_volume: u64(a, 10), indicative_side: a[12], closing_indicative_price10: a[13],
|
|
75
|
+
closing_indicative_volume: u64(a, 14), quote_flag: a[21], quote_side: a[22],
|
|
76
|
+
special_quote_time_us: u64(a, 23), indicative_open_price10: a[32], auction_reference_price10: a[35],
|
|
77
|
+
volume: u64(a, 3) };
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
class Decoder {
|
|
56
81
|
// options.protocolVersion: 16000 for the Next demo (default), 18000 for SBI BRiSK.
|
|
57
|
-
// options.callbacks: { send(Buffer), heartbeat(ns BigInt), frameNumbers(Array), basePrice(
|
|
82
|
+
// options.callbacks: { send(Buffer), heartbeat(ns BigInt), frameNumbers(Array), basePrice(issueIndex),
|
|
58
83
|
// authError(), marketFinished() }, the vendor client's six WASM callbacks.
|
|
84
|
+
// options.boot: { exceptional_sq, base_prices } from the app boot response, applied in the
|
|
85
|
+
// vendor client's order: after the master is loaded, before the snapshot.
|
|
59
86
|
static async create(assets, options = {}) {
|
|
60
87
|
// Isolate the legacy glue's globals and process exception handlers. No UI,
|
|
61
88
|
// network, filesystem access or account state is needed inside this VM.
|
|
@@ -74,8 +101,10 @@ class Decoder {
|
|
|
74
101
|
return new Decoder(wasm, assets, options);
|
|
75
102
|
}
|
|
76
103
|
|
|
77
|
-
constructor(w, assets, { protocolVersion = manifest.protocol_version, callbacks = {} } = {}) {
|
|
104
|
+
constructor(w, assets, { protocolVersion = manifest.protocol_version, callbacks = {}, boot = {} } = {}) {
|
|
78
105
|
this.w = w;
|
|
106
|
+
this.layoutShift = stockViewShift(protocolVersion);
|
|
107
|
+
this.basePriceQueue = new Set();
|
|
79
108
|
this.authError = false;
|
|
80
109
|
this.marketFinished = false;
|
|
81
110
|
this.initialFrames = null;
|
|
@@ -96,7 +125,8 @@ class Decoder {
|
|
|
96
125
|
this.lastHeartbeat = BigInt(low >>> 0) + (BigInt(high >>> 0) << 32n);
|
|
97
126
|
callbacks.heartbeat?.(this.lastHeartbeat);
|
|
98
127
|
}, 'viii'),
|
|
99
|
-
|
|
128
|
+
// basePrice(id, issueIndex): the decoder changed an issue's base price or limits.
|
|
129
|
+
add((id, issueIndex) => { this.basePriceQueue.add(issueIndex); callbacks.basePrice?.(issueIndex); }, 'vii'),
|
|
100
130
|
add(() => { this.authError = true; callbacks.authError?.(); }, 'vii'),
|
|
101
131
|
add(() => { this.marketFinished = true; callbacks.marketFinished?.(); }, 'vi'),
|
|
102
132
|
protocolVersion);
|
|
@@ -114,10 +144,54 @@ class Decoder {
|
|
|
114
144
|
name: Buffer.from(w.HEAPU8.subarray(this.buf + 144 * issue_id + 68,
|
|
115
145
|
this.buf + 144 * issue_id + 68 + a[16])).toString('utf8') };
|
|
116
146
|
});
|
|
147
|
+
this.boot = this.applyBoot(boot);
|
|
117
148
|
this.unserialize(assets['snapshot.dat']);
|
|
118
149
|
this.date = String(w._getDate(this.id));
|
|
119
150
|
}
|
|
120
151
|
|
|
152
|
+
// The vendor client pushes the boot response's exceptional special quotes and base-price
|
|
153
|
+
// changes into the decoder right after the master, then re-reads the master's limits.
|
|
154
|
+
applyBoot({ exceptional_sq = [], base_prices = [] } = {}) {
|
|
155
|
+
const byCode = new Map(this.master.map(m => [m.code, m.issue_id]));
|
|
156
|
+
const number = (value, what) => {
|
|
157
|
+
if (!Number.isFinite(value)) throw new Error(`Boot ${what} is not a number: ${JSON.stringify(value)}`);
|
|
158
|
+
return value;
|
|
159
|
+
};
|
|
160
|
+
for (const sq of exceptional_sq) {
|
|
161
|
+
if (!/^\d+$/.test(String(sq.issue_code))) {
|
|
162
|
+
throw new Error(`Boot exceptional_sq names issue code ${sq.issue_code}; only numeric codes are supported`);
|
|
163
|
+
}
|
|
164
|
+
this.w._pushSQ(this.id, Number(sq.issue_code), sq.buy_sell === 'B' ? 0 : 1, 10 * number(sq.jump_range, 'jump_range'),
|
|
165
|
+
number(sq.jump_sec, 'jump_sec'), 10 * number(sq.quote_limit_down, 'quote_limit_down'),
|
|
166
|
+
10 * number(sq.quote_limit_up, 'quote_limit_up'));
|
|
167
|
+
}
|
|
168
|
+
for (const change of base_prices) {
|
|
169
|
+
const issue = byCode.get(String(change.issue_code));
|
|
170
|
+
if (issue === undefined) throw new Error(`Boot base_prices names issue code ${change.issue_code}, which is not in the master`);
|
|
171
|
+
this.w._pushBasePrice(this.id, issue, number(change.base_price10, 'base_price10'),
|
|
172
|
+
number(change.limit_down10, 'limit_down10'), number(change.limit_up10, 'limit_up10'));
|
|
173
|
+
this.basePriceQueue.add(issue);
|
|
174
|
+
}
|
|
175
|
+
return { exceptional_sq: exceptional_sq.length, base_prices: base_prices.length, applied: this.applyBasePrice() };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Re-read the master's price limits for issues whose base price changed (the vendor client
|
|
179
|
+
// does this after boot and after every frame); cheap when nothing is queued.
|
|
180
|
+
applyBasePrice() {
|
|
181
|
+
if (!this.basePriceQueue.size) return 0;
|
|
182
|
+
this.w._getStockMaster(this.id, this.buf, this.master.length * 144);
|
|
183
|
+
let applied = 0;
|
|
184
|
+
for (const issue of this.basePriceQueue) {
|
|
185
|
+
const m = this.master[issue];
|
|
186
|
+
if (!m) throw new Error(`Base price change for unknown issue index ${issue}`);
|
|
187
|
+
const a = new Uint32Array(this.w.HEAPU8.buffer, this.buf + 144 * issue, 36);
|
|
188
|
+
m.base_price10 = a[2]; m.limit_up10 = a[3]; m.limit_down10 = a[4];
|
|
189
|
+
applied++;
|
|
190
|
+
}
|
|
191
|
+
this.basePriceQueue.clear();
|
|
192
|
+
return applied;
|
|
193
|
+
}
|
|
194
|
+
|
|
121
195
|
push(data) {
|
|
122
196
|
for (let offset = 0; offset < data.length; offset += BUFFER) {
|
|
123
197
|
const part = data.subarray(offset, offset + BUFFER);
|
|
@@ -214,15 +288,7 @@ class Decoder {
|
|
|
214
288
|
if (!m) throw new Error(`Unknown issue ID: ${issue_id}`);
|
|
215
289
|
if (!this.w._getStockView(this.id, issue_id, this.buf)) throw new Error(`Stock view unavailable: ${m.code}`);
|
|
216
290
|
const words = new Uint32Array(this.w.HEAPU8.buffer, this.buf, this.viewWords);
|
|
217
|
-
const
|
|
218
|
-
const quote = { issue_id, code: m.code, frame: a[19], max_frame: a[20],
|
|
219
|
-
source_time_us: u64(a, 17), last_price10: words[1], open_price10: words[2],
|
|
220
|
-
bid_price10: a[7], ask_price10: a[8], indicative_price10: a[9],
|
|
221
|
-
indicative_volume: u64(a, 10), indicative_side: a[12],
|
|
222
|
-
closing_indicative_price10: a[13], closing_indicative_volume: u64(a, 14),
|
|
223
|
-
quote_flag: a[21], quote_side: a[22], special_quote_time_us: u64(a, 23),
|
|
224
|
-
indicative_open_price10: a[32], auction_reference_price10: a[35],
|
|
225
|
-
volume: u64(a, 3) };
|
|
291
|
+
const quote = { issue_id, code: m.code, ...parseStockView(words, this.ohlc, this.layoutShift) };
|
|
226
292
|
// SBI's build has no portfolio export, so its quotes carry no issue_status.
|
|
227
293
|
if (this.w._getPortfolio) {
|
|
228
294
|
if (!this.w._getPortfolio(this.id, Number(m.code), this.buf)) throw new Error(`Portfolio unavailable: ${m.code}`);
|
|
@@ -315,7 +381,7 @@ async function main(argv) {
|
|
|
315
381
|
} });
|
|
316
382
|
}
|
|
317
383
|
|
|
318
|
-
module.exports = { Decoder, QuoteHistory, frames, u64, loadAssets, replay, main };
|
|
384
|
+
module.exports = { Decoder, QuoteHistory, frames, u64, loadAssets, replay, main, parseStockView, stockViewShift, STOCK_VIEW_SHIFT };
|
|
319
385
|
if (require.main === module) main(process.argv.slice(2)).catch(error => {
|
|
320
386
|
console.error(error.message); process.exitCode = 1;
|
|
321
387
|
});
|
|
@@ -248,14 +248,38 @@ class Link {
|
|
|
248
248
|
|
|
249
249
|
// The vendor client closes the connection when its last heartbeat is older than 7 s, but
|
|
250
250
|
// only once it has seen one. Beats come from the WASM's heartbeat callback.
|
|
251
|
-
function heartbeatMonitor({ intervalMs = 7000, checkEveryMs = 1000, onFail, now = () => performance.now() }) {
|
|
251
|
+
function heartbeatMonitor({ intervalMs = 7000, checkEveryMs = 1000, onFail, now = () => performance.now(), staleCheck = null }) {
|
|
252
252
|
let last = null;
|
|
253
253
|
const timer = setInterval(() => {
|
|
254
254
|
if (last !== null && now() - last > intervalMs) {
|
|
255
255
|
onFail(new Error(`SBI BRiSK connection check failure: no heartbeat for ${Math.round((now() - last) / 1000)}s`));
|
|
256
256
|
}
|
|
257
|
+
const stale = staleCheck?.();
|
|
258
|
+
if (stale) onFail(stale);
|
|
257
259
|
}, checkEveryMs);
|
|
258
260
|
return { beat() { last = now(); }, stop() { clearInterval(timer); } };
|
|
259
261
|
}
|
|
260
262
|
|
|
261
|
-
|
|
263
|
+
// The vendor client's second liveness rule: during trading hours (08:00-15:00 JST, not the
|
|
264
|
+
// 11:30-12:10 break) the decoder's data time must not lag the server clock by more than 90 s.
|
|
265
|
+
// The server clock comes from the heartbeat; nothing is checked before the first one, or on a
|
|
266
|
+
// day other than the market date.
|
|
267
|
+
function staleDataCheck({ date, thresholdUs = 90_000_000, now = Date.now }) {
|
|
268
|
+
let server = null;
|
|
269
|
+
return {
|
|
270
|
+
beat(ns) { server = { serverMs: Number(ns / 1_000_000n), localMs: now() }; },
|
|
271
|
+
check(decoderTimeUs) {
|
|
272
|
+
if (!server || !decoderTimeUs) return null;
|
|
273
|
+
const jst = new Date(server.serverMs + (now() - server.localMs) + 9 * 3600e3);
|
|
274
|
+
if (jst.toISOString().slice(0, 10) !== date) return null;
|
|
275
|
+
const hour = jst.getUTCHours(), minute = jst.getUTCMinutes();
|
|
276
|
+
if (hour >= 15 || hour <= 7 || (hour === 11 && minute >= 30) || (hour === 12 && minute < 10)) return null;
|
|
277
|
+
const lag = 1e6 * (60 * (60 * hour + minute) + jst.getUTCSeconds()) - decoderTimeUs;
|
|
278
|
+
return lag > thresholdUs
|
|
279
|
+
? new Error(`SBI BRiSK data is stale: the decoder's clock is ${Math.round(lag / 1e6)}s behind the server (limit ${thresholdUs / 1e6}s)`)
|
|
280
|
+
: null;
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = { Link, SBI_PROFILE, resolveProfile, heartbeatMonitor, staleDataCheck, redact };
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
const crypto = require('node:crypto');
|
|
14
14
|
const { once } = require('node:events');
|
|
15
15
|
const { Decoder } = require('./decoder.cjs');
|
|
16
|
-
const { Link, resolveProfile, heartbeatMonitor, redact } = require('./engineio.cjs');
|
|
16
|
+
const { Link, resolveProfile, heartbeatMonitor, staleDataCheck, redact } = require('./engineio.cjs');
|
|
17
17
|
|
|
18
18
|
const ORIGIN = 'https://sbi.brisk.jp';
|
|
19
19
|
const PROTOCOL_VERSION = 18000;
|
|
@@ -113,21 +113,51 @@ async function decoderAssets(session) {
|
|
|
113
113
|
throw new Error('SBI BRiSK decoder not found in the app bundles; the site layout may have changed');
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// The master's record layout also depends on the protocol version, and SBI's is not independently
|
|
117
|
+
// confirmed. A wrong layout would read tick types as codes, limits as lot sizes and so on, so
|
|
118
|
+
// refuse a master that does not look like a market. (The 2021 demo's passes every test at 99.98%.)
|
|
119
|
+
function checkMaster(master, protocolVersion) {
|
|
120
|
+
const fraction = predicate => master.filter(predicate).length / (master.length || 1);
|
|
121
|
+
const problems = [];
|
|
122
|
+
if (master.length < 1000) problems.push(`only ${master.length} issues`);
|
|
123
|
+
const rules = [
|
|
124
|
+
['numeric codes', 0.95, m => /^\d{3,4}$/.test(m.code)],
|
|
125
|
+
['positive base prices', 0.98, m => m.base_price10 > 0],
|
|
126
|
+
['limits around the base price', 0.98, m => m.limit_up10 > m.base_price10 && m.base_price10 > m.limit_down10],
|
|
127
|
+
['lot sizes of 1 to 1,000,000', 0.98, m => m.lot_size >= 1 && m.lot_size <= 1e6],
|
|
128
|
+
['names', 0.95, m => m.name.length > 0],
|
|
129
|
+
];
|
|
130
|
+
for (const [what, minimum, predicate] of rules) {
|
|
131
|
+
const seen = fraction(predicate);
|
|
132
|
+
if (seen < minimum) problems.push(`${what}: ${(100 * seen).toFixed(1)}% (need ${(100 * minimum).toFixed(0)}%)`);
|
|
133
|
+
}
|
|
134
|
+
const distinct = new Set(master.map(m => m.code)).size / (master.length || 1);
|
|
135
|
+
if (distinct < 0.99) problems.push(`distinct codes: ${(100 * distinct).toFixed(1)}% (need 99%)`);
|
|
136
|
+
if (problems.length) {
|
|
137
|
+
throw new Error(`SBI BRiSK stock master layout not recognised for protocol ${protocolVersion}: ${problems.join('; ')}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
116
141
|
function checkAbi(wasm) {
|
|
117
142
|
const missing = REQUIRED.filter(name => typeof wasm[name] !== 'function');
|
|
118
143
|
if (missing.length) throw new Error(`SBI BRiSK decoder changed; missing ${missing.join(', ')}`);
|
|
119
144
|
}
|
|
120
145
|
|
|
121
146
|
async function live({ cookies, codes = [], emit, fetchImpl = fetch, WebSocketImpl = WebSocket, startTimeoutMs = 60000,
|
|
122
|
-
protocolVersion
|
|
123
|
-
connectTimeoutMs = 15000, catchUpBackoffMs }) {
|
|
147
|
+
protocolVersion, profile: overrides = {}, trace = null, catchUp = null, heartbeatMs = 7000,
|
|
148
|
+
staleSeconds = 90, connectTimeoutMs = 15000, catchUpBackoffMs }) {
|
|
124
149
|
const profile = resolveProfile(overrides);
|
|
125
150
|
const session = new Session(cookies, fetchImpl);
|
|
126
151
|
const began = performance.now();
|
|
127
152
|
const frontend = await session.get('/api/frontend/boot');
|
|
128
153
|
session.token = frontend.api_token;
|
|
129
154
|
const app = await session.get('/api/app/boot');
|
|
130
|
-
|
|
155
|
+
// The server says which decoder protocol it speaks (the vendor client passes it to the WASM and
|
|
156
|
+
// falls back to 16000); an explicit option wins, then the boot response, then SBI's 18000.
|
|
157
|
+
protocolVersion ??= app.flex_version ?? PROTOCOL_VERSION;
|
|
158
|
+
trace?.(`boot: date ${app.date}, series ${app.series}, flex_version ${protocolVersion}, `
|
|
159
|
+
+ `${(app.base_prices || []).length} base prices, ${(app.exceptional_sq || []).length} exceptional special quotes, `
|
|
160
|
+
+ `ws_url ${redact(app.ws_url)}`);
|
|
131
161
|
const { assets, paths } = await decoderAssets(session);
|
|
132
162
|
assets['master.dat'] = await session.get(`/api/master/${encodeURIComponent(app.master)}`, 'bytes');
|
|
133
163
|
assets['snapshot.dat'] = await session.get(`/api/snapshot/${encodeURIComponent(app.snapshot)}`, 'bytes');
|
|
@@ -141,18 +171,22 @@ async function live({ cookies, codes = [], emit, fetchImpl = fetch, WebSocketImp
|
|
|
141
171
|
settle({ code: -1, reason: '' });
|
|
142
172
|
};
|
|
143
173
|
let monitor = null, timer = null; // started with the link and always stopped, so a failure cannot leave the process hanging
|
|
144
|
-
const
|
|
174
|
+
const stale = staleSeconds ? staleDataCheck({ date: app.date, thresholdUs: staleSeconds * 1e6 }) : null;
|
|
175
|
+
const decoder = await Decoder.create(assets, { protocolVersion, check: checkAbi,
|
|
176
|
+
boot: { exceptional_sq: app.exceptional_sq, base_prices: app.base_prices }, callbacks: {
|
|
145
177
|
send: bytes => { if (link) link.send(bytes); },
|
|
146
|
-
heartbeat:
|
|
178
|
+
heartbeat: ns => { monitor?.beat(); stale?.beat(ns); },
|
|
147
179
|
authError: () => fail(new Error('SBI BRiSK reports another WebSocket session for this user (only one is allowed); close the other one')),
|
|
148
180
|
marketFinished: () => { marketFinished = true; },
|
|
149
181
|
} });
|
|
182
|
+
checkMaster(decoder.master, protocolVersion);
|
|
150
183
|
const selected = new Set(codes);
|
|
151
184
|
const master = decoder.master.filter(m => !selected.size || selected.has(m.code));
|
|
152
185
|
const missing = codes.filter(code => !master.some(m => m.code === code));
|
|
153
186
|
if (missing.length) throw new Error(`Codes not in the SBI master: ${missing.join(',')}`);
|
|
154
187
|
const ids = new Set(master.map(m => m.issue_id));
|
|
155
188
|
const input_transport = { kind: 'sbi_websocket', origin: ORIGIN + '/', series: app.series,
|
|
189
|
+
protocol_version: protocolVersion, boot_applied: decoder.boot,
|
|
156
190
|
decoder: paths, decoder_sha256: crypto.createHash('sha256').update(assets['fita.wasm']).digest('hex'),
|
|
157
191
|
setup_ms: performance.now() - began };
|
|
158
192
|
|
|
@@ -209,6 +243,7 @@ async function live({ cookies, codes = [], emit, fetchImpl = fetch, WebSocketImp
|
|
|
209
243
|
const handle = async (frame, received_unix_ms) => {
|
|
210
244
|
const t0 = process.hrtime.bigint();
|
|
211
245
|
decoder.feed(frame);
|
|
246
|
+
decoder.applyBasePrice();
|
|
212
247
|
const quotes = decoder.changed().filter(id => ids.has(id)).map(id => decoder.quote(id));
|
|
213
248
|
updates += quotes.length;
|
|
214
249
|
await emit({ type: 'quotes', seq: seq++, source_time_us: decoder.time(), received_unix_ms,
|
|
@@ -224,12 +259,14 @@ async function live({ cookies, codes = [], emit, fetchImpl = fetch, WebSocketImp
|
|
|
224
259
|
}
|
|
225
260
|
try {
|
|
226
261
|
decoder.feed(frame);
|
|
262
|
+
decoder.applyBasePrice();
|
|
227
263
|
if (!starting && decoder.initialFrames) { starting = true; begin(); }
|
|
228
264
|
} catch (error) { fail(error); }
|
|
229
265
|
};
|
|
230
266
|
|
|
231
267
|
try {
|
|
232
|
-
monitor = heartbeatMonitor({ intervalMs: heartbeatMs, checkEveryMs: Math.min(1000, heartbeatMs / 4), onFail: fail
|
|
268
|
+
monitor = heartbeatMonitor({ intervalMs: heartbeatMs, checkEveryMs: Math.min(1000, heartbeatMs / 4), onFail: fail,
|
|
269
|
+
staleCheck: stale && (() => stale.check(decoder.time())) });
|
|
233
270
|
timer = setTimeout(() => fail(new Error(`SBI BRiSK stream did not initialize within ${startTimeoutMs / 1000}s; `
|
|
234
271
|
+ 'no first frame numbers arrived (run with --trace-protocol to see what the server sent)')), startTimeoutMs);
|
|
235
272
|
link = new Link({ WebSocketImpl, url: new URL(app.ws_url, ORIGIN.replace('https:', 'wss:')),
|
|
@@ -281,7 +318,7 @@ async function main(argv, env = process.env) {
|
|
|
281
318
|
} });
|
|
282
319
|
}
|
|
283
320
|
|
|
284
|
-
module.exports = { Session, decoderAssets, checkAbi, live, main, fetchCatchUp, CATCH_UP_FORMATS, PROTOCOL_VERSION, REQUIRED };
|
|
321
|
+
module.exports = { Session, decoderAssets, checkAbi, checkMaster, live, main, fetchCatchUp, CATCH_UP_FORMATS, PROTOCOL_VERSION, REQUIRED };
|
|
285
322
|
if (require.main === module) main(process.argv.slice(2)).catch(error => {
|
|
286
323
|
console.error(error.message); process.exitCode = 1;
|
|
287
324
|
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|