mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 04:50:57 +08:00
Feature/custom strategy (#1173)
* feat: add Strategy Studio with multi-timeframe support - Add Strategy Studio page with three-column layout for strategy management - Support multi-timeframe K-line data selection (5m, 15m, 1h, 4h, etc.) - Add GetWithTimeframes() function in market package for fetching multiple timeframes - Add TimeframeSeriesData struct for storing per-timeframe technical indicators - Update formatMarketData() to display all selected timeframes in AI prompt - Add strategy API endpoints for CRUD operations and test run - Integrate real AI test runs with configured AI models - Support custom AI500 and OI Top API URLs from strategy config * docs: add Strategy Studio screenshot to README files * feat: add quant data integration and fix position click navigation - Integrate quant data API (netflow, OI, price changes) into Strategy Studio - Add enable_quant_data toggle in indicator editor - Fix position symbol click to navigate to chart (sync defaultSymbol prop) - Fix TradingView chart fullscreen flickering
This commit is contained in:
@@ -109,6 +109,7 @@ type Context struct {
|
||||
MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用
|
||||
MultiTFMarket map[string]map[string]*market.Data `json:"-"`
|
||||
OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射
|
||||
QuantDataMap map[string]*QuantData `json:"-"` // 量化数据映射(资金流向、持仓变化)
|
||||
BTCETHLeverage int `json:"-"` // BTC/ETH杠杆倍数(从配置读取)
|
||||
AltcoinLeverage int `json:"-"` // 山寨币杠杆倍数(从配置读取)
|
||||
}
|
||||
|
||||
@@ -177,6 +177,193 @@ func (e *StrategyEngine) FetchExternalData() (map[string]interface{}, error) {
|
||||
return externalData, nil
|
||||
}
|
||||
|
||||
// QuantData 量化数据结构(资金流向、持仓变化、价格变化)
|
||||
type QuantData struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Price float64 `json:"price"`
|
||||
Netflow *NetflowData `json:"netflow,omitempty"`
|
||||
OI map[string]*OIData `json:"oi,omitempty"`
|
||||
PriceChange map[string]float64 `json:"price_change,omitempty"`
|
||||
}
|
||||
|
||||
type NetflowData struct {
|
||||
Institution *FlowTypeData `json:"institution,omitempty"`
|
||||
Personal *FlowTypeData `json:"personal,omitempty"`
|
||||
}
|
||||
|
||||
type FlowTypeData struct {
|
||||
Future map[string]float64 `json:"future,omitempty"`
|
||||
Spot map[string]float64 `json:"spot,omitempty"`
|
||||
}
|
||||
|
||||
type OIData struct {
|
||||
CurrentOI float64 `json:"current_oi"`
|
||||
NetLong float64 `json:"net_long"`
|
||||
NetShort float64 `json:"net_short"`
|
||||
Delta map[string]*OIDeltaData `json:"delta,omitempty"`
|
||||
}
|
||||
|
||||
type OIDeltaData struct {
|
||||
OIDelta float64 `json:"oi_delta"`
|
||||
OIDeltaValue float64 `json:"oi_delta_value"`
|
||||
OIDeltaPercent float64 `json:"oi_delta_percent"`
|
||||
}
|
||||
|
||||
// FetchQuantData 获取单个币种的量化数据
|
||||
func (e *StrategyEngine) FetchQuantData(symbol string) (*QuantData, error) {
|
||||
if !e.config.Indicators.EnableQuantData || e.config.Indicators.QuantDataAPIURL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 替换 {symbol} 占位符
|
||||
url := strings.Replace(e.config.Indicators.QuantDataAPIURL, "{symbol}", symbol, -1)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var apiResp struct {
|
||||
Code int `json:"code"`
|
||||
Data *QuantData `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("解析JSON失败: %w", err)
|
||||
}
|
||||
|
||||
if apiResp.Code != 0 {
|
||||
return nil, fmt.Errorf("API返回错误码: %d", apiResp.Code)
|
||||
}
|
||||
|
||||
return apiResp.Data, nil
|
||||
}
|
||||
|
||||
// FetchQuantDataBatch 批量获取量化数据
|
||||
func (e *StrategyEngine) FetchQuantDataBatch(symbols []string) map[string]*QuantData {
|
||||
result := make(map[string]*QuantData)
|
||||
|
||||
if !e.config.Indicators.EnableQuantData || e.config.Indicators.QuantDataAPIURL == "" {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, symbol := range symbols {
|
||||
data, err := e.FetchQuantData(symbol)
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 获取 %s 量化数据失败: %v", symbol, err)
|
||||
continue
|
||||
}
|
||||
if data != nil {
|
||||
result[symbol] = data
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// formatQuantData 格式化量化数据
|
||||
func (e *StrategyEngine) formatQuantData(data *QuantData) string {
|
||||
if data == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("📊 量化数据:\n")
|
||||
|
||||
// 价格变化
|
||||
if len(data.PriceChange) > 0 {
|
||||
sb.WriteString("价格变化: ")
|
||||
timeframes := []string{"5m", "15m", "1h", "4h", "24h"}
|
||||
parts := []string{}
|
||||
for _, tf := range timeframes {
|
||||
if v, ok := data.PriceChange[tf]; ok {
|
||||
parts = append(parts, fmt.Sprintf("%s: %+.2f%%", tf, v))
|
||||
}
|
||||
}
|
||||
sb.WriteString(strings.Join(parts, " | "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// 资金流向
|
||||
if data.Netflow != nil {
|
||||
sb.WriteString("资金流向(USDT):\n")
|
||||
|
||||
// 机构资金
|
||||
if data.Netflow.Institution != nil {
|
||||
if data.Netflow.Institution.Future != nil {
|
||||
sb.WriteString(" 机构合约: ")
|
||||
parts := []string{}
|
||||
for _, tf := range []string{"1h", "4h", "24h"} {
|
||||
if v, ok := data.Netflow.Institution.Future[tf]; ok {
|
||||
parts = append(parts, fmt.Sprintf("%s: %+.0f", tf, v))
|
||||
}
|
||||
}
|
||||
sb.WriteString(strings.Join(parts, " | "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
if data.Netflow.Institution.Spot != nil {
|
||||
sb.WriteString(" 机构现货: ")
|
||||
parts := []string{}
|
||||
for _, tf := range []string{"1h", "4h", "24h"} {
|
||||
if v, ok := data.Netflow.Institution.Spot[tf]; ok {
|
||||
parts = append(parts, fmt.Sprintf("%s: %+.0f", tf, v))
|
||||
}
|
||||
}
|
||||
sb.WriteString(strings.Join(parts, " | "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 散户资金
|
||||
if data.Netflow.Personal != nil {
|
||||
if data.Netflow.Personal.Future != nil {
|
||||
sb.WriteString(" 散户合约: ")
|
||||
parts := []string{}
|
||||
for _, tf := range []string{"1h", "4h", "24h"} {
|
||||
if v, ok := data.Netflow.Personal.Future[tf]; ok {
|
||||
parts = append(parts, fmt.Sprintf("%s: %+.0f", tf, v))
|
||||
}
|
||||
}
|
||||
sb.WriteString(strings.Join(parts, " | "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 持仓数据
|
||||
if len(data.OI) > 0 {
|
||||
for exchange, oiData := range data.OI {
|
||||
sb.WriteString(fmt.Sprintf("持仓(%s): 当前%.2f | 多%.2f 空%.2f\n",
|
||||
exchange, oiData.CurrentOI, oiData.NetLong, oiData.NetShort))
|
||||
if len(oiData.Delta) > 0 {
|
||||
sb.WriteString(" 持仓变化: ")
|
||||
parts := []string{}
|
||||
for _, tf := range []string{"1h", "4h", "24h"} {
|
||||
if d, ok := oiData.Delta[tf]; ok {
|
||||
parts = append(parts, fmt.Sprintf("%s: %+.2f%%", tf, d.OIDeltaPercent))
|
||||
}
|
||||
}
|
||||
sb.WriteString(strings.Join(parts, " | "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// fetchSingleExternalSource 获取单个外部数据源
|
||||
func (e *StrategyEngine) fetchSingleExternalSource(source store.ExternalDataSource) (interface{}, error) {
|
||||
client := &http.Client{
|
||||
@@ -316,6 +503,13 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
|
||||
sourceTags := e.formatCoinSourceTag(coin.Sources)
|
||||
sb.WriteString(fmt.Sprintf("### %d. %s%s\n\n", displayedCount, coin.Symbol, sourceTags))
|
||||
sb.WriteString(e.formatMarketData(marketData))
|
||||
|
||||
// 添加量化数据(如果有)
|
||||
if ctx.QuantDataMap != nil {
|
||||
if quantData, hasQuant := ctx.QuantDataMap[coin.Symbol]; hasQuant {
|
||||
sb.WriteString(e.formatQuantData(quantData))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
@@ -358,6 +552,13 @@ func (e *StrategyEngine) formatPositionInfo(index int, pos PositionInfo, ctx *Co
|
||||
// 使用策略配置的指标输出市场数据
|
||||
if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok {
|
||||
sb.WriteString(e.formatMarketData(marketData))
|
||||
|
||||
// 添加量化数据(如果有)
|
||||
if ctx.QuantDataMap != nil {
|
||||
if quantData, hasQuant := ctx.QuantDataMap[pos.Symbol]; hasQuant {
|
||||
sb.WriteString(e.formatQuantData(quantData))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
@@ -706,6 +907,10 @@ func (e *StrategyEngine) writeAvailableIndicators(sb *strings.Builder) {
|
||||
if len(e.config.CoinSource.StaticCoins) > 0 || e.config.CoinSource.UseCoinPool || e.config.CoinSource.UseOITop {
|
||||
sb.WriteString("- AI500 / OI_Top 筛选标签(若有)\n")
|
||||
}
|
||||
|
||||
if indicators.EnableQuantData {
|
||||
sb.WriteString("- 量化数据(机构/散户资金流向、持仓变化、多周期价格变化)\n")
|
||||
}
|
||||
}
|
||||
|
||||
// GetRiskControlConfig 获取风险控制配置
|
||||
|
||||
@@ -91,6 +91,9 @@ type IndicatorConfig struct {
|
||||
ATRPeriods []int `json:"atr_periods,omitempty"` // 默认 [14]
|
||||
// 外部数据源
|
||||
ExternalDataSources []ExternalDataSource `json:"external_data_sources,omitempty"`
|
||||
// 量化数据源(资金流向、持仓变化、价格变化)
|
||||
EnableQuantData bool `json:"enable_quant_data"` // 是否启用量化数据
|
||||
QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // 量化数据 API 地址
|
||||
}
|
||||
|
||||
// KlineConfig K线配置
|
||||
@@ -211,6 +214,8 @@ func (s *StrategyStore) initDefaultData() error {
|
||||
EMAPeriods: []int{20, 50},
|
||||
RSIPeriods: []int{7, 14},
|
||||
ATRPeriods: []int{14},
|
||||
EnableQuantData: true,
|
||||
QuantDataAPIURL: "http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c",
|
||||
},
|
||||
RiskControl: RiskControlConfig{
|
||||
MaxPositions: 3,
|
||||
|
||||
@@ -683,6 +683,27 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 获取量化数据(如果策略配置启用)
|
||||
if strategyConfig.Indicators.EnableQuantData && strategyConfig.Indicators.QuantDataAPIURL != "" {
|
||||
// 收集需要查询的币种(候选币种 + 持仓币种)
|
||||
symbolsToQuery := make(map[string]bool)
|
||||
for _, coin := range candidateCoins {
|
||||
symbolsToQuery[coin.Symbol] = true
|
||||
}
|
||||
for _, pos := range positionInfos {
|
||||
symbolsToQuery[pos.Symbol] = true
|
||||
}
|
||||
|
||||
symbols := make([]string, 0, len(symbolsToQuery))
|
||||
for sym := range symbolsToQuery {
|
||||
symbols = append(symbols, sym)
|
||||
}
|
||||
|
||||
logger.Infof("📊 [%s] 正在获取 %d 个币种的量化数据...", at.name, len(symbols))
|
||||
ctx.QuantDataMap = at.strategyEngine.FetchQuantDataBatch(symbols)
|
||||
logger.Infof("📊 [%s] 成功获取 %d 个币种的量化数据", at.name, len(ctx.QuantDataMap))
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { EquityChart } from './EquityChart'
|
||||
import { TradingViewChart } from './TradingViewChart'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
@@ -7,13 +7,25 @@ import { BarChart3, CandlestickChart } from 'lucide-react'
|
||||
|
||||
interface ChartTabsProps {
|
||||
traderId: string
|
||||
selectedSymbol?: string // 从外部选择的币种
|
||||
updateKey?: number // 强制更新的 key
|
||||
}
|
||||
|
||||
type ChartTab = 'equity' | 'kline'
|
||||
|
||||
export function ChartTabs({ traderId }: ChartTabsProps) {
|
||||
export function ChartTabs({ traderId, selectedSymbol, updateKey }: ChartTabsProps) {
|
||||
const { language } = useLanguage()
|
||||
const [activeTab, setActiveTab] = useState<ChartTab>('equity')
|
||||
const [chartSymbol, setChartSymbol] = useState<string>('BTCUSDT')
|
||||
|
||||
// 当从外部选择币种时,自动切换到K线图
|
||||
useEffect(() => {
|
||||
if (selectedSymbol) {
|
||||
console.log('[ChartTabs] 收到币种选择:', selectedSymbol, 'updateKey:', updateKey)
|
||||
setChartSymbol(selectedSymbol)
|
||||
setActiveTab('kline')
|
||||
}
|
||||
}, [selectedSymbol, updateKey])
|
||||
|
||||
console.log('[ChartTabs] rendering, activeTab:', activeTab)
|
||||
|
||||
@@ -81,7 +93,12 @@ export function ChartTabs({ traderId }: ChartTabsProps) {
|
||||
{activeTab === 'equity' ? (
|
||||
<EquityChart traderId={traderId} embedded />
|
||||
) : (
|
||||
<TradingViewChart height={400} embedded />
|
||||
<TradingViewChart
|
||||
height={400}
|
||||
embedded
|
||||
defaultSymbol={chartSymbol}
|
||||
key={chartSymbol} // 强制重新渲染当币种变化时
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,6 +66,14 @@ function TradingViewChartComponent({
|
||||
const [showSymbolDropdown, setShowSymbolDropdown] = useState(false)
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
|
||||
// 当外部传入的 defaultSymbol 变化时,更新内部 symbol
|
||||
useEffect(() => {
|
||||
if (defaultSymbol && defaultSymbol !== symbol) {
|
||||
console.log('[TradingViewChart] 更新币种:', defaultSymbol)
|
||||
setSymbol(defaultSymbol)
|
||||
}
|
||||
}, [defaultSymbol])
|
||||
|
||||
// 获取完整的交易对符号 (合约格式: BINANCE:BTCUSDT.P)
|
||||
const getFullSymbol = () => {
|
||||
const exchangeInfo = EXCHANGES.find((e) => e.id === exchange)
|
||||
@@ -102,7 +110,8 @@ function TradingViewChartComponent({
|
||||
script.type = 'text/javascript'
|
||||
script.async = true
|
||||
script.innerHTML = JSON.stringify({
|
||||
autosize: true,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
symbol: getFullSymbol(),
|
||||
interval: timeInterval,
|
||||
timezone: 'Etc/UTC',
|
||||
@@ -147,9 +156,10 @@ function TradingViewChartComponent({
|
||||
<div
|
||||
className={`${embedded ? '' : 'binance-card'} overflow-hidden ${embedded ? '' : 'animate-fade-in'} ${
|
||||
isFullscreen
|
||||
? 'fixed inset-0 z-50 rounded-none'
|
||||
? 'fixed inset-0 z-50 rounded-none flex flex-col'
|
||||
: ''
|
||||
}`}
|
||||
style={isFullscreen ? { background: '#0B0E11' } : undefined}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
@@ -354,8 +364,9 @@ function TradingViewChartComponent({
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
height: isFullscreen ? 'calc(100vh - 60px)' : height,
|
||||
height: isFullscreen ? 'calc(100vh - 65px)' : height,
|
||||
background: '#0B0E11',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Clock, Activity } from 'lucide-react'
|
||||
import { Clock, Activity, Database } from 'lucide-react'
|
||||
import type { IndicatorConfig } from '../../types'
|
||||
|
||||
interface IndicatorEditorProps {
|
||||
@@ -51,6 +51,9 @@ export function IndicatorEditor({
|
||||
intraday: { zh: '日内', en: 'Intraday' },
|
||||
swing: { zh: '波段', en: 'Swing' },
|
||||
position: { zh: '趋势', en: 'Position' },
|
||||
quantData: { zh: '量化数据', en: 'Quant Data' },
|
||||
quantDataDesc: { zh: '资金流向、持仓变化、价格变化(按币种查询)', en: 'Netflow, OI delta, price change (per coin)' },
|
||||
quantDataUrl: { zh: '量化数据 API', en: 'Quant Data API' },
|
||||
}
|
||||
return translations[key]?.[language] || key
|
||||
}
|
||||
@@ -257,6 +260,57 @@ export function IndicatorEditor({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quant Data Source */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Database className="w-4 h-4" style={{ color: '#22c55e' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
||||
</div>
|
||||
<p className="text-xs mb-3" style={{ color: '#848E9C' }}>{t('quantDataDesc')}</p>
|
||||
|
||||
<div
|
||||
className="p-3 rounded-lg space-y-3"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
{/* Enable Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} />
|
||||
<span className="text-xs" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_quant_data || false}
|
||||
onChange={(e) =>
|
||||
!disabled && onChange({ ...config, enable_quant_data: e.target.checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="w-4 h-4 rounded accent-green-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* API URL */}
|
||||
{config.enable_quant_data && (
|
||||
<div>
|
||||
<label className="text-[10px] mb-1 block" style={{ color: '#848E9C' }}>
|
||||
{t('quantDataUrl')} <span style={{ color: '#5E6673' }}>({'{symbol}'} = 币种)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.quant_data_api_url || ''}
|
||||
onChange={(e) =>
|
||||
!disabled && onChange({ ...config, quant_data_api_url: e.target.value })
|
||||
}
|
||||
disabled={disabled}
|
||||
placeholder="http://example.com/api/coin/{symbol}?include=netflow,oi,price"
|
||||
className="w-full px-2 py-1.5 rounded text-xs font-mono"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ export const translations = {
|
||||
|
||||
// TradingView Chart
|
||||
marketChart: 'Market Chart',
|
||||
viewChart: 'Click to view chart',
|
||||
enterSymbol: 'Enter symbol...',
|
||||
popularSymbols: 'Popular Symbols',
|
||||
fullscreen: 'Fullscreen',
|
||||
@@ -1083,6 +1084,7 @@ export const translations = {
|
||||
|
||||
// TradingView Chart
|
||||
marketChart: '行情图表',
|
||||
viewChart: '点击查看图表',
|
||||
enterSymbol: '输入币种...',
|
||||
popularSymbols: '热门币种',
|
||||
fullscreen: '全屏',
|
||||
|
||||
@@ -59,6 +59,16 @@ export default function TraderDashboard() {
|
||||
return saved ? parseInt(saved, 10) : 5
|
||||
})
|
||||
|
||||
// 选中的币种(用于图表显示)
|
||||
const [selectedChartSymbol, setSelectedChartSymbol] = useState<string | undefined>()
|
||||
const [chartUpdateKey, setChartUpdateKey] = useState(0)
|
||||
|
||||
// 点击持仓币种时调用
|
||||
const handlePositionSymbolClick = (symbol: string) => {
|
||||
setSelectedChartSymbol(symbol)
|
||||
setChartUpdateKey(prev => prev + 1) // 强制触发更新
|
||||
}
|
||||
|
||||
// 当 limit 变化时保存到 localStorage
|
||||
const handleLimitChange = (newLimit: number) => {
|
||||
setDecisionLimit(newLimit)
|
||||
@@ -420,7 +430,11 @@ export default function TraderDashboard() {
|
||||
<div className="space-y-6">
|
||||
{/* Chart Tabs (Equity / K-line) */}
|
||||
<div className="animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
||||
<ChartTabs traderId={selectedTrader.trader_id} />
|
||||
<ChartTabs
|
||||
traderId={selectedTrader.trader_id}
|
||||
selectedSymbol={selectedChartSymbol}
|
||||
updateKey={chartUpdateKey}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Current Positions */}
|
||||
@@ -490,7 +504,24 @@ export default function TraderDashboard() {
|
||||
className="border-b border-gray-800 last:border-0"
|
||||
>
|
||||
<td className="py-3 font-mono font-semibold">
|
||||
{pos.symbol}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
console.log('点击了币种:', pos.symbol)
|
||||
handlePositionSymbolClick(pos.symbol)
|
||||
}}
|
||||
className="hover:underline cursor-pointer px-2 py-1 rounded transition-all"
|
||||
style={{
|
||||
color: '#F0B90B',
|
||||
background: 'rgba(240, 185, 11, 0.1)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.3)'
|
||||
}}
|
||||
title={t('viewChart', language)}
|
||||
>
|
||||
📈 {pos.symbol}
|
||||
</button>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<span
|
||||
|
||||
@@ -403,6 +403,9 @@ export interface IndicatorConfig {
|
||||
rsi_periods?: number[];
|
||||
atr_periods?: number[];
|
||||
external_data_sources?: ExternalDataSource[];
|
||||
// 量化数据源(资金流向、持仓变化、价格变化)
|
||||
enable_quant_data?: boolean;
|
||||
quant_data_api_url?: string;
|
||||
}
|
||||
|
||||
export interface KlineConfig {
|
||||
|
||||
Reference in New Issue
Block a user