Skip to content

Commit bfe2e89

Browse files
Ameclaude
authored andcommitted
feat: dynamic CCXT credentials + single-page New Account wizard
Fixes inability to configure wallet-based exchanges like Hyperliquid: CcxtBroker previously hardcoded apiKey + secret + password fields, but each CCXT exchange declares its own requiredCredentials map (10 standard fields including walletAddress, privateKey, uid, twofa, etc.). Backend - CcxtBroker.configSchema accepts all 10 CCXT credential fields, with legacy `apiSecret` aliased to `secret` for back-compat - Constructor passes all set fields through to ccxt[exchange]() generically - init() validates via the exchange's own checkRequiredCredentials() and reports which fields are missing - New endpoints: GET /ccxt/exchanges (list all CCXT-supported exchanges) and GET /ccxt/exchanges/:name/credentials (read requiredCredentials) - BrokerRegistryEntry gets a setupGuide field; ccxt/alpaca/ibkr each have a multi-paragraph guide explaining what they are, how to get credentials, and prerequisites Frontend - TradingPage CreateWizard refactored from two-step to single-page form (consistent with EditDialog and other config dialogs); deleted StepIndicator and the sensitive/non-sensitive field split that put Hyperliquid's walletAddress and privateKey on different pages - Selecting a platform shows the broker's setupGuide as a description block before the configuration fields - For CCXT: exchange dropdown is populated dynamically from /ccxt/exchanges, and credential fields are loaded based on the selected exchange via /ccxt/exchanges/:name/credentials - EditDialog also wired with dynamic CCXT credential fetching - Bump version to 0.9.0-beta.11 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 40e1dbd commit bfe2e89

9 files changed

Lines changed: 276 additions & 126 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "open-alice",
3-
"version": "0.9.0-beta.10",
3+
"version": "0.9.0-beta.11",
44
"description": "File-based trading agent engine",
55
"type": "module",
66
"scripts": {

src/connectors/web/routes/trading-config.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
import { Hono } from 'hono'
2+
import ccxt from 'ccxt'
23
import type { EngineContext } from '../../../core/types.js'
34
import {
45
readAccountsConfig, writeAccountsConfig,
56
accountConfigSchema,
67
} from '../../../core/config.js'
78
import { createBroker } from '../../../domain/trading/brokers/factory.js'
89
import { BROKER_REGISTRY } from '../../../domain/trading/brokers/registry.js'
10+
import type { BrokerConfigField } from '../../../domain/trading/brokers/types.js'
11+
12+
// ==================== CCXT credential field metadata ====================
13+
14+
/** Map of CCXT standard credential field name → UI display metadata. */
15+
const CCXT_CREDENTIAL_LABELS: Record<string, { label: string; type: BrokerConfigField['type']; sensitive: boolean; placeholder?: string }> = {
16+
apiKey: { label: 'API Key', type: 'password', sensitive: true },
17+
secret: { label: 'API Secret', type: 'password', sensitive: true },
18+
uid: { label: 'User ID', type: 'text', sensitive: false },
19+
accountId: { label: 'Account ID', type: 'text', sensitive: false },
20+
login: { label: 'Login', type: 'text', sensitive: false },
21+
password: { label: 'Passphrase', type: 'password', sensitive: true, placeholder: 'Required by some exchanges (e.g. OKX)' },
22+
twofa: { label: '2FA Secret', type: 'password', sensitive: true },
23+
privateKey: { label: 'Private Key', type: 'password', sensitive: true, placeholder: 'Wallet private key (for Hyperliquid, dYdX, etc.)' },
24+
walletAddress: { label: 'Wallet Address', type: 'text', sensitive: false, placeholder: '0x...' },
25+
token: { label: 'Token', type: 'password', sensitive: true },
26+
}
927

1028
// ==================== Credential helpers ====================
1129

@@ -58,6 +76,7 @@ export function createTradingConfigRoutes(ctx: EngineContext) {
5876
type,
5977
name: entry.name,
6078
description: entry.description,
79+
setupGuide: entry.setupGuide,
6180
badge: entry.badge,
6281
badgeColor: entry.badgeColor,
6382
fields: entry.configFields,
@@ -67,6 +86,44 @@ export function createTradingConfigRoutes(ctx: EngineContext) {
6786
return c.json({ brokerTypes })
6887
})
6988

89+
// ==================== CCXT dynamic exchange + credential metadata ====================
90+
91+
/** List all CCXT-supported exchanges (dynamically from the ccxt package). */
92+
app.get('/ccxt/exchanges', (c) => {
93+
const exchanges = (ccxt as unknown as { exchanges: string[] }).exchanges ?? []
94+
return c.json({ exchanges })
95+
})
96+
97+
/** Return the credential fields a given CCXT exchange requires (read from its requiredCredentials map). */
98+
app.get('/ccxt/exchanges/:name/credentials', (c) => {
99+
const name = c.req.param('name')
100+
const exchanges = ccxt as unknown as Record<string, new (opts?: Record<string, unknown>) => { requiredCredentials?: Record<string, boolean> }>
101+
const ExchangeClass = exchanges[name]
102+
if (!ExchangeClass) return c.json({ error: `Unknown exchange: ${name}` }, 404)
103+
104+
try {
105+
const inst = new ExchangeClass()
106+
const required = inst.requiredCredentials ?? {}
107+
const fields: BrokerConfigField[] = []
108+
for (const [key, needed] of Object.entries(required)) {
109+
if (!needed) continue
110+
const meta = CCXT_CREDENTIAL_LABELS[key]
111+
if (!meta) continue // skip unknown credential names (CCXT may add new ones)
112+
fields.push({
113+
name: key,
114+
type: meta.type,
115+
label: meta.label,
116+
required: true,
117+
sensitive: meta.sensitive,
118+
placeholder: meta.placeholder,
119+
})
120+
}
121+
return c.json({ fields })
122+
} catch (err) {
123+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500)
124+
}
125+
})
126+
70127
// ==================== Read all ====================
71128

72129
app.get('/', async (c) => {

src/domain/trading/brokers/ccxt/CcxtBroker.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@ function makeSwapMarket(base: string, quote: string, symbol?: string): any {
7474
}
7575
}
7676

77-
function makeAccount(overrides?: Partial<{ exchange: string; apiKey: string; apiSecret: string }>) {
77+
function makeAccount(overrides?: Partial<{ exchange: string; apiKey: string; secret: string }>) {
7878
return new CcxtBroker({
7979
exchange: overrides?.exchange ?? 'bybit',
8080
apiKey: overrides?.apiKey ?? 'k',
81-
apiSecret: overrides?.apiSecret ?? 's',
81+
secret: overrides?.secret ?? 's',
8282
sandbox: false,
8383
})
8484
}
@@ -92,7 +92,7 @@ function setInitialized(acc: CcxtBroker, markets: Record<string, any>) {
9292

9393
describe('CcxtBroker — constructor', () => {
9494
it('throws for unknown exchange', () => {
95-
expect(() => new CcxtBroker({ exchange: 'unknownxyz', apiKey: 'k', apiSecret: 's', sandbox: false })).toThrow(
95+
expect(() => new CcxtBroker({ exchange: 'unknownxyz', apiKey: 'k', secret: 's', sandbox: false })).toThrow(
9696
'Unknown CCXT exchange',
9797
)
9898
})
@@ -853,9 +853,9 @@ describe('CcxtBroker — getAccount', () => {
853853
})
854854

855855
it('throws BrokerError when no API credentials', async () => {
856-
const acc = new CcxtBroker({ exchange: 'bybit', apiKey: '', apiSecret: '', sandbox: false })
856+
const acc = new CcxtBroker({ exchange: 'bybit', apiKey: '', secret: '', sandbox: false })
857857

858-
await expect(acc.init()).rejects.toThrow('No API credentials configured')
858+
await expect(acc.init()).rejects.toThrow(/requires credentials/)
859859
})
860860
})
861861

src/domain/trading/brokers/ccxt/CcxtBroker.ts

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
type TpSlParams,
2626
} from '../types.js'
2727
import '../../contract-ext.js'
28-
import type { CcxtBrokerConfig, CcxtMarket, FundingRate, OrderBook, OrderBookLevel } from './ccxt-types.js'
28+
import { CCXT_CREDENTIAL_FIELDS, type CcxtBrokerConfig, type CcxtMarket, type FundingRate, type OrderBook, type OrderBookLevel } from './ccxt-types.js'
2929
import { MAX_INIT_RETRIES, INIT_RETRY_BASE_MS } from './ccxt-types.js'
3030
import {
3131
ccxtTypeToSecType,
@@ -69,21 +69,27 @@ export class CcxtBroker implements IBroker<CcxtBrokerMeta> {
6969
sandbox: z.boolean().default(false),
7070
demoTrading: z.boolean().default(false),
7171
options: z.record(z.string(), z.unknown()).optional(),
72+
// All 10 CCXT standard credential fields, all optional.
73+
// Each exchange requires its own subset (read via Exchange.requiredCredentials).
7274
apiKey: z.string().optional(),
73-
apiSecret: z.string().optional(),
75+
secret: z.string().optional(),
76+
apiSecret: z.string().optional(), // legacy alias for `secret`
77+
uid: z.string().optional(),
78+
accountId: z.string().optional(),
79+
login: z.string().optional(),
7480
password: z.string().optional(),
81+
twofa: z.string().optional(),
82+
privateKey: z.string().optional(),
83+
walletAddress: z.string().optional(),
84+
token: z.string().optional(),
7585
})
7686

87+
// Static base fields. Exchange dropdown options + per-exchange credential fields
88+
// are fetched dynamically by the frontend (see /api/trading/config/ccxt/* routes).
7789
static configFields: BrokerConfigField[] = [
78-
{ name: 'exchange', type: 'select', label: 'Exchange', required: true, options: [
79-
'binance', 'bybit', 'okx', 'bitget', 'gate', 'kucoin', 'coinbase',
80-
'kraken', 'htx', 'mexc', 'bingx', 'phemex', 'woo', 'hyperliquid',
81-
].map(e => ({ value: e, label: e.charAt(0).toUpperCase() + e.slice(1) })) },
90+
{ name: 'exchange', type: 'select', label: 'Exchange', required: true, options: [] },
8291
{ name: 'sandbox', type: 'boolean', label: 'Sandbox Mode', default: false },
8392
{ name: 'demoTrading', type: 'boolean', label: 'Demo Trading', default: false },
84-
{ name: 'apiKey', type: 'password', label: 'API Key', required: true, sensitive: true },
85-
{ name: 'apiSecret', type: 'password', label: 'API Secret', required: true, sensitive: true },
86-
{ name: 'password', type: 'password', label: 'Password', placeholder: 'Required by some exchanges (e.g. OKX)', sensitive: true },
8793
]
8894

8995
static fromConfig(config: { id: string; label?: string; brokerConfig: Record<string, unknown> }): CcxtBroker {
@@ -95,9 +101,17 @@ export class CcxtBroker implements IBroker<CcxtBrokerMeta> {
95101
sandbox: bc.sandbox,
96102
demoTrading: bc.demoTrading,
97103
options: bc.options,
98-
apiKey: bc.apiKey ?? '',
99-
apiSecret: bc.apiSecret ?? '',
104+
apiKey: bc.apiKey,
105+
// Accept both `secret` (CCXT-native) and legacy `apiSecret`
106+
secret: bc.secret ?? bc.apiSecret,
107+
uid: bc.uid,
108+
accountId: bc.accountId,
109+
login: bc.login,
100110
password: bc.password,
111+
twofa: bc.twofa,
112+
privateKey: bc.privateKey,
113+
walletAddress: bc.walletAddress,
114+
token: bc.token,
101115
})
102116
}
103117

@@ -133,12 +147,14 @@ export class CcxtBroker implements IBroker<CcxtBrokerMeta> {
133147
}
134148
const mergedOptions = { ...defaultOptions, ...config.options }
135149

136-
this.exchange = new ExchangeClass({
137-
apiKey: config.apiKey,
138-
secret: config.apiSecret,
139-
password: config.password,
140-
options: mergedOptions,
141-
})
150+
// Pass through all CCXT standard credential fields. CCXT ignores undefined.
151+
const cfgRecord = config as unknown as Record<string, unknown>
152+
const credentials: Record<string, unknown> = { options: mergedOptions }
153+
for (const field of CCXT_CREDENTIAL_FIELDS) {
154+
const v = cfgRecord[field]
155+
if (v !== undefined) credentials[field] = v
156+
}
157+
this.exchange = new ExchangeClass(credentials)
142158

143159
if (config.sandbox) {
144160
this.exchange.setSandboxMode(true)
@@ -164,10 +180,18 @@ export class CcxtBroker implements IBroker<CcxtBrokerMeta> {
164180
// ---- Lifecycle ----
165181

166182
async init(): Promise<void> {
167-
if (!this.exchange.apiKey || !this.exchange.secret) {
183+
// Validate credentials per the exchange's own requiredCredentials map.
184+
// Hyperliquid needs walletAddress + privateKey; OKX needs apiKey + secret + password; etc.
185+
try {
186+
this.exchange.checkRequiredCredentials()
187+
} catch (err) {
188+
const required = Object.entries(this.exchange.requiredCredentials ?? {})
189+
.filter(([, needed]) => needed)
190+
.map(([k]) => k)
191+
const missing = required.filter(k => !(this.exchange as unknown as Record<string, unknown>)[k])
168192
throw new BrokerError(
169193
'CONFIG',
170-
`No API credentials configured. Set apiKey and apiSecret in accounts.json to enable this account.`,
194+
`${this.exchangeName} requires credentials: ${required.join(', ')}. Missing: ${missing.join(', ') || 'unknown'}. (${err instanceof Error ? err.message : String(err)})`,
171195
)
172196
}
173197

src/domain/trading/brokers/ccxt/ccxt-types.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,30 @@ export interface CcxtBrokerConfig {
22
id?: string
33
label?: string
44
exchange: string
5-
apiKey: string
6-
apiSecret: string
7-
password?: string
85
sandbox: boolean
96
demoTrading?: boolean
107
options?: Record<string, unknown>
8+
// CCXT standard credential fields (all optional — each exchange requires a different subset)
9+
apiKey?: string
10+
secret?: string
11+
uid?: string
12+
accountId?: string
13+
login?: string
14+
password?: string
15+
twofa?: string
16+
privateKey?: string
17+
walletAddress?: string
18+
token?: string
1119
}
1220

21+
/** CCXT standard credential field names (matches base Exchange.requiredCredentials map). */
22+
export const CCXT_CREDENTIAL_FIELDS = [
23+
'apiKey', 'secret', 'uid', 'accountId', 'login',
24+
'password', 'twofa', 'privateKey', 'walletAddress', 'token',
25+
] as const
26+
27+
export type CcxtCredentialField = typeof CCXT_CREDENTIAL_FIELDS[number]
28+
1329
export interface CcxtMarket {
1430
id: string // exchange-native symbol, e.g. "BTCUSDT"
1531
symbol: string // CCXT unified format, e.g. "BTC/USDT:USDT"

src/domain/trading/brokers/registry.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export interface BrokerRegistryEntry {
4545
subtitleFields: SubtitleField[]
4646
/** Guard category — determines which guard types are available */
4747
guardCategory: 'crypto' | 'securities'
48+
/** Multi-line setup guide shown in the New Account wizard. Paragraphs separated by `\n\n`. */
49+
setupGuide?: string
4850
}
4951

5052
// ==================== Registry ====================
@@ -64,6 +66,13 @@ export const BROKER_REGISTRY: Record<string, BrokerRegistryEntry> = {
6466
{ field: 'sandbox', label: 'Sandbox' },
6567
],
6668
guardCategory: 'crypto',
69+
setupGuide: `CCXT is a unified library that connects to 100+ cryptocurrency exchanges through a single API. After picking a specific exchange below, the form will auto-load the credential fields that exchange requires.
70+
71+
Most exchanges (Binance, Bybit, OKX, etc.) use API key + secret — you can create them in your exchange account's API settings. OKX additionally requires a passphrase you set when creating the key.
72+
73+
Wallet-based exchanges like Hyperliquid use a wallet address + private key instead. For Hyperliquid, you can generate a dedicated API wallet at app.hyperliquid.xyz/API to avoid exposing your main wallet's private key.
74+
75+
Make sure to grant only the permissions you need (read + trade), and never enable withdrawal permissions on automated trading keys.`,
6776
},
6877
alpaca: {
6978
configSchema: AlpacaBroker.configSchema,
@@ -77,6 +86,9 @@ export const BROKER_REGISTRY: Record<string, BrokerRegistryEntry> = {
7786
{ field: 'paper', label: 'Paper Trading', falseLabel: 'Live Trading' },
7887
],
7988
guardCategory: 'securities',
89+
setupGuide: `Alpaca is a commission-free US equities broker with a clean REST API. It supports paper trading (free, simulated) and live trading.
90+
91+
Sign up at alpaca.markets, then create API keys from the dashboard. Toggle "Paper" on this form to use the paper trading endpoint with your paper keys, or off for live trading with your live keys (different key sets).`,
8092
},
8193
ibkr: {
8294
configSchema: IbkrBroker.configSchema,
@@ -91,5 +103,14 @@ export const BROKER_REGISTRY: Record<string, BrokerRegistryEntry> = {
91103
{ field: 'port' },
92104
],
93105
guardCategory: 'securities',
106+
setupGuide: `Interactive Brokers requires a local TWS (Trader Workstation) or IB Gateway process running on your machine. OpenAlice connects to it over a TCP socket — no API key needed, authentication happens via TWS login.
107+
108+
Before connecting:
109+
1. Open TWS / IB Gateway and log in to your paper or live account
110+
2. Enable API access: File → Global Configuration → API → Settings → "Enable ActiveX and Socket Clients"
111+
3. Note the socket port (paper: 7497, live: 7496)
112+
4. Add 127.0.0.1 to "Trusted IPs" if running locally
113+
114+
Paper trading requires a separate paper account login in TWS.`,
94115
},
95116
}

ui/src/api/trading.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { fetchJson } from './client'
2-
import type { TradingAccount, AccountSummary, AccountInfo, Position, WalletCommitLog, ReconnectResult, AccountConfig, WalletStatus, WalletPushResult, WalletRejectResult, TestConnectionResult, BrokerTypeInfo, UTASnapshotSummary, EquityCurvePoint } from './types'
2+
import type { TradingAccount, AccountSummary, AccountInfo, Position, WalletCommitLog, ReconnectResult, AccountConfig, WalletStatus, WalletPushResult, WalletRejectResult, TestConnectionResult, BrokerTypeInfo, BrokerConfigField, UTASnapshotSummary, EquityCurvePoint } from './types'
33

44
// ==================== Unified Trading API ====================
55

@@ -91,6 +91,14 @@ export const tradingApi = {
9191
return fetchJson('/api/trading/config/broker-types')
9292
},
9393

94+
async getCcxtExchanges(): Promise<{ exchanges: string[] }> {
95+
return fetchJson('/api/trading/config/ccxt/exchanges')
96+
},
97+
98+
async getCcxtCredentialFields(exchange: string): Promise<{ fields: BrokerConfigField[] }> {
99+
return fetchJson(`/api/trading/config/ccxt/exchanges/${encodeURIComponent(exchange)}/credentials`)
100+
},
101+
94102
// ==================== Trading Config CRUD ====================
95103

96104
async loadTradingConfig(): Promise<{ accounts: AccountConfig[] }> {

ui/src/api/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ export interface BrokerTypeInfo {
332332
type: string
333333
name: string
334334
description: string
335+
/** Multi-line setup guide shown in the New Account wizard. Paragraphs separated by `\n\n`. */
336+
setupGuide?: string
335337
badge: string
336338
badgeColor: string
337339
fields: BrokerConfigField[]

0 commit comments

Comments
 (0)