fix alpaca news auth and native worker wiring

This commit is contained in:
dirtydishes 2026-05-19 19:57:56 -04:00
parent e9739f5dc9
commit 7d25608b35
21 changed files with 285 additions and 80 deletions

View file

@ -1,4 +1,9 @@
import { decode, encode } from "@msgpack/msgpack";
import {
buildAlpacaAuthHeaders,
buildAlpacaWebSocketAuthMessage,
type AlpacaCredentials
} from "@islandflow/config";
import { createLogger } from "@islandflow/observability";
import type { OptionIngestAdapter, OptionIngestHandlers } from "./types";
import WebSocket from "ws";
@ -6,7 +11,7 @@ import WebSocket from "ws";
type AlpacaFeed = "indicative" | "opra";
type AlpacaOptionsAdapterConfig = {
apiKey: string;
credentials: AlpacaCredentials;
restUrl: string;
wsBaseUrl: string;
feed: AlpacaFeed;
@ -147,18 +152,12 @@ const normalizeUnderlyings = (value: string[]): string[] => {
return result;
};
const buildHeaders = (config: AlpacaOptionsAdapterConfig): Record<string, string> => {
return {
Authorization: `Bearer ${config.apiKey}`
};
};
const fetchJson = async <T>(
url: URL,
config: AlpacaOptionsAdapterConfig
): Promise<T> => {
const response = await fetch(url.toString(), {
headers: buildHeaders(config)
headers: buildAlpacaAuthHeaders(config.credentials)
});
if (!response.ok) {
@ -398,8 +397,8 @@ export const createAlpacaOptionsAdapter = (
return {
name: "alpaca",
start: async (handlers: OptionIngestHandlers) => {
if (!config.apiKey) {
throw new Error("Alpaca adapter requires ALPACA_API_KEY.");
if (!config.credentials.keyId) {
throw new Error("Alpaca adapter requires Alpaca credentials.");
}
const underlyings = normalizeUnderlyings(config.underlyings);
@ -485,15 +484,22 @@ export const createAlpacaOptionsAdapter = (
const wsUrl = `${wsBase}/${config.feed}`;
const ws = new WebSocket(wsUrl, {
headers: {
...buildHeaders(config),
...buildAlpacaAuthHeaders(config.credentials),
"Content-Type": "application/msgpack"
}
});
let seq = 0;
let stopped = false;
let subscribed = false;
const subscribe = () => {
if (subscribed) {
return;
}
subscribed = true;
ws.on("open", () => {
const subscribe: Record<string, unknown> = {
action: "subscribe",
trades: selectedSymbols
@ -504,6 +510,10 @@ export const createAlpacaOptionsAdapter = (
}
ws.send(encode(subscribe));
};
ws.on("open", () => {
ws.send(encode(buildAlpacaWebSocketAuthMessage(config.credentials)));
});
ws.on("message", (data) => {
@ -583,7 +593,13 @@ export const createAlpacaOptionsAdapter = (
if (type === "error") {
logger.error("alpaca stream error", { message });
} else if (type === "success" || type === "subscription") {
} else if (type === "success") {
const status = (message as { msg?: string }).msg ?? "";
if (status === "authenticated") {
subscribe();
}
logger.info("alpaca stream status", { message });
} else if (type === "subscription") {
logger.info("alpaca stream status", { message });
}
}

View file

@ -1,4 +1,4 @@
import { readEnv } from "@islandflow/config";
import { hasAlpacaCredentials, readEnv, resolveAlpacaCredentials } from "@islandflow/config";
import { createLogger } from "@islandflow/observability";
import {
SUBJECT_OPTION_NBBO,
@ -55,6 +55,10 @@ const envSchema = z.object({
CLICKHOUSE_DATABASE: z.string().default("default"),
OPTIONS_INGEST_ADAPTER: z.string().min(1).default("synthetic"),
ALPACA_API_KEY: z.string().default(""),
ALPACA_API_KEY_ID: z.string().default(""),
ALPACA_KEY_ID: z.string().default(""),
ALPACA_API_SECRET_KEY: z.string().default(""),
ALPACA_SECRET_KEY: z.string().default(""),
ALPACA_REST_URL: z.string().default("https://data.alpaca.markets"),
ALPACA_WS_BASE_URL: z.string().default("wss://stream.data.alpaca.markets/v1beta1"),
ALPACA_FEED: z.enum(["indicative", "opra"]).default("indicative"),
@ -120,6 +124,7 @@ const envSchema = z.object({
});
const env = readEnv(envSchema);
const alpacaCredentials = resolveAlpacaCredentials(env);
const syntheticModes = resolveSyntheticMarketModes({
syntheticMarketMode: env.SYNTHETIC_MARKET_MODE,
syntheticOptionsMode: env.SYNTHETIC_OPTIONS_MODE
@ -277,15 +282,17 @@ const selectAdapter = (
}
if (name === "alpaca") {
if (!env.ALPACA_API_KEY) {
logger.warn("alpaca credentials missing; set ALPACA_API_KEY");
throw new Error("ALPACA_API_KEY is required for the alpaca adapter.");
if (!hasAlpacaCredentials(alpacaCredentials)) {
logger.warn("alpaca credentials missing; set ALPACA_API_KEY_ID and ALPACA_API_SECRET_KEY");
throw new Error(
"Alpaca adapter requires ALPACA_API_KEY_ID and ALPACA_API_SECRET_KEY (or legacy ALPACA_API_KEY)."
);
}
const underlyings = env.ALPACA_UNDERLYINGS.split(",").map((symbol) => symbol.trim());
return createAlpacaOptionsAdapter({
apiKey: env.ALPACA_API_KEY,
credentials: alpacaCredentials,
restUrl: env.ALPACA_REST_URL,
wsBaseUrl: env.ALPACA_WS_BASE_URL,
feed: env.ALPACA_FEED,