Skip to content

Commit

Permalink
Merge pull request #169 from stoqey/market-scanner
Browse files Browse the repository at this point in the history
Market scanner
  • Loading branch information
rylorin authored Sep 14, 2023
2 parents 61d1e8a + 9d892cd commit 023965c
Show file tree
Hide file tree
Showing 46 changed files with 1,250 additions and 729 deletions.
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,26 @@
"!**/node_modules/**"
]
},
{
"type": "node",
"request": "launch",
"name": "Tool: market-scanner",
"skipFiles": [
"<node_internals>/**"
],
"runtimeArgs": [
"--trace-warnings"
],
"args": [

],
"program": "${workspaceFolder}/dist/tools/market-scanner.js",
"outputCapture": "std",
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**"
]
},
{
"type": "node",
"request": "launch",
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,19 @@
},
"devDependencies": {
"@types/jest": "^29.5.4",
"@types/node": "^18.17.14",
"@types/node": "^18.17.15",
"@types/source-map-support": "^0.5.7",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"ajv": "^8.12.0",
"eslint": "^8.48.0",
"eslint": "^8.49.0",
"eslint-plugin-jest": "^27.2.3",
"eslint-plugin-rxjs": "^5.0.3",
"jest": "^29.6.4",
"jest-environment-node": "^29.6.4",
"jest-junit": "^16.0.0",
"ts-jest": "^29.1.1",
"typedoc": "^0.25.0",
"typedoc": "^0.25.1",
"typescript": "^4.9.5"
},
"engines": {
Expand Down
191 changes: 178 additions & 13 deletions src/api-next/api-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
OrderBookRow,
OrderBookUpdate,
OrderState,
ScannerSubscription,
SecType,
TagValue,
} from "../";
Expand All @@ -38,6 +39,7 @@ import { MutableMarketData } from "../core/api-next/api/market/mutable-market-da
import { MutableAccountPositions } from "../core/api-next/api/position/mutable-account-positions-update";
import { IBApiAutoConnection } from "../core/api-next/auto-connection";
import { ConsoleLogger } from "../core/api-next/console-logger";
import { IBApiNextItemListUpdate } from "../core/api-next/item-list-update";
import { IBApiNextLogger } from "../core/api-next/logger";
import { IBApiNextSubscription } from "../core/api-next/subscription";
import { IBApiNextSubscriptionRegistry } from "../core/api-next/subscription-registry";
Expand All @@ -62,6 +64,12 @@ import {
SecurityDefinitionOptionParameterType,
} from "./";
import { Logger } from "./common/logger";
import {
MarketScannerItem,
MarketScannerItemRank,
MarketScannerRows,
MarketScannerUpdate,
} from "./market-scanner/market-scanner";

/**
* @internal
Expand Down Expand Up @@ -321,7 +329,7 @@ export class IBApiNext {
}

/** currentTime event handler. */
private onCurrentTime = (
private readonly onCurrentTime = (
subscriptions: Map<number, IBApiNextSubscription<number>>,
time: number,
): void => {
Expand Down Expand Up @@ -2076,7 +2084,7 @@ export class IBApiNext {
/** updateMktDepth event handler */
private readonly onUpdateMktDepth = (
subscriptions: Map<number, IBApiNextSubscription<OrderBook>>,
tickerId: number,
reqId: number,
position: number,
operation: number,
side: number,
Expand All @@ -2086,7 +2094,7 @@ export class IBApiNext {
// forward to L2 handler, but w/o market maker and smart depth set to false
this.onUpdateMktDepthL2(
subscriptions,
tickerId,
reqId,
position,
undefined,
operation,
Expand All @@ -2098,23 +2106,23 @@ export class IBApiNext {
};

// mutable
private readonly insertAtIndex = (
private insertAtMapIndex<T extends number, R>(
index: number,
key: OrderBookRowPosition,
value: OrderBookRow,
map: Map<OrderBookRowPosition, OrderBookRow>,
): Map<OrderBookRowPosition, OrderBookRow> => {
key: T,
value: R,
map: Map<T, R>,
): Map<T, R> {
const arr = Array.from(map);
arr.splice(index, 0, [key, value]);
map.clear();
arr.forEach(([k, v]) => map.set(k, v));
return map;
};
}

/** marketDepthL2 event handler */
private readonly onUpdateMktDepthL2 = (
subscriptions: Map<number, IBApiNextSubscription<OrderBook>>,
tickerId: number,
reqId: number,
position: number,
marketMaker: string,
operation: number,
Expand All @@ -2124,7 +2132,7 @@ export class IBApiNext {
isSmartDepth: boolean,
): void => {
// get subscription
const subscription = subscriptions.get(tickerId);
const subscription = subscriptions.get(reqId);
if (!subscription) {
return;
}
Expand Down Expand Up @@ -2166,7 +2174,7 @@ export class IBApiNext {
case 0:
// it's an insert

this.insertAtIndex(
this.insertAtMapIndex(
position,
position,
{
Expand All @@ -2178,7 +2186,7 @@ export class IBApiNext {
cachedRows,
);

this.insertAtIndex(
this.insertAtMapIndex(
position,
position,
{
Expand Down Expand Up @@ -2285,6 +2293,163 @@ export class IBApiNext {
);
}

private readonly onScannerParameters = (
subscriptions: Map<number, IBApiNextSubscription<string>>,
xml: string,
): void => {
subscriptions.forEach((sub) => {
sub.next({ all: xml });
sub.complete();
});
};

/**
* Requests an XML string that describes all possible scanner queries.
*/
getScannerParameters(): Promise<string> {
return lastValueFrom(
this.subscriptions
.register<string>(
() => {
this.api.reqScannerParameters();
},
undefined,
[[EventName.scannerParameters, this.onScannerParameters]],
"getScannerParameters", // use same instance id each time, to make sure there is only 1 pending request at time
)
.pipe(map((v: { all: string }) => v.all)),
{
defaultValue: "",
},
);
}

/**
* Provides the data resulting from the market scanner request.
* @param subscriptions
* @param reqId the request's identifier
* @param rank the ranking within the response of this bar.
* @param contract the data's ContractDetails
* @param distance according to query
* @param benchmark according to query
* @param projection according to query
* @param legStr describes the combo legs when the scanner is returning EFP
* @returns void
*/
private readonly onScannerData = (
subscriptions: Map<number, IBApiNextSubscription<MarketScannerRows>>,
reqId: number,
rank: number,
contract: ContractDetails,
distance: string,
benchmark: string,
projection: string,
legStr: string,
): void => {
// get subscription
const subscription = subscriptions.get(reqId);
if (!subscription) {
return;
}

const item: MarketScannerItem = {
rank,
contract,
distance,
benchmark,
projection,
legStr,
};

// console.log("onScannerData", item);

const lastValue = subscription.lastValue ?? {
all: new Map<MarketScannerItemRank, MarketScannerItem>(),
allset: false,
};

const existing = lastValue.all.get(rank) != undefined;
lastValue.all.set(rank, item);
if (lastValue.allset) {
const updated: MarketScannerRows = new Map<
MarketScannerItemRank,
MarketScannerItem
>();
updated.set(rank, item);
subscription.next({
all: lastValue.all,
allset: lastValue.allset,
changed: existing ? updated : undefined,
added: existing ? undefined : updated,
});
} else {
// console.log("saving for future use", lastValue);
subscription.lastValue = lastValue;
}
};

/**
* Indicates the scanner data reception has terminated.
* @param subscriptions
* @param reqId the request's identifier
* @returns
*/
private readonly onScannerDataEnd = (
subscriptions: Map<number, IBApiNextSubscription<MarketScannerRows>>,
reqId: number,
): void => {
const subscription = subscriptions.get(reqId);
if (!subscription) {
return;
}

const lastValue = subscription.lastValue ?? {
all: new Map<MarketScannerItemRank, MarketScannerItem>(),
};
const updated: IBApiNextItemListUpdate<MarketScannerRows> = {
all: lastValue.all,
allset: true,
added: lastValue.all,
};

// console.log("onScannerDataEnd", updated);

// subscription.next(updated);
subscription.next(updated);
};

/**
* It returns an observable that will emit a list of scanner subscriptions.
* @param {ScannerSubscription} scannerSubscription - ScannerSubscription
* @param {TagValue[]} [scannerSubscriptionOptions] - An array of TagValue objects.
* @param {TagValue[]} [scannerSubscriptionFilterOptions] - An optional array of TagValue objects.
* @returns An observable that will emit a list of items.
*/
getMarketScanner(
scannerSubscription: ScannerSubscription,
scannerSubscriptionOptions?: TagValue[],
scannerSubscriptionFilterOptions?: TagValue[],
): Observable<MarketScannerUpdate> {
return this.subscriptions.register<MarketScannerRows>(
(reqId) => {
this.api.reqScannerSubscription(
reqId,
scannerSubscription,
scannerSubscriptionOptions,
scannerSubscriptionFilterOptions,
);
},
(reqId) => {
this.api.cancelScannerSubscription(reqId);
},
[
[EventName.scannerData, this.onScannerData],
[EventName.scannerDataEnd, this.onScannerDataEnd],
],
`getMarketScanner-${JSON.stringify(scannerSubscription)}`,
);
}

/** histogramData event handler */
private readonly onHistogramData = (
subscriptions: Map<number, IBApiNextSubscription<HistogramEntry[]>>,
Expand Down
3 changes: 3 additions & 0 deletions src/api-next/common/item-list-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export interface ItemListUpdate<T> {
/** All items with its latest values, as received by TWS. */
readonly all: T;

/** all value is set with all items (ie not still being built = End message received from TWS) */
readonly allset?: boolean;

/** Items that have been added since last [[IBApiNextUpdate]]. */
readonly added?: T;

Expand Down
Loading

0 comments on commit 023965c

Please sign in to comment.