diff --git a/backend_api_python/app/services/live_trading/binance.py b/backend_api_python/app/services/live_trading/binance.py index 56e4b64..17618d7 100644 --- a/backend_api_python/app/services/live_trading/binance.py +++ b/backend_api_python/app/services/live_trading/binance.py @@ -19,7 +19,10 @@ class BinanceFuturesClient(BaseRestClient): - def __init__(self, *, api_key: str, secret_key: str, base_url: str = "https://fapi.binance.com", timeout_sec: float = 15.0): + def __init__(self, *, api_key: str, secret_key: str, base_url: str = None, enable_demo_trading: bool = False, timeout_sec: float = 15.0): + if not base_url: + base_url = "https://demo-fapi.binance.com" if enable_demo_trading else "https://fapi.binance.com" + super().__init__(base_url=base_url, timeout_sec=timeout_sec) self.api_key = (api_key or "").strip() self.secret_key = (secret_key or "").strip() diff --git a/backend_api_python/app/services/live_trading/binance_spot.py b/backend_api_python/app/services/live_trading/binance_spot.py index 4288c41..f8ab3e8 100644 --- a/backend_api_python/app/services/live_trading/binance_spot.py +++ b/backend_api_python/app/services/live_trading/binance_spot.py @@ -16,7 +16,10 @@ class BinanceSpotClient(BaseRestClient): - def __init__(self, *, api_key: str, secret_key: str, base_url: str = "https://api.binance.com", timeout_sec: float = 15.0): + def __init__(self, *, api_key: str, secret_key: str, base_url: str = None, enable_demo_trading: bool = False, timeout_sec: float = 15.0): + if not base_url: + base_url = "https://demo-api.binance.com" if enable_demo_trading else "https://api.binance.com" + super().__init__(base_url=base_url, timeout_sec=timeout_sec) self.api_key = (api_key or "").strip() self.secret_key = (secret_key or "").strip() diff --git a/backend_api_python/app/services/live_trading/factory.py b/backend_api_python/app/services/live_trading/factory.py index fb956a4..21cfebb 100644 --- a/backend_api_python/app/services/live_trading/factory.py +++ b/backend_api_python/app/services/live_trading/factory.py @@ -59,12 +59,18 @@ def create_client(exchange_config: Dict[str, Any], *, market_type: str = "swap") mt = "swap" if exchange_id == "binance": + # 检查是否启用模拟交易,支持布尔值和字符串 + enable_demo = exchange_config.get("enable_demo_trading") or exchange_config.get("enableDemoTrading") + is_demo = bool(enable_demo) if isinstance(enable_demo, bool) else str(enable_demo).lower() in ("true", "1", "yes") + if mt == "spot": - base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.binance.com" - return BinanceSpotClient(api_key=api_key, secret_key=secret_key, base_url=base_url) - # Default to USDT-M futures - base_url = _get(exchange_config, "base_url", "baseUrl") or "https://fapi.binance.com" - return BinanceFuturesClient(api_key=api_key, secret_key=secret_key, base_url=base_url) + default_url = "https://demo-api.binance.com" if is_demo else "https://api.binance.com" + base_url = _get(exchange_config, "base_url", "baseUrl") or default_url + return BinanceSpotClient(api_key=api_key, secret_key=secret_key, base_url=base_url, enable_demo_trading=is_demo) + # Default to USDT-M futures + default_url = "https://demo-fapi.binance.com" if is_demo else "https://fapi.binance.com" + base_url = _get(exchange_config, "base_url", "baseUrl") or default_url + return BinanceFuturesClient(api_key=api_key, secret_key=secret_key, base_url=base_url, enable_demo_trading=is_demo) if exchange_id == "okx": base_url = _get(exchange_config, "base_url", "baseUrl") or "https://www.okx.com" return OkxClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url) diff --git a/quantdinger_vue/src/views/trading-assistant/index.vue b/quantdinger_vue/src/views/trading-assistant/index.vue index a4936dc..5d1c280 100644 --- a/quantdinger_vue/src/views/trading-assistant/index.vue +++ b/quantdinger_vue/src/views/trading-assistant/index.vue @@ -22,18 +22,15 @@
-
+
{{ group.baseName }} - {{ group.strategies.length }} {{ $t('trading-assistant.symbolCount') }} + {{ group.strategies.length }} {{ + $t('trading-assistant.symbolCount') }}
@@ -68,8 +65,7 @@ v-for="item in group.strategies" :key="item.id" :class="['strategy-list-item', { active: selectedStrategy && selectedStrategy.id === item.id }]" - @click="handleSelectStrategy(item)" - > + @click="handleSelectStrategy(item)">
@@ -82,8 +78,7 @@ :class="[ item.status ? `status-${item.status}` : '', { 'status-stopped': item.status === 'stopped' } - ]" - > + ]"> {{ getStatusText(item.status) }}
@@ -123,8 +118,7 @@ v-for="item in groupedStrategies.ungrouped" :key="item.id" :class="['strategy-list-item', { active: selectedStrategy && selectedStrategy.id === item.id }]" - @click="handleSelectStrategy(item)" - > + @click="handleSelectStrategy(item)">
@@ -132,8 +126,7 @@ v-if="item.exchange_config && item.exchange_config.exchange_id" :color="getExchangeTagColor(item.exchange_config.exchange_id)" size="small" - class="exchange-tag" - > + class="exchange-tag"> {{ getExchangeDisplayName(item.exchange_config.exchange_id) }} @@ -142,8 +135,7 @@ v-if="item.strategy_type === 'PromptBasedStrategy'" color="purple" size="small" - class="strategy-type-tag" - > + class="strategy-type-tag"> AI @@ -159,8 +151,7 @@ :class="[ item.status ? `status-${item.status}` : '', { 'status-stopped': item.status === 'stopped' } - ]" - > + ]"> {{ getStatusText(item.status) }}
@@ -223,13 +214,16 @@
-
+
{{ $t('trading-assistant.detail.totalInvestment') }}
-
${{ ((selectedStrategy.initial_capital || selectedStrategy.trading_config?.initial_capital) || 0).toLocaleString() }}
+
${{ ((selectedStrategy.initial_capital || + selectedStrategy.trading_config?.initial_capital) || 0).toLocaleString() }}
@@ -241,7 +235,10 @@
{{ formatCurrency(currentEquity) }}
-
+
@@ -261,7 +258,9 @@ {{ selectedStrategy.trading_config.symbol }}
-
+
{{ selectedStrategy.indicator_config.indicator_name }}
@@ -269,11 +268,15 @@ {{ selectedStrategy.trading_config.leverage || 1 }}x
-
+
{{ getTradeDirectionText(selectedStrategy.trading_config.trade_direction) }}
-
+
{{ selectedStrategy.trading_config.timeframe }}
@@ -285,8 +288,7 @@ type="primary" size="large" class="action-btn start-btn" - @click="handleStartStrategy(selectedStrategy.id)" - > + @click="handleStartStrategy(selectedStrategy.id)"> {{ $t('trading-assistant.startStrategy') }} @@ -295,8 +297,7 @@ type="danger" size="large" class="action-btn stop-btn" - @click="handleStopStrategy(selectedStrategy.id)" - > + @click="handleStopStrategy(selectedStrategy.id)"> {{ $t('trading-assistant.stopStrategy') }} @@ -312,14 +313,10 @@ :strategy-id="selectedStrategy.id" :market-type="(selectedStrategy.trading_config && selectedStrategy.trading_config.market_type) || 'swap'" :leverage="(selectedStrategy.trading_config && selectedStrategy.trading_config.leverage) || 1" - :loading="loadingRecords" - /> + :loading="loadingRecords" /> - + @@ -337,8 +334,7 @@ @cancel="handleCloseModal" :maskClosable="false" :wrapClassName="isMobile ? 'mobile-modal' : ''" - :bodyStyle="{ maxHeight: '70vh', overflowY: 'auto' }" - > + :bodyStyle="{ maxHeight: '70vh', overflowY: 'auto' }"> @@ -361,13 +357,11 @@ @focus="handleIndicatorSelectFocus" @change="handleIndicatorChange" :loading="loadingIndicators" - :getPopupContainer="(triggerNode) => triggerNode.parentNode" - > + :getPopupContainer="(triggerNode) => triggerNode.parentNode"> + :value="String(indicator.id)">
{{ indicator.name }} @@ -392,11 +386,11 @@ + :placeholder="$t('trading-assistant.placeholders.inputStrategyName')" /> - + + :getPopupContainer="(triggerNode) => triggerNode.parentNode"> + :value="`${item.market}:${item.symbol}`">
{{ item.market }} @@ -433,13 +425,11 @@ :loading="loadingWatchlist" @change="handleMultiSymbolChange" :getPopupContainer="(triggerNode) => triggerNode.parentNode" - :maxTagCount="3" - > + :maxTagCount="3"> + :value="`${item.market}:${item.symbol}`">
{{ item.market }} @@ -450,7 +440,8 @@
- {{ isEditMode ? $t('trading-assistant.form.symbolHintCrypto') : $t('trading-assistant.form.symbolsHint') }} + {{ isEditMode ? $t('trading-assistant.form.symbolHintCrypto') : + $t('trading-assistant.form.symbolsHint') }}
@@ -462,16 +453,14 @@ :min="10" :step="100" :precision="2" - style="width: 100%" - /> + style="width: 100%" /> + @change="handleMarketTypeChange"> {{ $t('trading-assistant.form.marketTypeFutures') }} {{ $t('trading-assistant.form.marketTypeSpot') }} @@ -491,8 +480,7 @@ :max="form.getFieldValue('market_type') === 'spot' ? 1 : 125" :step="1" style="width: 100%" - :disabled="form.getFieldValue('market_type') === 'spot'" - /> + :disabled="form.getFieldValue('market_type') === 'spot'" />
{{ $t('trading-assistant.form.spotLeverageFixed') }} @@ -507,8 +495,7 @@ + :disabled="form.getFieldValue('market_type') === 'spot'"> {{ $t('trading-assistant.form.tradeDirectionLong') }} {{ $t('trading-assistant.form.tradeDirectionShort') }} @@ -517,7 +504,10 @@ {{ $t('trading-assistant.form.tradeDirectionBoth') }} -
+
{{ $t('trading-assistant.form.spotOnlyLongHint') }}
@@ -530,8 +520,7 @@ + :getPopupContainer="(triggerNode) => triggerNode.parentNode"> {{ $t('trading-assistant.form.timeframe1m') }} {{ $t('trading-assistant.form.timeframe5m') }} {{ $t('trading-assistant.form.timeframe15m') }} @@ -567,8 +556,7 @@ :max="100" :step="0.01" :precision="4" - style="width: 220px" - /> + style="width: 220px" /> @@ -579,8 +567,7 @@ :max="1000" :step="0.01" :precision="4" - style="width: 220px" - /> + style="width: 220px" /> @@ -590,8 +577,7 @@ + @change="onTrailingToggle" /> @@ -607,8 +593,7 @@ :max="100" :step="0.01" :precision="4" - style="width: 220px" - /> + style="width: 220px" /> @@ -619,8 +604,7 @@ :max="1000" :step="0.01" :precision="4" - style="width: 220px" - /> + style="width: 220px" /> @@ -633,16 +617,14 @@ + @change="onTrendAddToggle" /> + @change="onDcaAddToggle" /> @@ -656,8 +638,7 @@ :step="0.01" :precision="4" style="width: 220px" - @change="onScaleParamsChange" - /> + @change="onScaleParamsChange" /> @@ -669,8 +650,7 @@ :step="0.01" :precision="4" style="width: 220px" - @change="onScaleParamsChange" - /> + @change="onScaleParamsChange" /> @@ -684,8 +664,7 @@ :step="0.1" :precision="4" style="width: 220px" - @change="onScaleParamsChange" - /> + @change="onScaleParamsChange" /> @@ -697,8 +676,7 @@ :step="0.1" :precision="4" style="width: 220px" - @change="onScaleParamsChange" - /> + @change="onScaleParamsChange" /> @@ -712,8 +690,7 @@ :step="1" :precision="0" style="width: 220px" - @change="onScaleParamsChange" - /> + @change="onScaleParamsChange" /> @@ -725,8 +702,7 @@ :step="1" :precision="0" style="width: 220px" - @change="onScaleParamsChange" - /> + @change="onScaleParamsChange" /> @@ -736,12 +712,14 @@ - + - + @@ -754,8 +732,7 @@ :max="1000" :step="0.01" :precision="4" - style="width: 220px" - /> + style="width: 220px" /> @@ -766,8 +743,7 @@ :max="1000" :step="0.01" :precision="4" - style="width: 220px" - /> + style="width: 220px" /> @@ -780,8 +756,7 @@ :max="100" :step="0.1" :precision="4" - style="width: 220px" - /> + style="width: 220px" /> @@ -792,8 +767,7 @@ :max="100" :step="0.1" :precision="4" - style="width: 220px" - /> + style="width: 220px" /> @@ -806,8 +780,7 @@ :max="50" :step="1" :precision="0" - style="width: 100%" - /> + style="width: 100%" /> @@ -818,8 +791,7 @@ :max="50" :step="1" :precision="0" - style="width: 100%" - /> + style="width: 100%" /> @@ -830,8 +802,7 @@ + :help="$t('dashboard.indicator.backtest.hint.entryPctMax', { maxPct: Number(entryPctMaxUi || 0).toFixed(0) })"> + @change="onEntryPctChange" /> @@ -855,10 +825,7 @@ {{ $t('trading-assistant.form.enableAiFilter') }}
- +
{{ $t('trading-assistant.form.enableAiFilterHint') }}
@@ -875,10 +842,10 @@ + @change="onExecutionModeChange"> {{ $t('trading-assistant.form.executionModeSignal') }} - {{ $t('trading-assistant.form.executionModeLive') }} + {{ $t('trading-assistant.form.executionModeLive') + }}
{{ $t('trading-assistant.form.liveTradingNotSupportedHint') }} @@ -888,8 +855,7 @@ + @change="onNotifyChannelsChange"> {{ $t('trading-assistant.notify.browser') }} {{ $t('trading-assistant.notify.email') }} {{ $t('trading-assistant.notify.telegram') }} @@ -903,8 +869,7 @@ v-if="notifyChannelsUi.includes('telegram') || notifyChannelsUi.includes('email') || notifyChannelsUi.includes('discord')" type="info" showIcon - style="margin-bottom: 16px" - > + style="margin-bottom: 16px"> @@ -1093,13 +1037,8 @@ show-search option-filter-prop="children" :loading="loadingExchangeCredentials" - @change="handleCredentialSelectChange" - > - + @change="handleCredentialSelectChange"> + {{ formatCredentialLabel(cred) }} @@ -1116,13 +1055,11 @@ allow-clear show-search option-filter-prop="children" - @change="handleExchangeSelectChange" - > + @change="handleExchangeSelectChange"> + :value="exchange.value"> {{ exchange.displayName }} @@ -1133,8 +1070,7 @@ v-decorator="['api_key', { rules: [{ required: true, message: $t('trading-assistant.validation.apiKeyRequired') }] }]" :placeholder="$t('trading-assistant.placeholders.inputApiKey')" autocomplete="new-password" - @change="handleApiConfigChange" - /> + @change="handleApiConfigChange" /> @@ -1142,50 +1078,45 @@ v-decorator="['secret_key', { rules: [{ required: true, message: $t('trading-assistant.validation.secretKeyRequired') }] }]" :placeholder="$t('trading-assistant.placeholders.inputSecretKey')" autocomplete="new-password" - @change="handleApiConfigChange" - /> + @change="handleApiConfigChange" /> - + + @change="handleApiConfigChange" /> + + + + + + + + @change="onSaveCredentialChange"> {{ $t('trading-assistant.form.saveCredential') }} - + + :placeholder="$t('trading-assistant.placeholders.inputCredentialName')" /> - + {{ $t('trading-assistant.form.testConnection') }} @@ -1201,26 +1132,13 @@ @@ -1391,6 +1309,11 @@ export default { } return false }, + // 是否显示模拟交易开关 + showDemoTradingSwitch () { + // 目前仅支持 Binance 的 Demo Trading + return this.currentExchangeId && this.currentExchangeId.toLowerCase() === 'binance' + }, // Broker options for US/HK stocks (with i18n support) brokerOptions () { return BROKER_OPTIONS.map(broker => { @@ -1401,7 +1324,7 @@ export default { if (translated !== translationKey) { label = translated } - } catch (e) {} + } catch (e) { } if (!label) { label = broker.name || broker.value.toUpperCase() } @@ -1421,7 +1344,7 @@ export default { if (translated !== translationKey) { label = translated } - } catch (e) {} + } catch (e) { } if (!label) { label = broker.name || broker.value.toUpperCase() } @@ -1434,24 +1357,24 @@ export default { // 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 - } + 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 } - }) + } catch (e) { } + if (!label) { + label = exchange.value.charAt(0).toUpperCase() + exchange.value.slice(1) + } + return { + ...exchange, + displayName: label + } + }) }, // 策略分组显示 groupedStrategies () { @@ -1629,12 +1552,12 @@ export default { this.currentBrokerId = 'mt5' try { this.form && this.form.setFieldsValue && this.form.setFieldsValue({ forex_broker_id: 'mt5' }) - } catch (e) {} + } catch (e) { } } else if (['USStock', 'HShare'].includes(this.selectedMarketCategory)) { this.currentBrokerId = 'ibkr' try { this.form && this.form.setFieldsValue && this.form.setFieldsValue({ broker_id: 'ibkr' }) - } catch (e) {} + } catch (e) { } } // Markets without live trading support: force back to signal mode @@ -1644,14 +1567,14 @@ export default { this.executionModeUi = 'signal' try { this.form && this.form.setFieldsValue && this.form.setFieldsValue({ execution_mode: 'signal' }) - } catch (e) {} + } 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) {} + } catch (e) { } }, handleMultiSymbolChange (vals) { // vals: array like ["Crypto:BTC/USDT", "Crypto:ETH/USDT"] @@ -1672,12 +1595,12 @@ export default { this.currentBrokerId = 'mt5' try { this.form && this.form.setFieldsValue && this.form.setFieldsValue({ forex_broker_id: 'mt5' }) - } catch (e) {} + } catch (e) { } } else if (['USStock', 'HShare'].includes(this.selectedMarketCategory)) { this.currentBrokerId = 'ibkr' try { this.form && this.form.setFieldsValue && this.form.setFieldsValue({ broker_id: 'ibkr' }) - } catch (e) {} + } catch (e) { } } // Markets without live trading support: force back to signal mode @@ -1686,14 +1609,14 @@ export default { this.executionModeUi = 'signal' try { this.form && this.form.setFieldsValue && this.form.setFieldsValue({ execution_mode: 'signal' }) - } catch (e) {} + } catch (e) { } } // Clear exchange selection when market changes this.currentExchangeId = '' try { this.form && this.form.setFieldsValue && this.form.setFieldsValue({ exchange_id: undefined }) - } catch (e) {} + } catch (e) { } }, async loadExchangeCredentials () { this.loadingExchangeCredentials = true @@ -1774,7 +1697,7 @@ export default { this.saveCredentialUi = checked try { this.form && this.form.setFieldsValue && this.form.setFieldsValue({ save_credential: checked }) - } catch (err) {} + } catch (err) { } }, onExecutionModeChange (e) { const v = e && e.target ? e.target.value : e @@ -1784,7 +1707,7 @@ export default { this.executionModeUi = 'signal' try { this.form && this.form.setFieldsValue && this.form.setFieldsValue({ execution_mode: 'signal' }) - } catch (err) {} + } catch (err) { } } }, onNotifyChannelsChange (vals) { @@ -2521,7 +2444,7 @@ export default { // Ensure rc-form value is always in sync even if decorator event binding gets overridden. try { this.form && this.form.setFieldsValue && this.form.setFieldsValue({ enable_ai_filter: !!checked }) - } catch (e) {} + } catch (e) { } }, filterIndicatorOption (input, option) { const text = option.componentOptions.children[0].children[0].text @@ -2642,11 +2565,13 @@ export default { } // Clear API fields when exchange changes, as we rely on "Saved credential" + // to auto-fill api_key/secret_key. User must re-enter if changing exchange. this.$nextTick(() => { const fieldsToClear = { api_key: undefined, secret_key: undefined, - passphrase: undefined + passphrase: undefined, + enable_demo_trading: false // Reset demo switch too } setTimeout(() => { this.form.setFieldsValue(fieldsToClear) @@ -2836,7 +2761,8 @@ export default { exchange_id: values.exchange_id, api_key: values.api_key, secret_key: values.secret_key, - market_type: String(marketType || 'swap') + market_type: String(marketType || 'swap'), + enableDemoTrading: !!this.form.getFieldValue('enable_demo_trading') } if (this.needsPassphrase && values.passphrase) { @@ -2910,7 +2836,7 @@ export default { if (marketType === 'spot') { this.form.setFieldsValue({ leverage: 1, trade_direction: 'long' }) } - } catch (e) {} + } catch (e) { } // Init backtest-like UI states for Step 2 (Ant Form is not reactive). this.backtestCollapseKeys = ['risk'] @@ -2934,7 +2860,7 @@ export default { this.executionModeUi = execMode const chans = this.form.getFieldValue('notify_channels') || ['browser'] this.notifyChannelsUi = Array.isArray(chans) ? chans : ['browser'] - } catch (e) {} + } catch (e) { } this.currentStep++ } }, @@ -3041,6 +2967,7 @@ export default { credential_id: values.credential_id, api_key: values.api_key, secret_key: values.secret_key, + enableDemoTrading: !!values.enable_demo_trading, passphrase: this.needsPassphrase ? values.passphrase : undefined }) : undefined, trading_config: { @@ -3375,7 +3302,7 @@ export default { } // 确保文本可换行 - & > span { + &>span { word-break: break-word; line-height: 1.5; flex: 1; @@ -3835,9 +3762,12 @@ export default { } @keyframes statusPulse { - 0%, 100% { + + 0%, + 100% { opacity: 1; } + 50% { opacity: 0.5; } @@ -3870,14 +3800,14 @@ export default { display: flex; align-items: center; justify-content: center; - background: linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(248,250,252,0.9) 100%); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.8) 0%, rgba(248, 250, 252, 0.9) 100%); border-radius: @border-radius-lg; border: 2px dashed #e0e6ed; transition: all 0.3s ease; &:hover { border-color: @primary-color; - background: linear-gradient(135deg, rgba(24,144,255,0.02) 0%, rgba(24,144,255,0.05) 100%); + background: linear-gradient(135deg, rgba(24, 144, 255, 0.02) 0%, rgba(24, 144, 255, 0.05) 100%); } /deep/ .ant-empty-image { @@ -4149,10 +4079,13 @@ export default { } @keyframes pulse { - 0%, 100% { + + 0%, + 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(1.1); @@ -4201,6 +4134,7 @@ export default { } .ant-tabs-tabpane { + .trading-records, .position-records { width: 100%; @@ -4676,6 +4610,7 @@ export default { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); @@ -4721,5 +4656,4 @@ export default { } } } -