Supports Interactive Brokers, US and Hong Kong stocks.

Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
This commit is contained in:
TIANHE
2026-01-13 02:35:48 +08:00
parent 714dd47c86
commit 7527d73f25
20 changed files with 2250 additions and 148 deletions
+25 -2
View File
@@ -1324,8 +1324,30 @@ const locale = {
'trading-assistant.form.slippageHint': 'Estimated slippage percentage (optional)',
'trading-assistant.form.executionMode': 'Execution',
'trading-assistant.form.executionModeSignal': 'Signal only (push notifications)',
'trading-assistant.form.executionModeLive': 'Live trading (Crypto only)',
'trading-assistant.form.executionModeLive': 'Live trading',
'trading-assistant.form.liveTradingCryptoOnlyHint': 'Live trading is available for Crypto only. Other markets can only push signals.',
'trading-assistant.form.liveTradingNotSupportedHint': 'Live trading is not available for this market',
'trading-assistant.form.broker': 'Broker',
'trading-assistant.form.ibkrConnectionTitle': 'Interactive Brokers Connection',
'trading-assistant.form.ibkrConnectionHint': 'Make sure TWS or IB Gateway is running with API enabled',
'trading-assistant.validation.brokerRequired': 'Please select a broker',
'trading-assistant.placeholders.selectBroker': 'Select broker',
'trading-assistant.brokerNames': {
'ibkr': 'Interactive Brokers (IBKR)',
'futu': 'Futu Securities',
'tiger': 'Tiger Brokers',
'td': 'TD Ameritrade',
'schwab': 'Charles Schwab'
},
'trading-assistant.form.ibkrHost': 'Host',
'trading-assistant.form.ibkrPort': 'Port',
'trading-assistant.form.ibkrPortHint': 'TWS Live:7497, TWS Paper:7496, Gateway Live:4001, Gateway Paper:4002',
'trading-assistant.form.ibkrClientId': 'Client ID',
'trading-assistant.form.ibkrAccount': 'Account',
'trading-assistant.form.ibkrAccountHint': 'Leave empty to auto-select first account. Specify for multi-account users.',
'trading-assistant.placeholders.ibkrAccount': 'Optional, e.g. U1234567',
'trading-assistant.exchange.ibkrConnectionSuccess': 'IBKR connected successfully',
'trading-assistant.exchange.ibkrConnectionFailed': 'IBKR connection failed. Please check if TWS/Gateway is running.',
'trading-assistant.form.notifyChannels': 'Notification Channels',
'trading-assistant.form.notifyChannelsHint': 'Choose how you want to receive buy/sell and risk-management signals.',
'trading-assistant.notify.browser': 'Browser',
@@ -1520,7 +1542,8 @@ const locale = {
'ftxus': 'FTX US',
'binanceus': 'Binance US',
'binancecoinm': 'Binance COIN-M',
'binanceusdm': 'Binance USDⓈ-M'
'binanceusdm': 'Binance USDⓈ-M',
'ibkr': 'Interactive Brokers (IBKR)'
},
'ai-trading-assistant.title': 'AI Trading Assistant',
'ai-trading-assistant.strategyList': 'Strategy List',
+24 -1
View File
@@ -1204,6 +1204,28 @@ const locale = {
'trading-assistant.form.executionModeSignal': '仅信号通知',
'trading-assistant.form.executionModeLive': '实盘自动交易',
'trading-assistant.form.liveTradingCryptoOnlyHint': '实盘交易功能仅支持加密货币市场',
'trading-assistant.form.liveTradingNotSupportedHint': '当前市场不支持实盘交易',
'trading-assistant.form.broker': '券商',
'trading-assistant.form.ibkrConnectionTitle': '盈透证券连接配置',
'trading-assistant.form.ibkrConnectionHint': '请确保 TWS 或 IB Gateway 已启动并启用 API 连接',
'trading-assistant.validation.brokerRequired': '请选择券商',
'trading-assistant.placeholders.selectBroker': '选择券商',
'trading-assistant.brokerNames': {
'ibkr': '盈透证券 (Interactive Brokers)',
'futu': '富途证券 (Futu)',
'tiger': '老虎证券 (Tiger Brokers)',
'td': 'TD Ameritrade',
'schwab': 'Charles Schwab'
},
'trading-assistant.form.ibkrHost': '主机地址',
'trading-assistant.form.ibkrPort': '端口',
'trading-assistant.form.ibkrPortHint': 'TWS实盘:7497, TWS模拟:7496, Gateway实盘:4001, Gateway模拟:4002',
'trading-assistant.form.ibkrClientId': '客户端ID',
'trading-assistant.form.ibkrAccount': '账户号',
'trading-assistant.form.ibkrAccountHint': '留空自动选择第一个账户,多账户用户可指定账户号',
'trading-assistant.placeholders.ibkrAccount': '可选,如 U1234567',
'trading-assistant.exchange.ibkrConnectionSuccess': '盈透证券连接成功',
'trading-assistant.exchange.ibkrConnectionFailed': '盈透证券连接失败,请检查 TWS/Gateway 是否运行',
'trading-assistant.form.notifyChannels': '通知渠道',
'trading-assistant.form.notifyChannelsHint': '选择信号触发时的通知方式',
'trading-assistant.form.notifyEmail': '邮箱地址',
@@ -1414,7 +1436,8 @@ const locale = {
'ftxus': 'FTX US',
'binanceus': 'Binance US',
'binancecoinm': 'Binance COIN-M',
'binanceusdm': 'Binance USDⓈ-M'
'binanceusdm': 'Binance USDⓈ-M',
'ibkr': '盈透证券 (IBKR)'
},
'ai-trading-assistant.title': 'AI交易助手',
'ai-trading-assistant.strategyList': '策略列表',
@@ -874,14 +874,14 @@
<a-form-item :label="$t('trading-assistant.form.executionMode')">
<a-radio-group
v-decorator="['execution_mode', { initialValue: 'signal' }]"
:disabled="!isCryptoMarket"
:disabled="!canUseLiveTrading"
@change="onExecutionModeChange"
>
<a-radio value="signal">{{ $t('trading-assistant.form.executionModeSignal') }}</a-radio>
<a-radio value="live" :disabled="!isCryptoMarket">{{ $t('trading-assistant.form.executionModeLive') }}</a-radio>
<a-radio value="live" :disabled="!canUseLiveTrading">{{ $t('trading-assistant.form.executionModeLive') }}</a-radio>
</a-radio-group>
<div v-if="!isCryptoMarket" class="form-item-hint" style="color: #ff9800;">
{{ $t('trading-assistant.form.liveTradingCryptoOnlyHint') }}
<div v-if="!canUseLiveTrading" class="form-item-hint" style="color: #ff9800;">
{{ $t('trading-assistant.form.liveTradingNotSupportedHint') }}
</div>
</a-form-item>
@@ -950,10 +950,10 @@
/>
</a-form-item>
<a-divider v-if="executionModeUi === 'live' && isCryptoMarket" />
<a-divider v-if="executionModeUi === 'live' && canUseLiveTrading" />
<!-- Live trading: exchange credentials (crypto only) -->
<div v-if="executionModeUi === 'live' && isCryptoMarket">
<!-- Live trading: exchange credentials -->
<div v-if="executionModeUi === 'live' && canUseLiveTrading">
<a-alert
type="info"
show-icon
@@ -962,98 +962,179 @@
:description="$t('trading-assistant.form.liveTradingConfigHint')"
/>
<a-form-item :label="$t('trading-assistant.form.savedCredential')">
<a-select
v-decorator="['credential_id', { getValueFromEvent: (val) => val || undefined }]"
:placeholder="$t('trading-assistant.placeholders.selectSavedCredential')"
allow-clear
show-search
option-filter-prop="children"
:loading="loadingExchangeCredentials"
@change="handleCredentialSelectChange"
>
<a-select-option
v-for="cred in exchangeCredentials"
:key="cred.id"
:value="cred.id"
<!-- ========== Broker Configuration (US/HK Stocks) ========== -->
<template v-if="isIBKRMarket">
<a-form-item :label="$t('trading-assistant.form.broker')">
<a-select
v-decorator="['broker_id', {
initialValue: 'ibkr',
rules: [{ required: true, message: $t('trading-assistant.validation.brokerRequired') }]
}]"
:placeholder="$t('trading-assistant.placeholders.selectBroker')"
:getPopupContainer="getModalPopupContainer"
@change="handleBrokerSelectChange"
>
{{ formatCredentialLabel(cred) }}
</a-select-option>
</a-select>
<div class="form-item-hint">{{ $t('trading-assistant.form.savedCredentialHint') }}</div>
</a-form-item>
<a-select-option
v-for="broker in brokerOptions"
:key="broker.value"
:value="broker.value"
>
{{ broker.displayName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('trading-assistant.form.exchange')">
<a-select
v-decorator="['exchange_id', {
rules: [{ required: true, message: $t('trading-assistant.validation.exchangeRequired') }],
getValueFromEvent: (val) => val || undefined
}]"
:placeholder="$t('trading-assistant.placeholders.selectExchange')"
allow-clear
show-search
option-filter-prop="children"
@change="handleExchangeSelectChange"
>
<a-select-option
v-for="exchange in formattedExchangeOptions"
:key="exchange.value"
:value="exchange.value"
<!-- IBKR specific configuration -->
<template v-if="currentBrokerId === 'ibkr'">
<a-alert
type="info"
show-icon
style="margin-bottom: 16px;"
:message="$t('trading-assistant.form.ibkrConnectionTitle')"
:description="$t('trading-assistant.form.ibkrConnectionHint')"
/>
<a-form-item :label="$t('trading-assistant.form.ibkrHost')">
<a-input
v-decorator="['ibkr_host', { initialValue: '127.0.0.1' }]"
placeholder="127.0.0.1"
@change="handleApiConfigChange"
/>
</a-form-item>
<a-form-item :label="$t('trading-assistant.form.ibkrPort')">
<a-input-number
v-decorator="['ibkr_port', { initialValue: 7497 }]"
placeholder="7497"
:min="1"
:max="65535"
style="width: 100%"
@change="handleApiConfigChange"
/>
<div class="form-item-hint">{{ $t('trading-assistant.form.ibkrPortHint') }}</div>
</a-form-item>
<a-form-item :label="$t('trading-assistant.form.ibkrClientId')">
<a-input-number
v-decorator="['ibkr_client_id', { initialValue: 1 }]"
placeholder="1"
:min="1"
:max="999"
style="width: 100%"
@change="handleApiConfigChange"
/>
</a-form-item>
<a-form-item :label="$t('trading-assistant.form.ibkrAccount')">
<a-input
v-decorator="['ibkr_account', { initialValue: '' }]"
:placeholder="$t('trading-assistant.placeholders.ibkrAccount')"
@change="handleApiConfigChange"
/>
<div class="form-item-hint">{{ $t('trading-assistant.form.ibkrAccountHint') }}</div>
</a-form-item>
</template>
<!-- Future broker configurations can be added here -->
<!-- <template v-else-if="currentBrokerId === 'futu'">...</template> -->
</template>
<!-- ========== Crypto Exchange Configuration ========== -->
<template v-else>
<a-form-item :label="$t('trading-assistant.form.savedCredential')">
<a-select
v-decorator="['credential_id', { getValueFromEvent: (val) => val || undefined }]"
:placeholder="$t('trading-assistant.placeholders.selectSavedCredential')"
allow-clear
show-search
option-filter-prop="children"
:loading="loadingExchangeCredentials"
@change="handleCredentialSelectChange"
>
{{ exchange.displayName }}
</a-select-option>
</a-select>
</a-form-item>
<a-select-option
v-for="cred in exchangeCredentials"
:key="cred.id"
:value="cred.id"
>
{{ formatCredentialLabel(cred) }}
</a-select-option>
</a-select>
<div class="form-item-hint">{{ $t('trading-assistant.form.savedCredentialHint') }}</div>
</a-form-item>
<a-form-item :label="$t('trading-assistant.form.apiKey')">
<a-input-password
v-decorator="['api_key', { rules: [{ required: true, message: $t('trading-assistant.validation.apiKeyRequired') }] }]"
:placeholder="$t('trading-assistant.placeholders.inputApiKey')"
autocomplete="new-password"
@change="handleApiConfigChange"
/>
</a-form-item>
<a-form-item :label="$t('trading-assistant.form.exchange')">
<a-select
v-decorator="['exchange_id', {
rules: [{ required: true, message: $t('trading-assistant.validation.exchangeRequired') }],
getValueFromEvent: (val) => val || undefined
}]"
:placeholder="$t('trading-assistant.placeholders.selectExchange')"
allow-clear
show-search
option-filter-prop="children"
@change="handleExchangeSelectChange"
>
<a-select-option
v-for="exchange in cryptoExchangeOptions"
:key="exchange.value"
:value="exchange.value"
>
{{ exchange.displayName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('trading-assistant.form.secretKey')">
<a-input-password
v-decorator="['secret_key', { rules: [{ required: true, message: $t('trading-assistant.validation.secretKeyRequired') }] }]"
:placeholder="$t('trading-assistant.placeholders.inputSecretKey')"
autocomplete="new-password"
@change="handleApiConfigChange"
/>
</a-form-item>
<a-form-item :label="$t('trading-assistant.form.apiKey')">
<a-input-password
v-decorator="['api_key', { rules: [{ required: true, message: $t('trading-assistant.validation.apiKeyRequired') }] }]"
:placeholder="$t('trading-assistant.placeholders.inputApiKey')"
autocomplete="new-password"
@change="handleApiConfigChange"
/>
</a-form-item>
<a-form-item
v-if="needsPassphrase"
:label="$t('trading-assistant.form.passphrase')"
>
<a-input-password
v-decorator="['passphrase', { rules: [{ required: true, message: $t('trading-assistant.validation.passphraseRequired') }] }]"
:placeholder="$t('trading-assistant.placeholders.inputPassphrase')"
autocomplete="new-password"
@change="handleApiConfigChange"
/>
</a-form-item>
<a-form-item :label="$t('trading-assistant.form.secretKey')">
<a-input-password
v-decorator="['secret_key', { rules: [{ required: true, message: $t('trading-assistant.validation.secretKeyRequired') }] }]"
:placeholder="$t('trading-assistant.placeholders.inputSecretKey')"
autocomplete="new-password"
@change="handleApiConfigChange"
/>
</a-form-item>
<a-form-item>
<a-checkbox
v-decorator="['save_credential', { valuePropName: 'checked', initialValue: false }]"
@change="onSaveCredentialChange"
<a-form-item
v-if="needsPassphrase"
:label="$t('trading-assistant.form.passphrase')"
>
{{ $t('trading-assistant.form.saveCredential') }}
</a-checkbox>
</a-form-item>
<a-input-password
v-decorator="['passphrase', { rules: [{ required: true, message: $t('trading-assistant.validation.passphraseRequired') }] }]"
:placeholder="$t('trading-assistant.placeholders.inputPassphrase')"
autocomplete="new-password"
@change="handleApiConfigChange"
/>
</a-form-item>
<a-form-item
v-if="saveCredentialUi"
:label="$t('trading-assistant.form.credentialName')"
>
<a-input
v-decorator="['credential_name']"
:placeholder="$t('trading-assistant.placeholders.inputCredentialName')"
/>
</a-form-item>
<a-form-item>
<a-checkbox
v-decorator="['save_credential', { valuePropName: 'checked', initialValue: false }]"
@change="onSaveCredentialChange"
>
{{ $t('trading-assistant.form.saveCredential') }}
</a-checkbox>
</a-form-item>
<a-form-item
v-if="saveCredentialUi"
:label="$t('trading-assistant.form.credentialName')"
>
<a-input
v-decorator="['credential_name']"
:placeholder="$t('trading-assistant.placeholders.inputCredentialName')"
/>
</a-form-item>
</template>
<!-- Test Connection Button (shared by both IBKR and Crypto) -->
<a-form-item>
<a-button
type="default"
@@ -1119,8 +1200,7 @@ const CRYPTO_SYMBOLS = [
'LINK/USDT', 'UNI/USDT', 'LTC/USDT', 'ATOM/USDT', 'ETC/USDT'
]
// 交易所选项(前端仅展示后端已适配/支持的交易所)
// 注意:这些标签在组件中会通过计算属性动态生成,以支持多语言
// Crypto exchange options
const EXCHANGE_OPTIONS = [
{ value: 'binance', labelKey: 'binance' },
{ value: 'okx', labelKey: 'okx' },
@@ -1133,6 +1213,16 @@ const EXCHANGE_OPTIONS = [
{ value: 'bitfinex', labelKey: 'bitfinex' }
]
// Traditional broker options (US/HK stocks) - extensible for future brokers
const BROKER_OPTIONS = [
{ value: 'ibkr', labelKey: 'ibkr', name: 'Interactive Brokers' }
// Future brokers can be added here:
// { value: 'td', labelKey: 'td', name: 'TD Ameritrade' },
// { value: 'schwab', labelKey: 'schwab', name: 'Charles Schwab' },
// { value: 'futu', labelKey: 'futu', name: 'Futu (富途)' },
// { value: 'tiger', labelKey: 'tiger', name: 'Tiger Brokers (老虎证券)' },
]
export default {
name: 'TradingAssistant',
mixins: [baseMixin],
@@ -1145,9 +1235,13 @@ export default {
return this.navTheme === 'dark' || this.navTheme === 'realdark'
},
needsPassphrase () {
// 需要 passphrase 的交易所
// Exchanges that require passphrase
return ['okx', 'okex', 'coinbaseexchange', 'kucoin', 'bitget'].includes(this.currentExchangeId)
},
// Check if current market uses IBKR (US Stock / HK Stock)
isIBKRMarket () {
return ['USStock', 'HShare'].includes(this.selectedMarketCategory)
},
// 预处理交易所列表,包含显示名称,提升性能
formattedExchangeOptions () {
return EXCHANGE_OPTIONS.map(exchange => {
@@ -1200,6 +1294,75 @@ export default {
const cat = this.selectedMarketCategory || 'Crypto'
return String(cat).toLowerCase() === 'crypto'
},
// Check if selected market supports live trading (Crypto or USStock/HShare with IBKR)
canUseLiveTrading () {
const cat = this.selectedMarketCategory || 'Crypto'
// Crypto always supports live trading via crypto exchanges
if (String(cat).toLowerCase() === 'crypto') {
return true
}
// USStock/HShare can use IBKR for live trading
if (['USStock', 'HShare'].includes(cat)) {
return true
}
return false
},
// Check if current market + exchange combination supports live trading
isLiveTradingAvailable () {
const cat = this.selectedMarketCategory || 'Crypto'
const exchangeId = this.currentExchangeId || ''
// Crypto markets use crypto exchanges
if (String(cat).toLowerCase() === 'crypto') {
return ['binance', 'okx', 'bitget', 'bybit', 'coinbaseexchange', 'kraken', 'kucoin', 'gate', 'bitfinex'].includes(exchangeId)
}
// USStock/HShare use IBKR
if (['USStock', 'HShare'].includes(cat)) {
return exchangeId === 'ibkr'
}
return false
},
// Broker options for US/HK stocks (with i18n support)
brokerOptions () {
return BROKER_OPTIONS.map(broker => {
let label = ''
try {
const translationKey = `trading-assistant.brokerNames.${broker.labelKey}`
const translated = this.$t(translationKey)
if (translated !== translationKey) {
label = translated
}
} catch (e) {}
if (!label) {
label = broker.name || broker.value.toUpperCase()
}
return {
...broker,
displayName: label
}
})
},
// Crypto exchange options only
cryptoExchangeOptions () {
return EXCHANGE_OPTIONS.map(exchange => {
let label = ''
try {
if (exchange.labelKey) {
const translationKey = `trading-assistant.exchangeNames.${exchange.labelKey}`
const translated = this.$t(translationKey)
if (translated !== translationKey) {
label = translated
}
}
} catch (e) {}
if (!label) {
label = exchange.value.charAt(0).toUpperCase() + exchange.value.slice(1)
}
return {
...exchange,
displayName: label
}
})
},
// 策略分组显示
groupedStrategies () {
const groups = {}
@@ -1260,6 +1423,7 @@ export default {
watchlist: [],
exchangeOptions: EXCHANGE_OPTIONS,
currentExchangeId: '',
currentBrokerId: 'ibkr',
testing: false,
testResult: null,
connectionTestResult: null,
@@ -1338,19 +1502,27 @@ export default {
// Keep selection reactive for Step 3 execution gating
this.selectedMarketCategory = market || 'Crypto'
// Non-crypto markets cannot use live trading. Force back to signal to keep UI consistent.
if (this.selectedMarketCategory !== 'Crypto') {
// Markets without live trading support: force back to signal mode
// Crypto, USStock, HShare support live trading; others do not
const supportsLiveTrading = ['Crypto', 'USStock', 'HShare'].includes(this.selectedMarketCategory)
if (!supportsLiveTrading) {
this.executionModeUi = 'signal'
try {
this.form && this.form.setFieldsValue && this.form.setFieldsValue({ execution_mode: 'signal' })
} catch (e) {}
}
// Clear exchange selection when market changes (different markets use different exchanges)
this.currentExchangeId = ''
try {
this.form && this.form.setFieldsValue && this.form.setFieldsValue({ exchange_id: undefined })
} catch (e) {}
},
handleMultiSymbolChange (vals) {
// vals: 数组,如 ["Crypto:BTC/USDT", "Crypto:ETH/USDT"]
// vals: array like ["Crypto:BTC/USDT", "Crypto:ETH/USDT"]
this.selectedSymbols = vals || []
// 根据选中的币种更新市场类型
// Update market type based on selected symbols
if (vals && vals.length > 0) {
const firstVal = vals[0]
if (typeof firstVal === 'string' && firstVal.includes(':')) {
@@ -1360,13 +1532,20 @@ export default {
}
}
// Non-crypto markets cannot use live trading
if (this.selectedMarketCategory !== 'Crypto') {
// Markets without live trading support: force back to signal mode
const supportsLiveTrading = ['Crypto', 'USStock', 'HShare'].includes(this.selectedMarketCategory)
if (!supportsLiveTrading) {
this.executionModeUi = 'signal'
try {
this.form && this.form.setFieldsValue && this.form.setFieldsValue({ execution_mode: 'signal' })
} catch (e) {}
}
// Clear exchange selection when market changes
this.currentExchangeId = ''
try {
this.form && this.form.setFieldsValue && this.form.setFieldsValue({ exchange_id: undefined })
} catch (e) {}
},
async loadExchangeCredentials () {
this.loadingExchangeCredentials = true
@@ -1452,7 +1631,8 @@ export default {
onExecutionModeChange (e) {
const v = e && e.target ? e.target.value : e
this.executionModeUi = v || 'signal'
if (!this.isCryptoMarket && this.executionModeUi !== 'signal') {
// If market doesn't support live trading, force signal mode
if (!this.canUseLiveTrading && this.executionModeUi !== 'signal') {
this.executionModeUi = 'signal'
try {
this.form && this.form.setFieldsValue && this.form.setFieldsValue({ execution_mode: 'signal' })
@@ -1510,6 +1690,7 @@ export default {
this.strategyType = 'indicator'
this.currentStep = 0
this.currentExchangeId = ''
this.currentBrokerId = 'ibkr'
this.selectedIndicator = null
this.testResult = null
this.connectionTestResult = null
@@ -1521,7 +1702,7 @@ export default {
this.entryPctMaxUi = 100
this.aiFilterEnabledUi = false
this.selectedMarketCategory = 'Crypto'
this.selectedSymbols = [] // 重置多币种选择
this.selectedSymbols = []
this.form.resetFields()
this.form.setFieldsValue({
@@ -1683,36 +1864,49 @@ export default {
}
}
// 加载交易所配置
// Load exchange/broker configuration
if (strategy.exchange_config) {
this.currentExchangeId = strategy.exchange_config.exchange_id || ''
// Only set form fields if live trading is enabled (fields are rendered)
// Check executionModeUi and market category (via isCryptoMarket, but we need to ensure computed prop is updated or check directly)
const exchangeId = strategy.exchange_config.exchange_id || ''
const isLive = this.executionModeUi === 'live'
const isCrypto = this.selectedMarketCategory === 'Crypto'
const supportsLiveTrading = ['Crypto', 'USStock', 'HShare'].includes(this.selectedMarketCategory)
const isBrokerMarket = ['USStock', 'HShare'].includes(this.selectedMarketCategory)
if (isLive && isCrypto) {
// Prevent exchange change handler from clearing API fields while backfilling saved data.
this.suppressApiClearOnce = true
this.form.setFieldsValue({
exchange_id: strategy.exchange_config.exchange_id,
credential_id: strategy.exchange_config.credential_id || undefined,
api_key: strategy.exchange_config.api_key || '',
secret_key: strategy.exchange_config.secret_key || '',
passphrase: strategy.exchange_config.passphrase || ''
})
if (isLive && supportsLiveTrading) {
if (isBrokerMarket) {
// Broker configuration (US/HK stocks)
this.currentBrokerId = exchangeId || 'ibkr'
this.form.setFieldsValue({
broker_id: exchangeId || 'ibkr',
ibkr_host: strategy.exchange_config.ibkr_host || '127.0.0.1',
ibkr_port: strategy.exchange_config.ibkr_port || 7497,
ibkr_client_id: strategy.exchange_config.ibkr_client_id || 1,
ibkr_account: strategy.exchange_config.ibkr_account || ''
})
} else {
// Crypto exchange configuration
this.currentExchangeId = exchangeId
this.suppressApiClearOnce = true
this.form.setFieldsValue({
exchange_id: exchangeId,
credential_id: strategy.exchange_config.credential_id || undefined,
api_key: strategy.exchange_config.api_key || '',
secret_key: strategy.exchange_config.secret_key || '',
passphrase: strategy.exchange_config.passphrase || ''
})
// If a vault credential is selected, auto-fill secrets from vault (strategy rows may not store secrets).
const credId = strategy.exchange_config.credential_id
if (credId) {
await this.handleCredentialSelectChange(credId)
// If a vault credential is selected, auto-fill secrets from vault
const credId = strategy.exchange_config.credential_id
if (credId) {
await this.handleCredentialSelectChange(credId)
}
}
}
// 设置当前交易所ID (UI state)
if (strategy.exchange_config?.exchange_id) {
this.currentExchangeId = strategy.exchange_config.exchange_id
// Update UI state
if (isBrokerMarket) {
this.currentBrokerId = exchangeId || 'ibkr'
} else {
this.currentExchangeId = exchangeId
}
}
@@ -2252,7 +2446,8 @@ export default {
ftxus: 'blue',
binanceus: 'gold',
binancecoinm: 'gold',
binanceusdm: 'gold'
binanceusdm: 'gold',
ibkr: 'green'
}
return colorMap[exchangeId] || 'default'
},
@@ -2261,6 +2456,15 @@ export default {
this.testResult = null
this.connectionTestResult = null
},
getModalPopupContainer () {
// Return document.body for Select dropdown to avoid modal scroll issues
return window.document.body
},
handleBrokerSelectChange (value) {
this.currentBrokerId = value || 'ibkr'
this.testResult = null
this.connectionTestResult = null
},
handleExchangeSelectChange (value) {
this.currentExchangeId = value || ''
this.testResult = null
@@ -2355,13 +2559,54 @@ export default {
this.testing = true
try {
// 构建需要验证的字段列表
// IBKR uses different connection test (host/port instead of api_key/secret)
if (this.isIBKRMarket) {
const values = this.form.getFieldsValue(['ibkr_host', 'ibkr_port', 'ibkr_client_id', 'ibkr_account'])
const host = values.ibkr_host || '127.0.0.1'
const port = values.ibkr_port || 7497
const clientId = values.ibkr_client_id || 1
const account = values.ibkr_account || ''
try {
// Call IBKR connect API
const res = await this.$http.post('/api/ibkr/connect', {
host: host,
port: parseInt(port),
clientId: parseInt(clientId),
account: account
})
if (res.data && res.data.success) {
this.testResult = {
success: true,
message: this.$t('trading-assistant.exchange.ibkrConnectionSuccess')
}
this.$message.success(this.$t('trading-assistant.exchange.ibkrConnectionSuccess'))
} else {
this.testResult = {
success: false,
message: res.data?.error || this.$t('trading-assistant.exchange.ibkrConnectionFailed')
}
this.$message.error(this.testResult.message)
}
} catch (error) {
this.testResult = {
success: false,
message: error.response?.data?.error || error.message || this.$t('trading-assistant.exchange.ibkrConnectionFailed')
}
this.$message.error(this.testResult.message)
} finally {
this.testing = false
}
return
}
// Crypto exchanges: validate api_key/secret_key fields
const fieldsToValidate = ['exchange_id', 'api_key', 'secret_key']
if (this.needsPassphrase) {
fieldsToValidate.push('passphrase')
}
// 先验证表单字段
this.form.validateFields(fieldsToValidate, async (err, values) => {
if (err) {
this.testing = false
@@ -2375,11 +2620,9 @@ export default {
exchange_id: values.exchange_id,
api_key: values.api_key,
secret_key: values.secret_key,
// IMPORTANT: let backend pick correct Binance endpoints (spot vs futures)
market_type: String(marketType || 'swap')
}
// 如果需要 passphrase,添加它
if (this.needsPassphrase && values.passphrase) {
exchangeConfig.passphrase = values.passphrase
}
@@ -2489,7 +2732,7 @@ export default {
if (!err) {
try {
this.saving = true
const isLive = this.isCryptoMarket && values.execution_mode === 'live'
const isLive = this.canUseLiveTrading && values.execution_mode === 'live'
if (isLive) {
const testResult = this.testResult
@@ -2557,13 +2800,22 @@ export default {
indicator_name: indicator.name,
indicator_code: indicator.code || ''
},
exchange_config: isLive ? {
exchange_config: isLive ? (this.isIBKRMarket ? {
// Broker configuration (US/HK stocks)
exchange_id: values.broker_id || this.currentBrokerId || 'ibkr',
// IBKR specific fields
ibkr_host: values.ibkr_host || '127.0.0.1',
ibkr_port: values.ibkr_port || 7497,
ibkr_client_id: values.ibkr_client_id || 1,
ibkr_account: values.ibkr_account || ''
} : {
// Crypto exchange configuration
exchange_id: values.exchange_id,
credential_id: values.credential_id,
api_key: values.api_key,
secret_key: values.secret_key,
passphrase: this.needsPassphrase ? values.passphrase : undefined
} : undefined,
}) : undefined,
trading_config: {
initial_capital: values.initial_capital,
leverage: leverage,
@@ -2630,8 +2882,8 @@ export default {
const totalCreated = res.data?.total_created || this.selectedSymbols.length
this.$message.success(this.$t('trading-assistant.messages.batchCreateSuccess', { count: totalCreated }))
}
if (isLive && values.save_credential) {
// Save credential to vault (best-effort)
// Save credential to vault (crypto exchanges only, IBKR doesn't need this)
if (isLive && values.save_credential && !this.isIBKRMarket) {
try {
await createExchangeCredential({
user_id: 1,