feat: add AI grid trading and market regime classification

- Add GridTrader interface with PlaceLimitOrder, CancelOrder, GetOrderBook
- Implement GridTrader for all exchanges (Binance, Bybit, OKX, Bitget, Hyperliquid, Aster, Lighter)
- Add grid engine with ATR-based boundary calculation and fund distribution
- Add market regime classification documents (Chinese/English)
- Add GridConfigEditor component for frontend configuration
This commit is contained in:
tinkle-community
2026-01-13 10:33:02 +08:00
parent 705aa641b0
commit 5fb26c17dc
22 changed files with 3901 additions and 32 deletions

View File

@@ -0,0 +1,424 @@
import { Grid, DollarSign, TrendingUp, Shield } from 'lucide-react'
import type { GridStrategyConfig } from '../../types'
interface GridConfigEditorProps {
config: GridStrategyConfig
onChange: (config: GridStrategyConfig) => void
disabled?: boolean
language: string
}
// Default grid config
export const defaultGridConfig: GridStrategyConfig = {
symbol: 'BTCUSDT',
grid_count: 10,
total_investment: 1000,
leverage: 5,
upper_price: 0,
lower_price: 0,
use_atr_bounds: true,
atr_multiplier: 2.0,
distribution: 'gaussian',
max_drawdown_pct: 15,
stop_loss_pct: 5,
daily_loss_limit_pct: 10,
use_maker_only: true,
}
export function GridConfigEditor({
config,
onChange,
disabled,
language,
}: GridConfigEditorProps) {
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
// Section titles
tradingPair: { zh: '交易设置', en: 'Trading Setup' },
gridParameters: { zh: '网格参数', en: 'Grid Parameters' },
priceBounds: { zh: '价格边界', en: 'Price Bounds' },
riskControl: { zh: '风险控制', en: 'Risk Control' },
// Trading pair
symbol: { zh: '交易对', en: 'Trading Pair' },
symbolDesc: { zh: '选择要进行网格交易的交易对', en: 'Select trading pair for grid trading' },
// Investment
totalInvestment: { zh: '投资金额 (USDT)', en: 'Investment (USDT)' },
totalInvestmentDesc: { zh: '网格策略的总投资金额', en: 'Total investment for grid strategy' },
leverage: { zh: '杠杆倍数', en: 'Leverage' },
leverageDesc: { zh: '交易使用的杠杆倍数 (1-20)', en: 'Leverage for trading (1-20)' },
// Grid parameters
gridCount: { zh: '网格数量', en: 'Grid Count' },
gridCountDesc: { zh: '网格层级数量 (5-50)', en: 'Number of grid levels (5-50)' },
distribution: { zh: '资金分配方式', en: 'Distribution' },
distributionDesc: { zh: '网格层级的资金分配方式', en: 'Fund allocation across grid levels' },
uniform: { zh: '均匀分配', en: 'Uniform' },
gaussian: { zh: '高斯分配 (推荐)', en: 'Gaussian (Recommended)' },
pyramid: { zh: '金字塔分配', en: 'Pyramid' },
// Price bounds
useAtrBounds: { zh: '自动计算边界 (ATR)', en: 'Auto-calculate Bounds (ATR)' },
useAtrBoundsDesc: { zh: '基于 ATR 自动计算网格上下边界', en: 'Auto-calculate bounds based on ATR' },
atrMultiplier: { zh: 'ATR 倍数', en: 'ATR Multiplier' },
atrMultiplierDesc: { zh: '边界距离当前价格的 ATR 倍数', en: 'ATR multiplier for bounds distance' },
upperPrice: { zh: '上边界价格', en: 'Upper Price' },
upperPriceDesc: { zh: '网格上边界价格 (0=自动计算)', en: 'Grid upper bound (0=auto)' },
lowerPrice: { zh: '下边界价格', en: 'Lower Price' },
lowerPriceDesc: { zh: '网格下边界价格 (0=自动计算)', en: 'Grid lower bound (0=auto)' },
// Risk control
maxDrawdown: { zh: '最大回撤 (%)', en: 'Max Drawdown (%)' },
maxDrawdownDesc: { zh: '触发紧急退出的最大回撤百分比', en: 'Max drawdown before emergency exit' },
stopLoss: { zh: '止损 (%)', en: 'Stop Loss (%)' },
stopLossDesc: { zh: '单仓位止损百分比', en: 'Stop loss per position' },
dailyLossLimit: { zh: '日损失限制 (%)', en: 'Daily Loss Limit (%)' },
dailyLossLimitDesc: { zh: '每日最大亏损百分比', en: 'Maximum daily loss percentage' },
useMakerOnly: { zh: '仅使用 Maker 订单', en: 'Maker Only Orders' },
useMakerOnlyDesc: { zh: '使用限价单以降低手续费', en: 'Use limit orders for lower fees' },
}
return translations[key]?.[language] || key
}
const updateField = <K extends keyof GridStrategyConfig>(
key: K,
value: GridStrategyConfig[K]
) => {
if (!disabled) {
onChange({ ...config, [key]: value })
}
}
const inputStyle = {
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
}
const sectionStyle = {
background: '#0B0E11',
border: '1px solid #2B3139',
}
return (
<div className="space-y-6">
{/* Trading Setup */}
<div>
<div className="flex items-center gap-2 mb-4">
<DollarSign className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('tradingPair')}
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Symbol */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('symbol')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('symbolDesc')}
</p>
<select
value={config.symbol}
onChange={(e) => updateField('symbol', e.target.value)}
disabled={disabled}
className="w-full px-3 py-2 rounded"
style={inputStyle}
>
<option value="BTCUSDT">BTC/USDT</option>
<option value="ETHUSDT">ETH/USDT</option>
<option value="SOLUSDT">SOL/USDT</option>
<option value="BNBUSDT">BNB/USDT</option>
<option value="XRPUSDT">XRP/USDT</option>
<option value="DOGEUSDT">DOGE/USDT</option>
</select>
</div>
{/* Investment */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('totalInvestment')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('totalInvestmentDesc')}
</p>
<input
type="number"
value={config.total_investment}
onChange={(e) => updateField('total_investment', parseFloat(e.target.value) || 1000)}
disabled={disabled}
min={100}
step={100}
className="w-full px-3 py-2 rounded"
style={inputStyle}
/>
</div>
{/* Leverage */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('leverage')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('leverageDesc')}
</p>
<input
type="number"
value={config.leverage}
onChange={(e) => updateField('leverage', parseInt(e.target.value) || 5)}
disabled={disabled}
min={1}
max={20}
className="w-full px-3 py-2 rounded"
style={inputStyle}
/>
</div>
</div>
</div>
{/* Grid Parameters */}
<div>
<div className="flex items-center gap-2 mb-4">
<Grid className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('gridParameters')}
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Grid Count */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('gridCount')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('gridCountDesc')}
</p>
<input
type="number"
value={config.grid_count}
onChange={(e) => updateField('grid_count', parseInt(e.target.value) || 10)}
disabled={disabled}
min={5}
max={50}
className="w-full px-3 py-2 rounded"
style={inputStyle}
/>
</div>
{/* Distribution */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('distribution')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('distributionDesc')}
</p>
<select
value={config.distribution}
onChange={(e) => updateField('distribution', e.target.value as 'uniform' | 'gaussian' | 'pyramid')}
disabled={disabled}
className="w-full px-3 py-2 rounded"
style={inputStyle}
>
<option value="uniform">{t('uniform')}</option>
<option value="gaussian">{t('gaussian')}</option>
<option value="pyramid">{t('pyramid')}</option>
</select>
</div>
</div>
</div>
{/* Price Bounds */}
<div>
<div className="flex items-center gap-2 mb-4">
<TrendingUp className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('priceBounds')}
</h3>
</div>
{/* ATR Toggle */}
<div className="p-4 rounded-lg mb-4" style={sectionStyle}>
<div className="flex items-center justify-between">
<div>
<label className="block text-sm" style={{ color: '#EAECEF' }}>
{t('useAtrBounds')}
</label>
<p className="text-xs" style={{ color: '#848E9C' }}>
{t('useAtrBoundsDesc')}
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={config.use_atr_bounds}
onChange={(e) => updateField('use_atr_bounds', e.target.checked)}
disabled={disabled}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#F0B90B]"></div>
</label>
</div>
</div>
{config.use_atr_bounds ? (
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('atrMultiplier')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('atrMultiplierDesc')}
</p>
<input
type="number"
value={config.atr_multiplier}
onChange={(e) => updateField('atr_multiplier', parseFloat(e.target.value) || 2.0)}
disabled={disabled}
min={1}
max={5}
step={0.5}
className="w-32 px-3 py-2 rounded"
style={inputStyle}
/>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('upperPrice')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('upperPriceDesc')}
</p>
<input
type="number"
value={config.upper_price}
onChange={(e) => updateField('upper_price', parseFloat(e.target.value) || 0)}
disabled={disabled}
min={0}
step={0.01}
className="w-full px-3 py-2 rounded"
style={inputStyle}
/>
</div>
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('lowerPrice')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('lowerPriceDesc')}
</p>
<input
type="number"
value={config.lower_price}
onChange={(e) => updateField('lower_price', parseFloat(e.target.value) || 0)}
disabled={disabled}
min={0}
step={0.01}
className="w-full px-3 py-2 rounded"
style={inputStyle}
/>
</div>
</div>
)}
</div>
{/* Risk Control */}
<div>
<div className="flex items-center gap-2 mb-4">
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('riskControl')}
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxDrawdown')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('maxDrawdownDesc')}
</p>
<input
type="number"
value={config.max_drawdown_pct}
onChange={(e) => updateField('max_drawdown_pct', parseFloat(e.target.value) || 15)}
disabled={disabled}
min={5}
max={50}
className="w-full px-3 py-2 rounded"
style={inputStyle}
/>
</div>
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('stopLoss')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('stopLossDesc')}
</p>
<input
type="number"
value={config.stop_loss_pct}
onChange={(e) => updateField('stop_loss_pct', parseFloat(e.target.value) || 5)}
disabled={disabled}
min={1}
max={20}
className="w-full px-3 py-2 rounded"
style={inputStyle}
/>
</div>
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('dailyLossLimit')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('dailyLossLimitDesc')}
</p>
<input
type="number"
value={config.daily_loss_limit_pct}
onChange={(e) => updateField('daily_loss_limit_pct', parseFloat(e.target.value) || 10)}
disabled={disabled}
min={1}
max={30}
className="w-full px-3 py-2 rounded"
style={inputStyle}
/>
</div>
</div>
{/* Maker Only Toggle */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<div className="flex items-center justify-between">
<div>
<label className="block text-sm" style={{ color: '#EAECEF' }}>
{t('useMakerOnly')}
</label>
<p className="text-xs" style={{ color: '#848E9C' }}>
{t('useMakerOnlyDesc')}
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={config.use_maker_only}
onChange={(e) => updateField('use_maker_only', e.target.checked)}
disabled={disabled}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#F0B90B]"></div>
</label>
</div>
</div>
</div>
</div>
)
}

View File

@@ -37,6 +37,7 @@ import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
import { RiskControlEditor } from '../components/strategy/RiskControlEditor'
import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor'
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
import { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor'
import { DeepVoidBackground } from '../components/DeepVoidBackground'
const API_BASE = import.meta.env.VITE_API_BASE || ''
@@ -59,6 +60,7 @@ export function StrategyStudioPage() {
// Accordion states for left panel
const [expandedSections, setExpandedSections] = useState({
gridConfig: true,
coinSource: true,
indicators: false,
riskControl: false,
@@ -486,6 +488,12 @@ export function StrategyStudioPage() {
subtitle: { zh: '可视化配置和测试交易策略', en: 'Configure and test trading strategies' },
strategies: { zh: '策略', en: 'Strategies' },
newStrategy: { zh: '新建', en: 'New' },
strategyType: { zh: '策略类型', en: 'Strategy Type' },
aiTrading: { zh: 'AI 智能交易', en: 'AI Trading' },
aiTradingDesc: { zh: 'AI 分析市场并自主决策买卖', en: 'AI analyzes market and makes trading decisions' },
gridTrading: { zh: 'AI 网格交易', en: 'AI Grid Trading' },
gridTradingDesc: { zh: 'AI 控制网格策略,在震荡市场获利', en: 'AI-controlled grid strategy for ranging markets' },
gridConfig: { zh: '网格配置', en: 'Grid Configuration' },
coinSource: { zh: '币种来源', en: 'Coin Source' },
indicators: { zh: '技术指标', en: 'Indicators' },
riskControl: { zh: '风控参数', en: 'Risk Control' },
@@ -533,12 +541,33 @@ export function StrategyStudioPage() {
)
}
// Get current strategy type (default to ai_trading if not set)
const currentStrategyType = editingConfig?.strategy_type || 'ai_trading'
const configSections = [
// Grid Config - only for grid_trading
{
key: 'gridConfig' as const,
icon: Activity,
color: '#0ECB81',
title: t('gridConfig'),
forStrategyType: 'grid_trading' as const,
content: editingConfig?.grid_config && (
<GridConfigEditor
config={editingConfig.grid_config}
onChange={(gridConfig) => updateConfig('grid_config', gridConfig)}
disabled={selectedStrategy?.is_default}
language={language}
/>
),
},
// AI Trading sections
{
key: 'coinSource' as const,
icon: Target,
color: '#F0B90B',
title: t('coinSource'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<CoinSourceEditor
config={editingConfig.coin_source}
@@ -553,6 +582,7 @@ export function StrategyStudioPage() {
icon: BarChart3,
color: '#0ECB81',
title: t('indicators'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<IndicatorEditor
config={editingConfig.indicators}
@@ -567,6 +597,7 @@ export function StrategyStudioPage() {
icon: Shield,
color: '#F6465D',
title: t('riskControl'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<RiskControlEditor
config={editingConfig.risk_control}
@@ -581,6 +612,7 @@ export function StrategyStudioPage() {
icon: FileText,
color: '#a855f7',
title: t('promptSections'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<PromptSectionsEditor
config={editingConfig.prompt_sections}
@@ -595,6 +627,7 @@ export function StrategyStudioPage() {
icon: Settings,
color: '#60a5fa',
title: t('customPrompt'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<div>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
@@ -616,6 +649,7 @@ export function StrategyStudioPage() {
icon: Globe,
color: '#0ECB81',
title: t('publishSettings'),
forStrategyType: 'both' as const,
content: selectedStrategy && (
<PublishSettingsEditor
isPublic={selectedStrategy.is_public ?? false}
@@ -633,7 +667,9 @@ export function StrategyStudioPage() {
/>
),
},
]
].filter(section =>
section.forStrategyType === 'both' || section.forStrategyType === currentStrategyType
)
return (
<DeepVoidBackground className="h-[calc(100vh-64px)] flex flex-col bg-nofx-bg relative overflow-hidden">
@@ -813,6 +849,62 @@ export function StrategyStudioPage() {
</div>
</div>
{/* Strategy Type Selector */}
{editingConfig && (
<div className="mb-4 p-4 rounded-lg bg-nofx-bg-lighter border border-nofx-gold/20">
<div className="flex items-center gap-2 mb-3">
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium text-nofx-text">{t('strategyType')}</span>
</div>
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => {
if (!selectedStrategy?.is_default) {
updateConfig('strategy_type', 'ai_trading')
// Clear grid config when switching to AI trading
updateConfig('grid_config', undefined)
}
}}
disabled={selectedStrategy?.is_default}
className={`p-3 rounded-lg border transition-all ${
(!editingConfig.strategy_type || editingConfig.strategy_type === 'ai_trading')
? 'border-nofx-gold bg-nofx-gold/10'
: 'border-nofx-border hover:border-nofx-gold/50'
}`}
>
<div className="flex items-center gap-2 mb-1">
<Bot className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium text-nofx-text">{t('aiTrading')}</span>
</div>
<p className="text-xs text-nofx-text-muted text-left">{t('aiTradingDesc')}</p>
</button>
<button
onClick={() => {
if (!selectedStrategy?.is_default) {
updateConfig('strategy_type', 'grid_trading')
// Initialize grid config if not exists
if (!editingConfig.grid_config) {
updateConfig('grid_config', defaultGridConfig)
}
}
}}
disabled={selectedStrategy?.is_default}
className={`p-3 rounded-lg border transition-all ${
editingConfig.strategy_type === 'grid_trading'
? 'border-nofx-gold bg-nofx-gold/10'
: 'border-nofx-border hover:border-nofx-gold/50'
}`}
>
<div className="flex items-center gap-2 mb-1">
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
<span className="text-sm font-medium text-nofx-text">{t('gridTrading')}</span>
</div>
<p className="text-xs text-nofx-text-muted text-left">{t('gridTradingDesc')}</p>
</button>
</div>
</div>
)}
{/* Config Sections */}
<div className="space-y-2">
{configSections.map(({ key, icon: Icon, color, title, content }) => (

View File

@@ -151,6 +151,13 @@ export function TraderDashboardPage({
setPositionsCurrentPage(1)
}, [selectedTraderId, positionsPageSize])
// Auto-set chart symbol for grid trading
useEffect(() => {
if (status?.strategy_type === 'grid_trading' && status?.grid_symbol) {
setSelectedChartSymbol(status.grid_symbol)
}
}, [status?.strategy_type, status?.grid_symbol])
// Get current exchange info for perp-dex wallet display
const currentExchange = exchanges?.find(
(e) => e.id === selectedTrader?.exchange_id

View File

@@ -11,6 +11,8 @@ export interface SystemStatus {
stop_until: string
last_reset_time: string
ai_provider: string
strategy_type?: 'ai_trading' | 'grid_trading'
grid_symbol?: string
}
export interface AccountInfo {
@@ -462,6 +464,8 @@ export interface PromptSectionsConfig {
}
export interface StrategyConfig {
// Strategy type: "ai_trading" (default) or "grid_trading"
strategy_type?: 'ai_trading' | 'grid_trading';
// Language setting: "zh" for Chinese, "en" for English
// Determines the language used for data formatting and prompt generation
language?: 'zh' | 'en';
@@ -470,6 +474,38 @@ export interface StrategyConfig {
custom_prompt?: string;
risk_control: RiskControlConfig;
prompt_sections?: PromptSectionsConfig;
// Grid trading configuration (only used when strategy_type is 'grid_trading')
grid_config?: GridStrategyConfig;
}
// Grid trading specific configuration
export interface GridStrategyConfig {
// Trading pair (e.g., "BTCUSDT")
symbol: string;
// Number of grid levels (5-50)
grid_count: number;
// Total investment in USDT
total_investment: number;
// Leverage (1-20)
leverage: number;
// Upper price boundary (0 = auto-calculate from ATR)
upper_price: number;
// Lower price boundary (0 = auto-calculate from ATR)
lower_price: number;
// Use ATR to auto-calculate bounds
use_atr_bounds: boolean;
// ATR multiplier for bound calculation (default 2.0)
atr_multiplier: number;
// Position distribution: "uniform" | "gaussian" | "pyramid"
distribution: 'uniform' | 'gaussian' | 'pyramid';
// Maximum drawdown percentage before emergency exit
max_drawdown_pct: number;
// Stop loss percentage per position
stop_loss_pct: number;
// Daily loss limit percentage
daily_loss_limit_pct: number;
// Use maker-only orders for lower fees
use_maker_only: boolean;
}
export interface CoinSourceConfig {