diff --git a/README.md b/README.md
index e920b57..20da37c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
@@ -14,6 +14,10 @@ Your one-person Wall Street. Alice is an AI trading agent that gives you your ow
- **Reasoning-driven** — every trading decision is based on continuous reasoning and signal mixing.
- **OS-native** — Alice can interact with your operating system. Search the web through your browser, send messages via Telegram, and connect to local devices.
+
+
+
+
## Features
- **Multi-provider AI** — switch between Claude Code CLI, Vercel AI SDK, and Agent SDK at runtime, no restart needed
diff --git a/alice-full.png b/docs/images/alice-full.png
similarity index 100%
rename from alice-full.png
rename to docs/images/alice-full.png
diff --git a/docs/opentypebb-tutorial.md b/docs/opentypebb-tutorial.md
new file mode 100644
index 0000000..aea02c7
--- /dev/null
+++ b/docs/opentypebb-tutorial.md
@@ -0,0 +1,314 @@
+# Running OpenTypeBB with OpenAlice
+
+OpenTypeBB is a TypeScript-native port of the [OpenBB Platform](https://github.com/OpenBB-finance/OpenBB) — the open-source financial data infrastructure. It ships as an internal package (`@traderalice/opentypebb`) inside OpenAlice, giving you access to equity, crypto, currency, commodity, economy, and news data without spinning up a Python sidecar or messing with `uv`.
+
+This tutorial walks you through getting OpenAlice up and running with OpenTypeBB as the data backend.
+
+---
+
+## Prerequisites
+
+| Requirement | Version |
+|-------------|---------|
+| [Node.js](https://nodejs.org/) | 22+ |
+| [pnpm](https://pnpm.io/) | 10+ |
+| [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) | latest (installed & authenticated) |
+
+That's it. No Python, no `uv`, no Docker.
+
+---
+
+## 1. Clone & Install
+
+```bash
+git clone https://github.com/TraderAlice/OpenAlice.git
+cd OpenAlice
+pnpm install
+pnpm build
+```
+
+`pnpm install` resolves the monorepo workspace — the `@traderalice/opentypebb` package under `packages/opentypebb/` is linked automatically.
+
+## 2. Start the Dev Server
+
+```bash
+pnpm dev
+```
+
+Open [http://localhost:3002](http://localhost:3002) and start chatting. No API keys or extra config required — the default setup uses Claude Code as the AI backend with your existing login, and OpenTypeBB as the data engine via in-process SDK mode.
+
+> For frontend hot-reload during development, run `pnpm dev:ui` (port 5173) in a separate terminal.
+
+## 3. Verify OpenTypeBB Is Active
+
+OpenTypeBB is the **default** data backend. You don't need to configure anything — it's already on.
+
+Under the hood, OpenAlice reads `data/config/openbb.json`. If that file doesn't exist yet, it falls back to these defaults:
+
+```json
+{
+ "enabled": true,
+ "dataBackend": "sdk",
+ "providers": {
+ "equity": "yfinance",
+ "crypto": "yfinance",
+ "currency": "yfinance",
+ "newsCompany": "yfinance",
+ "newsWorld": "fmp"
+ },
+ "providerKeys": {},
+ "apiServer": {
+ "enabled": false,
+ "port": 6901
+ }
+}
+```
+
+Key settings:
+
+- **`dataBackend: "sdk"`** — This is the in-process mode. OpenTypeBB's `QueryExecutor` runs directly inside the Node.js process — no HTTP, no sidecar. This is the default.
+- **`dataBackend: "openbb"`** — Switches to making HTTP requests to an external OpenBB Platform API server (Python). You probably don't want this.
+- **`providers`** — Which data provider to use per asset class. `yfinance` works out of the box with no API key.
+
+## 4. Available Data Providers
+
+OpenTypeBB ships with 14 providers:
+
+| Provider | Covers | API Key? |
+|----------|--------|----------|
+| **yfinance** | Equity, crypto, currency, news | No |
+| **fmp** | Equity fundamentals, news, discovery | Required |
+| **intrinio** | Options data | Required |
+| **eia** | Energy (petroleum, natural gas, electricity) | Required |
+| **econdb** | Global economic data | Optional (higher rate limits) |
+| **federal_reserve** | FRED, FOMC, payrolls, PCE, Michigan, etc. | Optional (higher rate limits) |
+| **bls** | Bureau of Labor Statistics employment data | Optional (higher rate limits) |
+| **deribit** | Crypto derivatives | No |
+| **cboe** | Index data | No |
+| **multpl** | S&P 500 multiples (PE, earnings yield) | No |
+| **oecd** | GDP, economic indicators | No |
+| **imf** | International trade, CPI, balance of payments | No |
+| **ecb** | European balance of payments | No |
+| **stub** | Test/placeholder | No |
+
+**Most things work without any API key** — `yfinance` covers equity quotes, crypto prices, forex rates, and company news. `federal_reserve`, `bls`, and `econdb` also work keyless but with stricter rate limits. Only `fmp`, `intrinio`, and `eia` strictly require a key.
+
+## 5. Adding API Keys (Optional)
+
+To unlock additional providers (FMP, EIA, Intrinio, etc.), create or edit `data/config/openbb.json`:
+
+```json
+{
+ "providerKeys": {
+ "fmp": "your_fmp_api_key_here",
+ "eia": "your_eia_api_key_here"
+ }
+}
+```
+
+Or set them through the Web UI: open [http://localhost:3002](http://localhost:3002), go to the config panel, and edit the OpenBB section. Changes take effect immediately — no restart needed.
+
+The key names map to OpenBB credential fields automatically:
+`fmp` → `fmp_api_key`, `eia` → `eia_api_key`, `fred` → `fred_api_key`, etc.
+
+## 6. What Can You Do?
+
+Once running, Alice has access to a rich set of market data tools powered by OpenTypeBB:
+
+### Market Search
+Ask Alice to find any symbol across equities, crypto, and forex:
+> "Search for Tesla stock"
+> "Find crypto pairs with SOL"
+
+### Equity Data
+- Price quotes and historical OHLCV
+- Company profiles and financial statements
+- Analyst estimates and earnings calendar
+- Insider trading and institutional ownership
+- Market movers (top gainers, losers, most active)
+
+### Crypto & Forex
+- Real-time price data
+- Historical OHLCV with configurable intervals
+
+### Technical Analysis
+Built-in indicator calculator with formula expressions:
+> "Calculate RSI(14) for AAPL on the daily chart"
+> "Show me the 50-day and 200-day SMA crossover for BTC/USD"
+
+Uses syntax like `SMA(CLOSE('AAPL', '1d'), 50)`, `RSI(CLOSE('BTC/USD', '1d'), 14)`, etc.
+
+### Economy & Macro
+- GDP data (OECD, IMF)
+- FRED economic series (rates, inflation, employment)
+- PCE, CPI, nonfarm payrolls, FOMC documents
+- University of Michigan consumer sentiment
+- Fed manufacturing outlook surveys
+
+### Commodities
+- EIA petroleum & natural gas data
+- Spot commodity prices
+
+### News
+- Company-specific news
+- World market news
+- Background RSS collection with searchable archive
+
+## 7. Running OpenTypeBB as a Standalone HTTP Server
+
+If you want to use OpenTypeBB independently — for example, to connect it to [OpenBB Workspace](https://pro.openbb.co) or other tools — you can run it as a standalone API server:
+
+```bash
+# From the repo root:
+cd packages/opentypebb
+
+# Set your API key (optional — yfinance works without one)
+export FMP_API_KEY=your_key_here
+
+# Run the server
+npx tsx src/server.ts
+```
+
+The server starts on port 6901 (configurable via `OPENTYPEBB_PORT`):
+```
+Built widgets.json with 88 widgets
+OpenTypeBB listening on http://localhost:6901
+```
+
+### API Endpoints
+
+The server exposes OpenBB-compatible REST endpoints:
+
+```bash
+# Health check
+curl http://localhost:6901/api/v1/health
+
+# Get a stock quote
+curl "http://localhost:6901/api/v1/equity/price/quote?symbol=AAPL&provider=yfinance"
+
+# Get historical data
+curl "http://localhost:6901/api/v1/equity/price/historical?symbol=MSFT&provider=yfinance&start_date=2024-01-01"
+
+# Get crypto price
+curl "http://localhost:6901/api/v1/crypto/price/historical?symbol=BTC-USD&provider=yfinance"
+
+# Get world news (requires FMP key)
+curl "http://localhost:6901/api/v1/news/world?provider=fmp&limit=5"
+
+# GDP data from OECD
+curl "http://localhost:6901/api/v1/economy/gdp/nominal?provider=oecd&country=united_states"
+
+# Pass credentials per-request
+curl -H 'X-OpenBB-Credentials: {"fmp_api_key": "your_key"}' \
+ "http://localhost:6901/api/v1/equity/fundamental/income?symbol=AAPL&provider=fmp"
+
+# Discover available widgets (for OpenBB Workspace)
+curl http://localhost:6901/widgets.json
+```
+
+### Embedded Server Mode
+
+You can also run the API server embedded inside OpenAlice (alongside the agent). Edit `data/config/openbb.json`:
+
+```json
+{
+ "apiServer": {
+ "enabled": true,
+ "port": 6901
+ }
+}
+```
+
+Then `pnpm dev` will start both Alice and the OpenTypeBB HTTP API.
+
+## 8. Using OpenTypeBB as a Library
+
+You can also import OpenTypeBB directly in your own TypeScript project:
+
+```typescript
+import { createExecutor } from '@traderalice/opentypebb'
+
+const executor = createExecutor()
+
+// Get a stock quote (yfinance — no API key needed)
+const quotes = await executor.execute('yfinance', 'EquityQuote', {
+ symbol: 'AAPL',
+}, {})
+
+console.log(quotes)
+
+// Get historical crypto data
+const btcHistory = await executor.execute('yfinance', 'CryptoHistorical', {
+ symbol: 'BTC-USD',
+ start_date: '2024-01-01',
+}, {})
+
+// With an FMP API key
+const income = await executor.execute('fmp', 'IncomeStatement', {
+ symbol: 'AAPL',
+ period: 'annual',
+}, {
+ fmp_api_key: 'your_key_here',
+})
+```
+
+## 9. Architecture Overview
+
+```
+OpenAlice
+├── packages/opentypebb/ # The OpenTypeBB library
+│ ├── src/
+│ │ ├── index.ts # Library entry point
+│ │ ├── server.ts # Standalone HTTP server
+│ │ ├── core/ # Registry, executor, router, REST API
+│ │ ├── providers/ # 14 data providers (yfinance, fmp, oecd, ...)
+│ │ └── extensions/ # 9 domain routers (equity, crypto, economy, ...)
+│ └── package.json
+│
+├── src/openbb/
+│ ├── sdk/ # In-process SDK clients (equity, crypto, ...)
+│ │ ├── executor.ts # Singleton QueryExecutor
+│ │ ├── base-client.ts # Base class for SDK clients
+│ │ └── *-client.ts # Domain-specific SDK clients
+│ ├── equity/ # Equity data layer + SymbolIndex
+│ ├── crypto/ # Crypto data layer
+│ ├── currency/ # Currency/forex data layer
+│ ├── commodity/ # Commodity data layer
+│ ├── economy/ # Economy data layer
+│ ├── news/ # News data layer
+│ └── credential-map.ts # Config key → OpenBB credential mapping
+│
+├── src/extension/
+│ ├── analysis-kit/ # Technical indicator calculator
+│ ├── equity/ # Equity research tools
+│ ├── market/ # Unified symbol search
+│ └── news/ # News tools
+│
+└── data/config/openbb.json # Runtime configuration
+```
+
+**Data flow (SDK mode):**
+```
+Alice asks for AAPL quote
+ → ToolCenter dispatches to equity extension
+ → SDKEquityClient.getQuote()
+ → QueryExecutor.execute('yfinance', 'EquityQuote', { symbol: 'AAPL' })
+ → YFinanceFetcher hits Yahoo Finance API
+ → Returns structured data
+```
+
+No HTTP. No Python. No sidecar. Just TypeScript all the way down.
+
+---
+
+## TL;DR
+
+```bash
+git clone https://github.com/TraderAlice/OpenAlice.git
+cd OpenAlice
+pnpm install && pnpm build
+pnpm dev
+# Open http://localhost:3002 and ask Alice about any stock, crypto, or macro data
+```
+
+Everything works out of the box. OpenTypeBB is the default data backend, `yfinance` is the default provider, and neither requires an API key. Add keys to `data/config/openbb.json` when you want to unlock more providers.
diff --git a/package.json b/package.json
index 2f96b61..df02e7b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "open-alice",
- "version": "0.9.0-beta.4",
+ "version": "0.9.0-beta.5",
"description": "File-based trading agent engine",
"type": "module",
"scripts": {
@@ -45,7 +45,7 @@
"express": "^5.2.1",
"file-type": "^21.3.0",
"grammy": "^1.40.0",
- "hono": "^4.12.5",
+ "hono": "^4.12.7",
"json5": "^2.2.3",
"@traderalice/opentypebb": "workspace:*",
"pino": "^10.3.1",
diff --git a/packages/opentypebb/package.json b/packages/opentypebb/package.json
index 298068f..1a95858 100644
--- a/packages/opentypebb/package.json
+++ b/packages/opentypebb/package.json
@@ -1,6 +1,6 @@
{
"name": "@traderalice/opentypebb",
- "version": "0.1.1",
+ "version": "0.1.2",
"description": "TypeScript port of OpenBB Platform — financial data infrastructure",
"type": "module",
"exports": {
diff --git a/packages/opentypebb/src/core/api/app-loader.ts b/packages/opentypebb/src/core/api/app-loader.ts
index 6bcd3b4..fdb33c5 100644
--- a/packages/opentypebb/src/core/api/app-loader.ts
+++ b/packages/opentypebb/src/core/api/app-loader.ts
@@ -25,6 +25,7 @@ import { federalReserveProvider } from '../../providers/federal_reserve/index.js
import { intrinioProvider } from '../../providers/intrinio/index.js'
import { blsProvider } from '../../providers/bls/index.js'
import { eiaProvider } from '../../providers/eia/index.js'
+import { secProvider } from '../../providers/sec/index.js'
import { stubProvider } from '../../providers/stub/index.js'
// --- Extension routers ---
@@ -57,6 +58,7 @@ export function createRegistry(): Registry {
registry.includeProvider(intrinioProvider)
registry.includeProvider(blsProvider)
registry.includeProvider(eiaProvider)
+ registry.includeProvider(secProvider)
registry.includeProvider(stubProvider)
return registry
}
diff --git a/packages/opentypebb/src/index.ts b/packages/opentypebb/src/index.ts
index 591a1aa..7dddce4 100644
--- a/packages/opentypebb/src/index.ts
+++ b/packages/opentypebb/src/index.ts
@@ -39,6 +39,10 @@ export { OpenBBError, EmptyDataError, UnauthorizedError } from './core/provider/
// App loader — convenience functions to create a fully-loaded system
export { createRegistry, createExecutor, loadAllRouters } from './core/api/app-loader.js'
+// Widget builder — for OpenBB Workspace frontend integration
+export { buildWidgetsJson } from './core/api/widgets.js'
+export { mountWidgetsEndpoint } from './core/api/rest-api.js'
+
// Pre-built providers (for direct import if needed)
export { fmpProvider } from './providers/fmp/index.js'
export { yfinanceProvider } from './providers/yfinance/index.js'
diff --git a/packages/opentypebb/src/providers/sec/index.ts b/packages/opentypebb/src/providers/sec/index.ts
new file mode 100644
index 0000000..941a228
--- /dev/null
+++ b/packages/opentypebb/src/providers/sec/index.ts
@@ -0,0 +1,18 @@
+/**
+ * SEC Provider.
+ *
+ * Source: https://www.sec.gov/
+ * Free, no API key required.
+ */
+
+import { Provider } from '../../core/provider/abstract/provider.js'
+import { SECEquitySearchFetcher } from './models/equity-search.js'
+
+export const secProvider = new Provider({
+ name: 'sec',
+ description: 'SEC EDGAR — US public company filings and data.',
+ website: 'https://www.sec.gov/',
+ fetcherDict: {
+ EquitySearch: SECEquitySearchFetcher,
+ },
+})
diff --git a/packages/opentypebb/src/providers/sec/models/equity-search.ts b/packages/opentypebb/src/providers/sec/models/equity-search.ts
new file mode 100644
index 0000000..b9014a7
--- /dev/null
+++ b/packages/opentypebb/src/providers/sec/models/equity-search.ts
@@ -0,0 +1,89 @@
+/**
+ * SEC Equity Search Fetcher.
+ *
+ * Fetches the full company tickers list from SEC EDGAR (free, no API key).
+ * Source: https://www.sec.gov/files/company_tickers.json
+ *
+ * The JSON is a dict keyed by index: { "0": { cik_str, ticker, title }, ... }
+ * ~10,000 entries, sorted by market cap.
+ */
+
+import { z } from 'zod'
+import { Fetcher } from '../../../core/provider/abstract/fetcher.js'
+import { amakeRequest } from '../../../core/provider/utils/helpers.js'
+import { EquitySearchQueryParamsSchema, EquitySearchDataSchema } from '../../../standard-models/equity-search.js'
+
+// ==================== Provider-specific schemas ====================
+
+export const SECEquitySearchQueryParamsSchema = EquitySearchQueryParamsSchema.extend({
+ use_cache: z.boolean().default(true).describe('Whether to use the cache or not.'),
+ is_fund: z.boolean().default(false).describe('Whether to search the mutual funds/ETFs list.'),
+})
+
+export type SECEquitySearchQueryParams = z.infer
+
+export const SECEquitySearchDataSchema = EquitySearchDataSchema.extend({
+ cik: z.string().describe('Central Index Key'),
+})
+
+export type SECEquitySearchData = z.infer
+
+// ==================== Raw SEC JSON shape ====================
+
+interface SECTickerEntry {
+ cik_str: number
+ ticker: string
+ title: string
+}
+
+// ==================== Fetcher ====================
+
+const SEC_URL = 'https://www.sec.gov/files/company_tickers.json'
+const SEC_HEADERS = {
+ 'User-Agent': 'OpenTypeBB/1.0 contact@example.com',
+ 'Accept-Encoding': 'gzip, deflate',
+}
+
+export class SECEquitySearchFetcher extends Fetcher {
+ static override requireCredentials = false
+
+ static override transformQuery(params: Record): SECEquitySearchQueryParams {
+ return SECEquitySearchQueryParamsSchema.parse(params)
+ }
+
+ static override async extractData(
+ _query: SECEquitySearchQueryParams,
+ _credentials: Record | null,
+ ): Promise {
+ const raw = await amakeRequest>(SEC_URL, {
+ headers: SEC_HEADERS,
+ })
+
+ // raw is { "0": { cik_str, ticker, title }, "1": ... }
+ return Object.values(raw)
+ }
+
+ static override transformData(
+ query: SECEquitySearchQueryParams,
+ data: SECTickerEntry[],
+ ): SECEquitySearchData[] {
+ const q = query.query.toLowerCase()
+
+ // If empty query, return all (for bulk loading by SymbolIndex)
+ const filtered = q
+ ? data.filter((d) =>
+ d.ticker.toLowerCase().includes(q) ||
+ d.title.toLowerCase().includes(q) ||
+ String(d.cik_str).includes(q),
+ )
+ : data
+
+ return filtered.map((d) =>
+ SECEquitySearchDataSchema.parse({
+ symbol: d.ticker,
+ name: d.title,
+ cik: String(d.cik_str),
+ }),
+ )
+ }
+}
diff --git a/packages/opentypebb/src/standard-models/equity-search.ts b/packages/opentypebb/src/standard-models/equity-search.ts
new file mode 100644
index 0000000..ef6d107
--- /dev/null
+++ b/packages/opentypebb/src/standard-models/equity-search.ts
@@ -0,0 +1,20 @@
+/**
+ * Equity Search Standard Model.
+ * Maps to: openbb_core/provider/standard_models/equity_search.py
+ */
+
+import { z } from 'zod'
+
+export const EquitySearchQueryParamsSchema = z.object({
+ query: z.string().default('').describe('Search query.'),
+ is_symbol: z.boolean().default(false).describe('Whether to search by ticker symbol.'),
+}).passthrough()
+
+export type EquitySearchQueryParams = z.infer
+
+export const EquitySearchDataSchema = z.object({
+ symbol: z.string().nullable().default(null).describe('Symbol of the company.'),
+ name: z.string().nullable().default(null).describe('Name of the company.'),
+}).passthrough()
+
+export type EquitySearchData = z.infer
diff --git a/packages/opentypebb/src/standard-models/index.ts b/packages/opentypebb/src/standard-models/index.ts
index dab7f62..ad6453f 100644
--- a/packages/opentypebb/src/standard-models/index.ts
+++ b/packages/opentypebb/src/standard-models/index.ts
@@ -823,3 +823,12 @@ export {
ChokepointVolumeDataSchema,
type ChokepointVolumeData,
} from './chokepoint-volume.js'
+
+// --- Equity Search ---
+
+export {
+ EquitySearchQueryParamsSchema,
+ type EquitySearchQueryParams,
+ EquitySearchDataSchema,
+ type EquitySearchData,
+} from './equity-search.js'
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4a33103..a12de96 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -28,7 +28,7 @@ importers:
version: 2.0.2(grammy@1.40.0)
'@hono/node-server':
specifier: ^1.19.11
- version: 1.19.11(hono@4.12.5)
+ version: 1.19.11(hono@4.12.7)
'@modelcontextprotocol/sdk':
specifier: ^1.26.0
version: 1.26.0(zod@4.3.6)
@@ -63,8 +63,8 @@ importers:
specifier: ^1.40.0
version: 1.40.0
hono:
- specifier: ^4.12.5
- version: 4.12.5
+ specifier: ^4.12.7
+ version: 4.12.7
json5:
specifier: ^2.2.3
version: 2.2.3
@@ -1803,10 +1803,6 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
- hono@4.12.5:
- resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==}
- engines: {node: '>=16.9.0'}
-
hono@4.12.7:
resolution: {integrity: sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==}
engines: {node: '>=16.9.0'}
@@ -3285,10 +3281,6 @@ snapshots:
'@grammyjs/types@3.24.0': {}
- '@hono/node-server@1.19.11(hono@4.12.5)':
- dependencies:
- hono: 4.12.5
-
'@hono/node-server@1.19.11(hono@4.12.7)':
dependencies:
hono: 4.12.7
@@ -3422,7 +3414,7 @@ snapshots:
'@modelcontextprotocol/sdk@1.26.0(zod@4.3.6)':
dependencies:
- '@hono/node-server': 1.19.11(hono@4.12.5)
+ '@hono/node-server': 1.19.11(hono@4.12.7)
ajv: 8.18.0
ajv-formats: 3.0.1(ajv@8.18.0)
content-type: 1.0.5
@@ -3432,7 +3424,7 @@ snapshots:
eventsource-parser: 3.0.6
express: 5.2.1
express-rate-limit: 8.2.1(express@5.2.1)
- hono: 4.12.5
+ hono: 4.12.7
jose: 6.1.3
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
@@ -4402,8 +4394,6 @@ snapshots:
highlight.js@11.11.1: {}
- hono@4.12.5: {}
-
hono@4.12.7: {}
http-errors@2.0.1:
diff --git a/src/connectors/web/routes/config.ts b/src/connectors/web/routes/config.ts
index 6e3c55d..3353046 100644
--- a/src/connectors/web/routes/config.ts
+++ b/src/connectors/web/routes/config.ts
@@ -40,8 +40,8 @@ export function createConfigRoutes(opts?: ConfigRouteOpts) {
}
const body = await c.req.json()
const validated = await writeConfigSection(section, body)
- // Hot-reload connectors when their config changes
- if (section === 'connectors') {
+ // Hot-reload connectors / OpenBB server when their config changes
+ if (section === 'connectors' || section === 'openbb') {
await opts?.onConnectorsChange?.()
}
return c.json(validated)
diff --git a/src/core/config.ts b/src/core/config.ts
index a8605b7..66bbb37 100644
--- a/src/core/config.ts
+++ b/src/core/config.ts
@@ -124,9 +124,9 @@ const openbbSchema = z.object({
}).default({}),
dataBackend: z.enum(['sdk', 'openbb']).default('sdk'),
apiServer: z.object({
- enabled: z.boolean().default(false),
+ enabled: z.boolean().default(true),
port: z.number().int().min(1024).max(65535).default(6901),
- }).default({ enabled: false, port: 6901 }),
+ }).default({ enabled: true, port: 6901 }),
})
const compactionSchema = z.object({
diff --git a/src/extension/trading/providers/alpaca/AlpacaAccount.ts b/src/extension/trading/providers/alpaca/AlpacaAccount.ts
index 4bbc942..1e3f616 100644
--- a/src/extension/trading/providers/alpaca/AlpacaAccount.ts
+++ b/src/extension/trading/providers/alpaca/AlpacaAccount.ts
@@ -52,9 +52,16 @@ export class AlpacaAccount implements ITradingAccount {
// ---- Lifecycle ----
private static readonly MAX_INIT_RETRIES = 5
+ private static readonly MAX_AUTH_RETRIES = 2
private static readonly INIT_RETRY_BASE_MS = 1000
async init(): Promise {
+ if (!this.config.apiKey || !this.config.secretKey) {
+ throw new Error(
+ `No API credentials configured. Set apiKey and apiSecret in accounts.json to enable this account.`,
+ )
+ }
+
this.client = new Alpaca({
keyId: this.config.apiKey,
secretKey: this.config.secretKey,
@@ -71,6 +78,13 @@ export class AlpacaAccount implements ITradingAccount {
return
} catch (err) {
lastErr = err
+ const isAuthError = err instanceof Error &&
+ /40[13]|forbidden|unauthorized/i.test(err.message)
+ if (isAuthError && attempt >= AlpacaAccount.MAX_AUTH_RETRIES) {
+ throw new Error(
+ `Authentication failed — verify your Alpaca API key and secret are correct.`,
+ )
+ }
if (attempt < AlpacaAccount.MAX_INIT_RETRIES) {
const delay = AlpacaAccount.INIT_RETRY_BASE_MS * 2 ** (attempt - 1)
console.warn(`AlpacaAccount[${this.id}]: init attempt ${attempt}/${AlpacaAccount.MAX_INIT_RETRIES} failed, retrying in ${delay}ms...`)
diff --git a/src/extension/trading/providers/ccxt/CcxtAccount.ts b/src/extension/trading/providers/ccxt/CcxtAccount.ts
index 6d2d48e..1d8ce96 100644
--- a/src/extension/trading/providers/ccxt/CcxtAccount.ts
+++ b/src/extension/trading/providers/ccxt/CcxtAccount.ts
@@ -105,6 +105,13 @@ export class CcxtAccount implements ITradingAccount {
// ---- Lifecycle ----
async init(): Promise {
+ if (this.readOnly) {
+ console.log(
+ `CcxtAccount[${this.id}]: no API credentials — running in market-data-only mode. ` +
+ `Set apiKey and apiSecret in accounts.json for trading.`,
+ )
+ }
+
// CCXT's fetchMarkets fires all market-type requests via Promise.all —
// a single failure kills the entire batch. Monkey-patch fetchMarkets to
// run each type sequentially with per-type retries.
@@ -144,7 +151,14 @@ export class CcxtAccount implements ITradingAccount {
}
// Now loadMarkets will use our sequential fetchMarkets
- await this.exchange.loadMarkets()
+ try {
+ await this.exchange.loadMarkets()
+ } catch (err) {
+ throw new Error(
+ `Failed to connect to ${this.exchangeName} — check network connectivity. ` +
+ `${err instanceof Error ? err.message : String(err)}`,
+ )
+ }
const marketCount = Object.keys(this.exchange.markets).length
if (marketCount === 0) {
diff --git a/src/main.ts b/src/main.ts
index ceae7be..01297e2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -31,7 +31,7 @@ import { OpenBBEquityClient } from './openbb/equity/client.js'
import { OpenBBCryptoClient } from './openbb/crypto/client.js'
import { OpenBBCurrencyClient } from './openbb/currency/client.js'
import { OpenBBNewsClient } from './openbb/news/client.js'
-import { startEmbeddedOpenBBServer } from './server/opentypebb.js'
+import { OpenBBServerPlugin } from './server/opentypebb.js'
import { createMarketSearchTools } from './extension/market/index.js'
import { createNewsTools } from './extension/news/index.js'
import { createAnalysisTools } from './extension/analysis-kit/index.js'
@@ -248,9 +248,7 @@ async function main() {
newsClient = new SDKNewsClient(executor, 'news', undefined, credentials, routeMap)
}
- if (config.openbb.apiServer.enabled) {
- startEmbeddedOpenBBServer(config.openbb.apiServer.port)
- }
+ // OpenBB API server is started later via optionalPlugins
// ==================== Equity Symbol Index ====================
@@ -443,6 +441,10 @@ async function main() {
}))
}
+ if (config.openbb.apiServer.enabled) {
+ optionalPlugins.set('openbb-server', new OpenBBServerPlugin({ port: config.openbb.apiServer.port }))
+ }
+
// ==================== Connector Reconnect ====================
let connectorsReconnecting = false
@@ -484,6 +486,30 @@ async function main() {
changes.push('telegram started')
}
+ // --- OpenBB API Server ---
+ const openbbWanted = fresh.openbb.apiServer.enabled
+ const openbbRunning = optionalPlugins.has('openbb-server')
+ if (openbbRunning && !openbbWanted) {
+ await optionalPlugins.get('openbb-server')!.stop()
+ optionalPlugins.delete('openbb-server')
+ changes.push('openbb-server stopped')
+ } else if (!openbbRunning && openbbWanted) {
+ const p = new OpenBBServerPlugin({ port: fresh.openbb.apiServer.port })
+ await p.start(ctx)
+ optionalPlugins.set('openbb-server', p)
+ changes.push('openbb-server started')
+ } else if (openbbRunning && openbbWanted) {
+ const current = optionalPlugins.get('openbb-server') as OpenBBServerPlugin
+ if (current.port !== fresh.openbb.apiServer.port) {
+ await current.stop()
+ optionalPlugins.delete('openbb-server')
+ const p = new OpenBBServerPlugin({ port: fresh.openbb.apiServer.port })
+ await p.start(ctx)
+ optionalPlugins.set('openbb-server', p)
+ changes.push(`openbb-server restarted on port ${fresh.openbb.apiServer.port}`)
+ }
+ }
+
if (changes.length > 0) {
console.log(`reconnect: connectors — ${changes.join(', ')}`)
}
@@ -533,6 +559,8 @@ async function main() {
'trading-ccxt',
)
console.log('ccxt: provider tools registered')
+ }).catch((err) => {
+ console.error('ccxt: background init failed:', err instanceof Error ? err.message : String(err))
})
// ==================== Shutdown ====================
diff --git a/src/openbb/equity/SymbolIndex.ts b/src/openbb/equity/SymbolIndex.ts
index a9d9a76..a27094f 100644
--- a/src/openbb/equity/SymbolIndex.ts
+++ b/src/openbb/equity/SymbolIndex.ts
@@ -36,7 +36,7 @@ interface CacheEnvelope {
// ==================== Config ====================
/** 免费 provider 列表 — 扩展时在这里加 */
-const SOURCES = ['sec', 'tmx'] as const
+const SOURCES = ['sec'] as const
const CACHE_FILE = resolve('data/cache/equity/symbols.json')
const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
diff --git a/src/server/opentypebb.ts b/src/server/opentypebb.ts
index c4f2888..e6e0de7 100644
--- a/src/server/opentypebb.ts
+++ b/src/server/opentypebb.ts
@@ -9,9 +9,47 @@
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { serve } from '@hono/node-server'
-import { createExecutor, loadAllRouters } from '@traderalice/opentypebb'
+import { createExecutor, createRegistry, loadAllRouters, buildWidgetsJson, mountWidgetsEndpoint } from '@traderalice/opentypebb'
+import type { Plugin, EngineContext } from '../core/types.js'
+
+export class OpenBBServerPlugin implements Plugin {
+ readonly name = 'openbb-server'
+ readonly port: number
+ private server: ReturnType | null = null
+
+ constructor(opts: { port: number }) {
+ this.port = opts.port
+ }
+
+ async start(_ctx: EngineContext): Promise {
+ const registry = createRegistry()
+ const executor = createExecutor()
+
+ const app = new Hono()
+ app.use(cors())
+ app.get('/api/v1/health', (c) => c.json({ status: 'ok' }))
+
+ const rootRouter = loadAllRouters()
+
+ const widgetsJson = buildWidgetsJson(rootRouter, registry)
+ mountWidgetsEndpoint(app, widgetsJson)
+ console.log(`[openbb] Built widgets.json with ${Object.keys(widgetsJson).length} widgets`)
+
+ rootRouter.mountToHono(app, executor)
+
+ this.server = serve({ fetch: app.fetch, port: this.port })
+ console.log(`[openbb] Embedded API server listening on http://localhost:${this.port}`)
+ }
+
+ async stop(): Promise {
+ this.server?.close()
+ this.server = null
+ console.log('[openbb] Embedded API server stopped')
+ }
+}
export function startEmbeddedOpenBBServer(port: number): void {
+ const registry = createRegistry()
const executor = createExecutor()
const app = new Hono()
@@ -19,6 +57,12 @@ export function startEmbeddedOpenBBServer(port: number): void {
app.get('/api/v1/health', (c) => c.json({ status: 'ok' }))
const rootRouter = loadAllRouters()
+
+ // Build and mount widgets.json for OpenBB Workspace frontend
+ const widgetsJson = buildWidgetsJson(rootRouter, registry)
+ mountWidgetsEndpoint(app, widgetsJson)
+ console.log(`[openbb] Built widgets.json with ${Object.keys(widgetsJson).length} widgets`)
+
rootRouter.mountToHono(app, executor)
serve({ fetch: app.fetch, port })