@fullstackcraftllc/floe 0.0.18 → 0.0.19
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 +6 -11
- package/dist/apiclient/index.d.ts +204 -0
- package/dist/apiclient/index.js +721 -0
- package/dist/client/FloeClient.d.ts +4 -0
- package/dist/client/FloeClient.js +50 -0
- package/dist/client/brokers/WebullClient.d.ts +53 -0
- package/dist/client/brokers/WebullClient.js +290 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -2
- package/package.json +1 -3
|
@@ -12,6 +12,8 @@ export declare enum Broker {
|
|
|
12
12
|
TASTYTRADE = "tastytrade",
|
|
13
13
|
/** TradeStation brokerage API (uses HTTP streaming) */
|
|
14
14
|
TRADESTATION = "tradestation",
|
|
15
|
+
/** Webull brokerage API (uses proxied HTTP snapshots) */
|
|
16
|
+
WEBULL = "webull",
|
|
15
17
|
/** Charles Schwab brokerage API (uses WebSocket streaming) */
|
|
16
18
|
SCHWAB = "schwab",
|
|
17
19
|
/** Interactive Brokers Web API (OAuth) */
|
|
@@ -89,6 +91,8 @@ export declare class FloeClient {
|
|
|
89
91
|
private schwabClient;
|
|
90
92
|
/** Interactive Brokers broker client instance */
|
|
91
93
|
private ibkrClient;
|
|
94
|
+
/** Webull broker client instance */
|
|
95
|
+
private webullClient;
|
|
92
96
|
/** Event listeners registry for the EventEmitter pattern */
|
|
93
97
|
private eventListeners;
|
|
94
98
|
/** Callback for ticker data changes (legacy callback pattern) */
|
|
@@ -6,6 +6,7 @@ const TastyTradeClient_1 = require("./brokers/TastyTradeClient");
|
|
|
6
6
|
const TradeStationClient_1 = require("./brokers/TradeStationClient");
|
|
7
7
|
const SchwabClient_1 = require("./brokers/SchwabClient");
|
|
8
8
|
const IBKRClient_1 = require("./brokers/IBKRClient");
|
|
9
|
+
const WebullClient_1 = require("./brokers/WebullClient");
|
|
9
10
|
/**
|
|
10
11
|
* Supported broker integrations for the FloeClient.
|
|
11
12
|
* @enum {string}
|
|
@@ -20,6 +21,8 @@ var Broker;
|
|
|
20
21
|
Broker["TASTYTRADE"] = "tastytrade";
|
|
21
22
|
/** TradeStation brokerage API (uses HTTP streaming) */
|
|
22
23
|
Broker["TRADESTATION"] = "tradestation";
|
|
24
|
+
/** Webull brokerage API (uses proxied HTTP snapshots) */
|
|
25
|
+
Broker["WEBULL"] = "webull";
|
|
23
26
|
/** Charles Schwab brokerage API (uses WebSocket streaming) */
|
|
24
27
|
Broker["SCHWAB"] = "schwab";
|
|
25
28
|
/** Interactive Brokers Web API (OAuth) */
|
|
@@ -93,6 +96,8 @@ class FloeClient {
|
|
|
93
96
|
this.schwabClient = null;
|
|
94
97
|
/** Interactive Brokers broker client instance */
|
|
95
98
|
this.ibkrClient = null;
|
|
99
|
+
/** Webull broker client instance */
|
|
100
|
+
this.webullClient = null;
|
|
96
101
|
/** Event listeners registry for the EventEmitter pattern */
|
|
97
102
|
this.eventListeners = new Map();
|
|
98
103
|
/** Callback for ticker data changes (legacy callback pattern) */
|
|
@@ -193,6 +198,25 @@ class FloeClient {
|
|
|
193
198
|
// Connect to the streaming API
|
|
194
199
|
await this.tradeStationClient.connect();
|
|
195
200
|
break;
|
|
201
|
+
case Broker.WEBULL:
|
|
202
|
+
this.webullClient = new WebullClient_1.WebullClient({
|
|
203
|
+
authToken,
|
|
204
|
+
verbose: this.verbose,
|
|
205
|
+
});
|
|
206
|
+
this.webullClient.on('tickerUpdate', (ticker) => {
|
|
207
|
+
this.emit('tickerUpdate', ticker);
|
|
208
|
+
});
|
|
209
|
+
this.webullClient.on('optionUpdate', (option) => {
|
|
210
|
+
this.emit('optionUpdate', option);
|
|
211
|
+
});
|
|
212
|
+
this.webullClient.on('error', (error) => {
|
|
213
|
+
this.emit('error', error);
|
|
214
|
+
});
|
|
215
|
+
this.webullClient.on('disconnected', () => {
|
|
216
|
+
this.emit('disconnected', { broker, reason: 'Webull market-data polling stopped' });
|
|
217
|
+
});
|
|
218
|
+
await this.webullClient.connect();
|
|
219
|
+
break;
|
|
196
220
|
case Broker.SCHWAB:
|
|
197
221
|
// For Schwab, authToken is the OAuth access token
|
|
198
222
|
this.schwabClient = new SchwabClient_1.SchwabClient({
|
|
@@ -275,6 +299,10 @@ class FloeClient {
|
|
|
275
299
|
this.ibkrClient.disconnect();
|
|
276
300
|
this.ibkrClient = null;
|
|
277
301
|
}
|
|
302
|
+
if (this.webullClient) {
|
|
303
|
+
this.webullClient.disconnect();
|
|
304
|
+
this.webullClient = null;
|
|
305
|
+
}
|
|
278
306
|
const broker = this.currentBroker;
|
|
279
307
|
this.currentBroker = null;
|
|
280
308
|
this.currentSubscribedTickers = [];
|
|
@@ -311,6 +339,9 @@ class FloeClient {
|
|
|
311
339
|
case Broker.TRADESTATION:
|
|
312
340
|
this.tradeStationClient?.subscribe(tickers);
|
|
313
341
|
break;
|
|
342
|
+
case Broker.WEBULL:
|
|
343
|
+
this.webullClient?.subscribe(tickers);
|
|
344
|
+
break;
|
|
314
345
|
case Broker.SCHWAB:
|
|
315
346
|
this.schwabClient?.subscribe(tickers);
|
|
316
347
|
break;
|
|
@@ -357,6 +388,9 @@ class FloeClient {
|
|
|
357
388
|
case Broker.TRADESTATION:
|
|
358
389
|
this.tradeStationClient?.subscribe(symbols);
|
|
359
390
|
break;
|
|
391
|
+
case Broker.WEBULL:
|
|
392
|
+
this.webullClient?.subscribe(symbols);
|
|
393
|
+
break;
|
|
360
394
|
case Broker.SCHWAB:
|
|
361
395
|
this.schwabClient?.subscribe(symbols);
|
|
362
396
|
break;
|
|
@@ -395,6 +429,9 @@ class FloeClient {
|
|
|
395
429
|
case Broker.TRADESTATION:
|
|
396
430
|
this.tradeStationClient?.unsubscribe(tickers);
|
|
397
431
|
break;
|
|
432
|
+
case Broker.WEBULL:
|
|
433
|
+
this.webullClient?.unsubscribe(tickers);
|
|
434
|
+
break;
|
|
398
435
|
case Broker.SCHWAB:
|
|
399
436
|
this.schwabClient?.unsubscribe(tickers);
|
|
400
437
|
break;
|
|
@@ -433,6 +470,9 @@ class FloeClient {
|
|
|
433
470
|
case Broker.TRADESTATION:
|
|
434
471
|
this.tradeStationClient?.unsubscribe(symbols);
|
|
435
472
|
break;
|
|
473
|
+
case Broker.WEBULL:
|
|
474
|
+
this.webullClient?.unsubscribe(symbols);
|
|
475
|
+
break;
|
|
436
476
|
case Broker.SCHWAB:
|
|
437
477
|
this.schwabClient?.unsubscribe(symbols);
|
|
438
478
|
break;
|
|
@@ -470,6 +510,9 @@ class FloeClient {
|
|
|
470
510
|
case Broker.TRADESTATION:
|
|
471
511
|
this.tradeStationClient?.unsubscribeFromAll();
|
|
472
512
|
break;
|
|
513
|
+
case Broker.WEBULL:
|
|
514
|
+
this.webullClient?.unsubscribeFromAll();
|
|
515
|
+
break;
|
|
473
516
|
case Broker.SCHWAB:
|
|
474
517
|
this.schwabClient?.unsubscribeFromAll();
|
|
475
518
|
break;
|
|
@@ -527,6 +570,9 @@ class FloeClient {
|
|
|
527
570
|
// TradeStation provides open interest via stream, no separate fetch needed
|
|
528
571
|
// OI is automatically populated when streaming option chains
|
|
529
572
|
break;
|
|
573
|
+
case Broker.WEBULL:
|
|
574
|
+
await this.webullClient?.fetchOpenInterest(symbolsToFetch);
|
|
575
|
+
break;
|
|
530
576
|
case Broker.SCHWAB:
|
|
531
577
|
await this.schwabClient?.fetchOpenInterest(symbolsToFetch);
|
|
532
578
|
break;
|
|
@@ -557,6 +603,8 @@ class FloeClient {
|
|
|
557
603
|
return this.tastyTradeClient?.getOption(occSymbol);
|
|
558
604
|
case Broker.TRADESTATION:
|
|
559
605
|
return this.tradeStationClient?.getOption(occSymbol);
|
|
606
|
+
case Broker.WEBULL:
|
|
607
|
+
return this.webullClient?.getOption(occSymbol);
|
|
560
608
|
case Broker.SCHWAB:
|
|
561
609
|
return this.schwabClient?.getOption(occSymbol);
|
|
562
610
|
case Broker.IBKR:
|
|
@@ -586,6 +634,8 @@ class FloeClient {
|
|
|
586
634
|
return this.tastyTradeClient?.getAllOptions() ?? new Map();
|
|
587
635
|
case Broker.TRADESTATION:
|
|
588
636
|
return this.tradeStationClient?.getAllOptions() ?? new Map();
|
|
637
|
+
case Broker.WEBULL:
|
|
638
|
+
return this.webullClient?.getAllOptions() ?? new Map();
|
|
589
639
|
case Broker.SCHWAB:
|
|
590
640
|
return this.schwabClient?.getAllOptions() ?? new Map();
|
|
591
641
|
case Broker.IBKR:
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { BaseBrokerClient, BaseBrokerClientOptions } from './BaseBrokerClient';
|
|
2
|
+
export interface WebullAuthTokenConfig {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
proxyUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ParsedWebullAuthToken {
|
|
7
|
+
accessToken: string;
|
|
8
|
+
proxyUrl: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createWebullAuthToken(config: WebullAuthTokenConfig): string;
|
|
11
|
+
export declare function parseWebullAuthToken(authToken: string): ParsedWebullAuthToken;
|
|
12
|
+
export interface WebullClientOptions extends BaseBrokerClientOptions {
|
|
13
|
+
authToken: string;
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
optionRequestIntervalMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class WebullClient extends BaseBrokerClient {
|
|
18
|
+
protected readonly brokerName = "Webull";
|
|
19
|
+
private readonly accessToken;
|
|
20
|
+
private readonly proxyUrl;
|
|
21
|
+
private readonly pollIntervalMs;
|
|
22
|
+
private readonly optionRequestIntervalMs;
|
|
23
|
+
private connected;
|
|
24
|
+
private tickerTimer;
|
|
25
|
+
private optionTimer;
|
|
26
|
+
private tickerRequestInFlight;
|
|
27
|
+
private optionRequestChain;
|
|
28
|
+
private lastOptionRequestAt;
|
|
29
|
+
private optionChunkIndex;
|
|
30
|
+
private pendingOptionRequests;
|
|
31
|
+
constructor(options: WebullClientOptions);
|
|
32
|
+
connect(): Promise<void>;
|
|
33
|
+
disconnect(): void;
|
|
34
|
+
subscribe(symbols: string[]): void;
|
|
35
|
+
unsubscribe(symbols: string[]): void;
|
|
36
|
+
unsubscribeFromAll(): void;
|
|
37
|
+
isConnected(): boolean;
|
|
38
|
+
fetchOpenInterest(occSymbols: string[]): Promise<void>;
|
|
39
|
+
private startPolling;
|
|
40
|
+
private stopPolling;
|
|
41
|
+
private getSubscribedTickers;
|
|
42
|
+
private getSubscribedOptions;
|
|
43
|
+
private pollTickers;
|
|
44
|
+
private pollNextOptionChunk;
|
|
45
|
+
private enqueueOptionSnapshot;
|
|
46
|
+
private requestSnapshots;
|
|
47
|
+
private updateTicker;
|
|
48
|
+
private updateOption;
|
|
49
|
+
private buildGreeks;
|
|
50
|
+
private toTimestamp;
|
|
51
|
+
private isWebullOptionSymbol;
|
|
52
|
+
private chunk;
|
|
53
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebullClient = void 0;
|
|
4
|
+
exports.createWebullAuthToken = createWebullAuthToken;
|
|
5
|
+
exports.parseWebullAuthToken = parseWebullAuthToken;
|
|
6
|
+
const occ_1 = require("../../utils/occ");
|
|
7
|
+
const BaseBrokerClient_1 = require("./BaseBrokerClient");
|
|
8
|
+
const DEFAULT_PROXY_URL = '/.netlify/functions/webullMarketData';
|
|
9
|
+
const AUTH_TOKEN_PREFIX = 'floe-webull:';
|
|
10
|
+
const OPTION_BATCH_SIZE = 20;
|
|
11
|
+
function createWebullAuthToken(config) {
|
|
12
|
+
if (!config.accessToken) {
|
|
13
|
+
throw new Error('Webull access token is required');
|
|
14
|
+
}
|
|
15
|
+
return `${AUTH_TOKEN_PREFIX}${encodeURIComponent(JSON.stringify({
|
|
16
|
+
accessToken: config.accessToken,
|
|
17
|
+
proxyUrl: config.proxyUrl ?? DEFAULT_PROXY_URL,
|
|
18
|
+
}))}`;
|
|
19
|
+
}
|
|
20
|
+
function parseWebullAuthToken(authToken) {
|
|
21
|
+
if (!authToken) {
|
|
22
|
+
throw new Error('Webull auth token is required');
|
|
23
|
+
}
|
|
24
|
+
if (!authToken.startsWith(AUTH_TOKEN_PREFIX)) {
|
|
25
|
+
return { accessToken: authToken, proxyUrl: DEFAULT_PROXY_URL };
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(decodeURIComponent(authToken.slice(AUTH_TOKEN_PREFIX.length)));
|
|
29
|
+
if (!parsed.accessToken) {
|
|
30
|
+
throw new Error('missing accessToken');
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
accessToken: parsed.accessToken,
|
|
34
|
+
proxyUrl: parsed.proxyUrl || DEFAULT_PROXY_URL,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new Error(`Invalid Webull auth token: ${error instanceof Error ? error.message : String(error)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
class WebullClient extends BaseBrokerClient_1.BaseBrokerClient {
|
|
42
|
+
constructor(options) {
|
|
43
|
+
super(options);
|
|
44
|
+
this.brokerName = 'Webull';
|
|
45
|
+
this.connected = false;
|
|
46
|
+
this.tickerTimer = null;
|
|
47
|
+
this.optionTimer = null;
|
|
48
|
+
this.tickerRequestInFlight = false;
|
|
49
|
+
this.optionRequestChain = Promise.resolve();
|
|
50
|
+
this.lastOptionRequestAt = 0;
|
|
51
|
+
this.optionChunkIndex = 0;
|
|
52
|
+
this.pendingOptionRequests = 0;
|
|
53
|
+
const auth = parseWebullAuthToken(options.authToken);
|
|
54
|
+
this.accessToken = auth.accessToken;
|
|
55
|
+
this.proxyUrl = auth.proxyUrl;
|
|
56
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 1000;
|
|
57
|
+
this.optionRequestIntervalMs = options.optionRequestIntervalMs ?? 1000;
|
|
58
|
+
}
|
|
59
|
+
async connect() {
|
|
60
|
+
if (this.connected)
|
|
61
|
+
return;
|
|
62
|
+
this.connected = true;
|
|
63
|
+
this.emit('connected', undefined);
|
|
64
|
+
this.startPolling();
|
|
65
|
+
this.log('Connected through market-data proxy');
|
|
66
|
+
}
|
|
67
|
+
disconnect() {
|
|
68
|
+
if (!this.connected)
|
|
69
|
+
return;
|
|
70
|
+
this.connected = false;
|
|
71
|
+
this.stopPolling();
|
|
72
|
+
this.subscribedSymbols.clear();
|
|
73
|
+
this.emit('disconnected', undefined);
|
|
74
|
+
}
|
|
75
|
+
subscribe(symbols) {
|
|
76
|
+
for (const symbol of symbols) {
|
|
77
|
+
const normalized = symbol.replace(/\s+/g, '').toUpperCase();
|
|
78
|
+
if (normalized)
|
|
79
|
+
this.subscribedSymbols.add(normalized);
|
|
80
|
+
}
|
|
81
|
+
if (symbols.some((symbol) => !this.isWebullOptionSymbol(symbol))) {
|
|
82
|
+
void this.pollTickers();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
unsubscribe(symbols) {
|
|
86
|
+
for (const symbol of symbols) {
|
|
87
|
+
this.subscribedSymbols.delete(symbol.replace(/\s+/g, '').toUpperCase());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
unsubscribeFromAll() {
|
|
91
|
+
this.subscribedSymbols.clear();
|
|
92
|
+
this.optionChunkIndex = 0;
|
|
93
|
+
}
|
|
94
|
+
isConnected() {
|
|
95
|
+
return this.connected;
|
|
96
|
+
}
|
|
97
|
+
async fetchOpenInterest(occSymbols) {
|
|
98
|
+
const options = Array.from(new Set(occSymbols
|
|
99
|
+
.map((symbol) => symbol.replace(/\s+/g, '').toUpperCase())
|
|
100
|
+
.filter((symbol) => this.isWebullOptionSymbol(symbol))));
|
|
101
|
+
const requests = [];
|
|
102
|
+
for (let index = 0; index < options.length; index += OPTION_BATCH_SIZE) {
|
|
103
|
+
requests.push(this.enqueueOptionSnapshot(options.slice(index, index + OPTION_BATCH_SIZE)));
|
|
104
|
+
}
|
|
105
|
+
await Promise.all(requests);
|
|
106
|
+
}
|
|
107
|
+
startPolling() {
|
|
108
|
+
if (!this.tickerTimer) {
|
|
109
|
+
this.tickerTimer = setInterval(() => void this.pollTickers(), this.pollIntervalMs);
|
|
110
|
+
}
|
|
111
|
+
if (!this.optionTimer) {
|
|
112
|
+
this.optionTimer = setInterval(() => void this.pollNextOptionChunk(), this.pollIntervalMs);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
stopPolling() {
|
|
116
|
+
if (this.tickerTimer)
|
|
117
|
+
clearInterval(this.tickerTimer);
|
|
118
|
+
if (this.optionTimer)
|
|
119
|
+
clearInterval(this.optionTimer);
|
|
120
|
+
this.tickerTimer = null;
|
|
121
|
+
this.optionTimer = null;
|
|
122
|
+
}
|
|
123
|
+
getSubscribedTickers() {
|
|
124
|
+
return Array.from(this.subscribedSymbols).filter((symbol) => !this.isWebullOptionSymbol(symbol));
|
|
125
|
+
}
|
|
126
|
+
getSubscribedOptions() {
|
|
127
|
+
return Array.from(this.subscribedSymbols).filter((symbol) => this.isWebullOptionSymbol(symbol));
|
|
128
|
+
}
|
|
129
|
+
async pollTickers() {
|
|
130
|
+
const symbols = this.getSubscribedTickers();
|
|
131
|
+
if (!this.connected || this.tickerRequestInFlight || symbols.length === 0)
|
|
132
|
+
return;
|
|
133
|
+
this.tickerRequestInFlight = true;
|
|
134
|
+
try {
|
|
135
|
+
for (let index = 0; index < symbols.length; index += OPTION_BATCH_SIZE) {
|
|
136
|
+
const snapshots = await this.requestSnapshots('stockSnapshot', symbols.slice(index, index + OPTION_BATCH_SIZE));
|
|
137
|
+
snapshots.forEach((snapshot) => this.updateTicker(snapshot));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
this.emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
this.tickerRequestInFlight = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async pollNextOptionChunk() {
|
|
148
|
+
const symbols = this.getSubscribedOptions();
|
|
149
|
+
if (!this.connected || symbols.length === 0 || this.pendingOptionRequests > 0)
|
|
150
|
+
return;
|
|
151
|
+
const chunks = this.chunk(symbols, OPTION_BATCH_SIZE);
|
|
152
|
+
const chunk = chunks[this.optionChunkIndex % chunks.length];
|
|
153
|
+
this.optionChunkIndex = (this.optionChunkIndex + 1) % chunks.length;
|
|
154
|
+
try {
|
|
155
|
+
await this.enqueueOptionSnapshot(chunk);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
this.emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
enqueueOptionSnapshot(symbols) {
|
|
162
|
+
this.pendingOptionRequests += 1;
|
|
163
|
+
const request = this.optionRequestChain.then(async () => {
|
|
164
|
+
try {
|
|
165
|
+
const waitMs = Math.max(0, this.optionRequestIntervalMs - (Date.now() - this.lastOptionRequestAt));
|
|
166
|
+
if (waitMs > 0)
|
|
167
|
+
await this.sleep(waitMs);
|
|
168
|
+
if (!this.connected)
|
|
169
|
+
return;
|
|
170
|
+
this.lastOptionRequestAt = Date.now();
|
|
171
|
+
const snapshots = await this.requestSnapshots('optionSnapshot', symbols);
|
|
172
|
+
snapshots.forEach((snapshot) => this.updateOption(snapshot));
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
this.pendingOptionRequests -= 1;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
this.optionRequestChain = request.catch(() => undefined);
|
|
179
|
+
return request;
|
|
180
|
+
}
|
|
181
|
+
async requestSnapshots(operation, symbols) {
|
|
182
|
+
const response = await fetch(this.proxyUrl, {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
headers: { 'Content-Type': 'application/json' },
|
|
185
|
+
body: JSON.stringify({ operation, symbols, accessToken: this.accessToken }),
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const body = await response.text();
|
|
189
|
+
throw new Error(`Webull ${operation} request failed (${response.status}): ${body || response.statusText}`);
|
|
190
|
+
}
|
|
191
|
+
const data = await response.json();
|
|
192
|
+
if (Array.isArray(data))
|
|
193
|
+
return data;
|
|
194
|
+
if (Array.isArray(data.data))
|
|
195
|
+
return data.data;
|
|
196
|
+
throw new Error(`Webull ${operation} returned an invalid response`);
|
|
197
|
+
}
|
|
198
|
+
updateTicker(snapshot) {
|
|
199
|
+
const symbol = snapshot.symbol?.toUpperCase();
|
|
200
|
+
if (!symbol)
|
|
201
|
+
return;
|
|
202
|
+
const bid = this.toNumber(snapshot.bid);
|
|
203
|
+
const ask = this.toNumber(snapshot.ask);
|
|
204
|
+
const last = this.toNumber(snapshot.price);
|
|
205
|
+
const ticker = {
|
|
206
|
+
symbol,
|
|
207
|
+
spot: bid > 0 && ask > 0 ? (bid + ask) / 2 : last,
|
|
208
|
+
bid,
|
|
209
|
+
bidSize: this.toNumber(snapshot.bid_size),
|
|
210
|
+
ask,
|
|
211
|
+
askSize: this.toNumber(snapshot.ask_size),
|
|
212
|
+
last,
|
|
213
|
+
volume: this.toNumber(snapshot.volume),
|
|
214
|
+
timestamp: this.toTimestamp(snapshot.quote_time ?? snapshot.last_trade_time),
|
|
215
|
+
};
|
|
216
|
+
this.tickerCache.set(symbol, ticker);
|
|
217
|
+
this.emit('tickerUpdate', ticker);
|
|
218
|
+
}
|
|
219
|
+
updateOption(snapshot) {
|
|
220
|
+
const occSymbol = snapshot.symbol?.replace(/\s+/g, '').toUpperCase();
|
|
221
|
+
if (!occSymbol)
|
|
222
|
+
return;
|
|
223
|
+
let parsed;
|
|
224
|
+
try {
|
|
225
|
+
parsed = (0, occ_1.parseOCCSymbol)(occSymbol);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const bid = this.toNumber(snapshot.bid);
|
|
231
|
+
const ask = this.toNumber(snapshot.ask);
|
|
232
|
+
const last = this.toNumber(snapshot.price);
|
|
233
|
+
const openInterest = this.toNumber(snapshot.open_interest);
|
|
234
|
+
this.setBaseOpenInterest(occSymbol, openInterest);
|
|
235
|
+
const option = {
|
|
236
|
+
occSymbol,
|
|
237
|
+
underlying: parsed.symbol,
|
|
238
|
+
strike: this.toNumber(snapshot.strike_price) || parsed.strike,
|
|
239
|
+
expiration: parsed.expiration.toISOString().split('T')[0],
|
|
240
|
+
expirationTimestamp: parsed.expiration.getTime(),
|
|
241
|
+
optionType: parsed.optionType,
|
|
242
|
+
bid,
|
|
243
|
+
bidSize: this.toNumber(snapshot.bid_size),
|
|
244
|
+
ask,
|
|
245
|
+
askSize: this.toNumber(snapshot.ask_size),
|
|
246
|
+
mark: bid > 0 && ask > 0 ? (bid + ask) / 2 : last,
|
|
247
|
+
last,
|
|
248
|
+
volume: this.toNumber(snapshot.volume),
|
|
249
|
+
openInterest,
|
|
250
|
+
liveOpenInterest: this.calculateLiveOpenInterest(occSymbol),
|
|
251
|
+
impliedVolatility: this.toNumber(snapshot.imp_vol),
|
|
252
|
+
timestamp: this.toTimestamp(snapshot.quote_time ?? snapshot.last_trade_time),
|
|
253
|
+
greeks: this.buildGreeks(snapshot),
|
|
254
|
+
};
|
|
255
|
+
this.optionCache.set(occSymbol, option);
|
|
256
|
+
this.emit('optionUpdate', option);
|
|
257
|
+
}
|
|
258
|
+
buildGreeks(snapshot) {
|
|
259
|
+
return {
|
|
260
|
+
price: this.toNumber(snapshot.price),
|
|
261
|
+
delta: this.toNumber(snapshot.delta),
|
|
262
|
+
gamma: this.toNumber(snapshot.gamma),
|
|
263
|
+
theta: this.toNumber(snapshot.theta),
|
|
264
|
+
vega: this.toNumber(snapshot.vega),
|
|
265
|
+
rho: this.toNumber(snapshot.rho),
|
|
266
|
+
charm: 0,
|
|
267
|
+
vanna: 0,
|
|
268
|
+
volga: 0,
|
|
269
|
+
speed: 0,
|
|
270
|
+
zomma: 0,
|
|
271
|
+
color: 0,
|
|
272
|
+
ultima: 0,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
toTimestamp(value) {
|
|
276
|
+
const timestamp = this.toNumber(value);
|
|
277
|
+
return timestamp > 0 ? timestamp : Date.now();
|
|
278
|
+
}
|
|
279
|
+
isWebullOptionSymbol(symbol) {
|
|
280
|
+
return BaseBrokerClient_1.OCC_OPTION_PATTERN.test(symbol.replace(/\s+/g, '').toUpperCase());
|
|
281
|
+
}
|
|
282
|
+
chunk(items, size) {
|
|
283
|
+
const chunks = [];
|
|
284
|
+
for (let index = 0; index < items.length; index += size) {
|
|
285
|
+
chunks.push(items.slice(index, index + size));
|
|
286
|
+
}
|
|
287
|
+
return chunks;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
exports.WebullClient = WebullClient;
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export { FloeClient, Broker } from './client/FloeClient';
|
|
|
20
20
|
export { TradierClient } from './client/brokers/TradierClient';
|
|
21
21
|
export { TastyTradeClient } from './client/brokers/TastyTradeClient';
|
|
22
22
|
export { TradeStationClient } from './client/brokers/TradeStationClient';
|
|
23
|
+
export { WebullClient, createWebullAuthToken, parseWebullAuthToken, } from './client/brokers/WebullClient';
|
|
24
|
+
export type { ParsedWebullAuthToken, WebullAuthTokenConfig, WebullClientOptions, } from './client/brokers/WebullClient';
|
|
23
25
|
export type { AggressorSide, IntradayTrade } from './client/brokers/TradierClient';
|
|
24
26
|
export { genericAdapter, schwabAdapter, ibkrAdapter, tdaAdapter, brokerAdapters, getAdapter, createOptionChain, } from './adapters';
|
|
25
27
|
export { computeVarianceSwapIV, computeImpliedVolatility, } from './iv';
|
|
@@ -28,3 +30,5 @@ export { computeRealizedVolatility, } from './rv';
|
|
|
28
30
|
export type { PriceObservation, RealizedVolatilityResult, } from './rv';
|
|
29
31
|
export { computeVolResponseZScore, buildVolResponseObservation, } from './volresponse';
|
|
30
32
|
export type { VolResponseObservation, VolResponseCoefficients, VolResponseConfig, VolResponseResult, } from './volresponse';
|
|
33
|
+
export { ApiClient, NewApiClient, APIError, } from './apiclient';
|
|
34
|
+
export type { FetchLike, ApiContext, CtxEquivalent, HindsightDataRequest, DealerMinuteSurfacesRequest, AMTRequest, HindsightEvent, DealerMinuteSurface, AMTSessionStatsRow, AMTEventsRow, MinuteSurface, SurfacePoint, OptionsScreenerRequest, OptionsScreenerResponse, } from './apiclient';
|
package/dist/index.js
CHANGED
|
@@ -20,8 +20,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
20
20
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
21
21
|
};
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.
|
|
24
|
-
exports.buildVolResponseObservation = exports.computeVolResponseZScore = void 0;
|
|
23
|
+
exports.createOptionChain = exports.getAdapter = exports.brokerAdapters = exports.tdaAdapter = exports.ibkrAdapter = exports.schwabAdapter = exports.genericAdapter = exports.parseWebullAuthToken = exports.createWebullAuthToken = exports.WebullClient = exports.TradeStationClient = exports.TastyTradeClient = exports.TradierClient = exports.Broker = exports.FloeClient = exports.analyzeHedgeFlow = exports.computePressureCloud = exports.computeCharmIntegral = exports.computeHedgeImpulseCurve = exports.interpolateIVAtStrike = exports.deriveRegimeParams = exports.OPEX_CONFIG = exports.CRISIS_CONFIG = exports.LOW_VOL_CONFIG = exports.DEFAULT_ADJUSTMENT_CONFIG = exports.getSignificantAdjustmentLevels = exports.getEdgeAtPrice = exports.estimateExposureAdjustedPDF = exports.getQuantile = exports.getCumulativeProbability = exports.getProbabilityInRange = exports.estimateImpliedProbabilityDistributions = exports.estimateImpliedProbabilityDistribution = exports.generateOCCSymbolsAroundSpot = exports.generateOCCSymbolsForStrikes = exports.generateStrikesAroundSpot = exports.parseOCCSymbol = exports.buildOCCSymbol = exports.normalPDF = exports.cumulativeNormalDistribution = exports.calculateSharesNeededToCover = exports.calculateGammaVannaCharmExposures = exports.smoothTotalVarianceSmile = exports.getIVForStrike = exports.getIVSurfaces = exports.getTimeToExpirationInYears = exports.getMillisecondsToExpiration = exports.calculateImpliedVolatility = exports.calculateGreeks = exports.blackScholes = void 0;
|
|
24
|
+
exports.APIError = exports.NewApiClient = exports.ApiClient = exports.buildVolResponseObservation = exports.computeVolResponseZScore = exports.computeRealizedVolatility = exports.computeImpliedVolatility = exports.computeVarianceSwapIV = void 0;
|
|
25
25
|
// Core types
|
|
26
26
|
__exportStar(require("./types"), exports);
|
|
27
27
|
// Black-Scholes pricing and Greeks
|
|
@@ -91,6 +91,10 @@ var TastyTradeClient_1 = require("./client/brokers/TastyTradeClient");
|
|
|
91
91
|
Object.defineProperty(exports, "TastyTradeClient", { enumerable: true, get: function () { return TastyTradeClient_1.TastyTradeClient; } });
|
|
92
92
|
var TradeStationClient_1 = require("./client/brokers/TradeStationClient");
|
|
93
93
|
Object.defineProperty(exports, "TradeStationClient", { enumerable: true, get: function () { return TradeStationClient_1.TradeStationClient; } });
|
|
94
|
+
var WebullClient_1 = require("./client/brokers/WebullClient");
|
|
95
|
+
Object.defineProperty(exports, "WebullClient", { enumerable: true, get: function () { return WebullClient_1.WebullClient; } });
|
|
96
|
+
Object.defineProperty(exports, "createWebullAuthToken", { enumerable: true, get: function () { return WebullClient_1.createWebullAuthToken; } });
|
|
97
|
+
Object.defineProperty(exports, "parseWebullAuthToken", { enumerable: true, get: function () { return WebullClient_1.parseWebullAuthToken; } });
|
|
94
98
|
// Broker adapters
|
|
95
99
|
var adapters_1 = require("./adapters");
|
|
96
100
|
Object.defineProperty(exports, "genericAdapter", { enumerable: true, get: function () { return adapters_1.genericAdapter; } });
|
|
@@ -111,3 +115,8 @@ Object.defineProperty(exports, "computeRealizedVolatility", { enumerable: true,
|
|
|
111
115
|
var volresponse_1 = require("./volresponse");
|
|
112
116
|
Object.defineProperty(exports, "computeVolResponseZScore", { enumerable: true, get: function () { return volresponse_1.computeVolResponseZScore; } });
|
|
113
117
|
Object.defineProperty(exports, "buildVolResponseObservation", { enumerable: true, get: function () { return volresponse_1.buildVolResponseObservation; } });
|
|
118
|
+
// Dataset API clients (Hindsight + Dealer minute surfaces + AMT + Options Screeners)
|
|
119
|
+
var apiclient_1 = require("./apiclient");
|
|
120
|
+
Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return apiclient_1.ApiClient; } });
|
|
121
|
+
Object.defineProperty(exports, "NewApiClient", { enumerable: true, get: function () { return apiclient_1.NewApiClient; } });
|
|
122
|
+
Object.defineProperty(exports, "APIError", { enumerable: true, get: function () { return apiclient_1.APIError; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullstackcraftllc/floe",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "Production-ready options analytics toolkit. Normalize broker data structures and calculate Black-Scholes, Greeks, and exposures with a clean, type-safe API. Built for trading platforms and fintech applications.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"test": "jest",
|
|
10
10
|
"prepublishOnly": "npm run build",
|
|
11
11
|
"type-check": "tsc --noEmit",
|
|
12
|
-
"prepare": "husky",
|
|
13
12
|
"copy-whitepaper": "cp whitepaper/whitepaper.pdf site/public/whitepaper.pdf"
|
|
14
13
|
},
|
|
15
14
|
"keywords": [
|
|
@@ -44,7 +43,6 @@
|
|
|
44
43
|
"@types/jest": "^29.5.0",
|
|
45
44
|
"@types/node": "^20.0.0",
|
|
46
45
|
"dotenv": "^17.2.3",
|
|
47
|
-
"husky": "^9.1.7",
|
|
48
46
|
"jest": "^29.5.0",
|
|
49
47
|
"ts-jest": "^29.1.0",
|
|
50
48
|
"typescript": "^5.9.3"
|