Trae Assistant
Enhance Quant Grid Master: Robustness, UI, and HF Deployment
fe49021
<!DOCTYPE html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quant Grid Master - 量化网格交易大师</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
gray: {
900: '#1a1b1e',
800: '#25262b',
700: '#2c2e33',
600: '#373a40',
},
primary: '#4dabf7',
success: '#40c057',
danger: '#fa5252'
}
}
}
}
</script>
<style>
[v-cloak] { display: none; }
body { background-color: #1a1b1e; color: #c1c2c5; }
.chart-container { height: 400px; width: 100%; }
.input-dark {
background-color: #2c2e33;
border: 1px solid #373a40;
color: #fff;
}
.input-dark:focus {
border-color: #4dabf7;
outline: none;
ring: 2px solid #4dabf7;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1a1b1e;
}
::-webkit-scrollbar-thumb {
background: #373a40;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #4dabf7;
}
</style>
</head>
<body class="h-screen overflow-hidden flex flex-col font-sans">
<div id="app" v-cloak class="flex-1 flex flex-col h-full">
<!-- Header -->
<header class="bg-gray-800 border-b border-gray-700 p-4 flex justify-between items-center shadow-md z-10">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-cyan-400 rounded-lg flex items-center justify-center font-bold text-white shadow-lg shadow-blue-500/30">Q</div>
<h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-cyan-300">Quant Grid Master</h1>
<span class="text-xs px-2 py-0.5 rounded bg-gray-700 text-gray-400 border border-gray-600">Beta</span>
</div>
<div class="flex gap-4 text-sm text-gray-400 items-center">
<span class="hidden md:inline">量化网格交易模拟系统</span>
<a href="https://huggingface.co/spaces/duqing026/quant-grid-master" target="_blank" class="hover:text-white transition">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path></svg>
</a>
</div>
</header>
<!-- Main Content -->
<main class="flex-1 flex overflow-hidden">
<!-- Sidebar / Controls -->
<aside class="w-80 bg-gray-800 border-r border-gray-700 overflow-y-auto p-4 flex flex-col gap-6 custom-scrollbar">
<!-- Import/Export Tools -->
<div class="grid grid-cols-2 gap-2">
<button @click="triggerFileInput" class="py-1.5 px-3 bg-gray-700 hover:bg-gray-600 text-gray-300 text-xs rounded border border-gray-600 transition flex items-center justify-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
导入配置
</button>
<input type="file" ref="fileInput" class="hidden" accept=".json" @change="uploadConfig">
<button @click="exportConfig" class="py-1.5 px-3 bg-gray-700 hover:bg-gray-600 text-gray-300 text-xs rounded border border-gray-600 transition flex items-center justify-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
导出配置
</button>
</div>
<!-- Market Data Gen -->
<div class="space-y-3">
<h2 class="text-sm font-semibold text-gray-300 uppercase tracking-wider flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
1. 市场数据生成
</h2>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="text-xs text-gray-500 block mb-1">初始价格 (Start)</label>
<input type="number" v-model.number="dataConfig.start_price" class="w-full px-3 py-2 rounded text-sm input-dark transition focus:ring-1">
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">持续天数 (Days)</label>
<input type="number" v-model.number="dataConfig.days" class="w-full px-3 py-2 rounded text-sm input-dark transition focus:ring-1">
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">波动率 (Volatility)</label>
<input type="number" step="0.01" v-model.number="dataConfig.volatility" class="w-full px-3 py-2 rounded text-sm input-dark transition focus:ring-1">
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">趋势 (Trend)</label>
<input type="number" step="0.001" v-model.number="dataConfig.trend" class="w-full px-3 py-2 rounded text-sm input-dark transition focus:ring-1">
</div>
</div>
<button @click="generateData" :disabled="loading" class="w-full py-2 bg-gray-700 hover:bg-gray-600 text-white rounded text-sm transition font-medium flex justify-center items-center gap-2 border border-gray-600">
<span v-if="loading" class="animate-spin"></span>
<span v-else></span>
生成模拟行情
</button>
</div>
<!-- Strategy Config -->
<div class="space-y-3 pt-4 border-t border-gray-700">
<h2 class="text-sm font-semibold text-gray-300 uppercase tracking-wider flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span>
2. 网格策略参数
</h2>
<div>
<label class="text-xs text-gray-500 block mb-1">投入金额 (Investment USDT)</label>
<div class="relative">
<span class="absolute left-3 top-2 text-gray-500 text-sm">$</span>
<input type="number" v-model.number="strategyConfig.investment" class="w-full pl-6 pr-3 py-2 rounded text-sm input-dark font-mono text-yellow-500 transition focus:ring-1">
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="text-xs text-gray-500 block mb-1">区间下限 (Low)</label>
<input type="number" v-model.number="strategyConfig.lower_price" class="w-full px-3 py-2 rounded text-sm input-dark transition focus:ring-1">
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">区间上限 (High)</label>
<input type="number" v-model.number="strategyConfig.upper_price" class="w-full px-3 py-2 rounded text-sm input-dark transition focus:ring-1">
</div>
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">网格数量 (Grids)</label>
<div class="flex items-center gap-2">
<input type="range" min="2" max="100" v-model.number="strategyConfig.grid_num" class="flex-1 accent-blue-500 h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer">
<input type="number" v-model.number="strategyConfig.grid_num" class="w-16 px-2 py-1 rounded text-sm input-dark text-center">
</div>
</div>
<button @click="runSimulation" :disabled="!hasData || loading" :class="{'opacity-50 cursor-not-allowed': !hasData || loading}" class="w-full py-2.5 bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-500 hover:to-blue-400 text-white rounded text-sm font-bold shadow-lg shadow-blue-900/50 transition transform active:scale-95 flex justify-center items-center gap-2">
<span v-if="loading && hasData" class="animate-spin"></span>
开始回测 (Run Backtest)
</button>
</div>
<!-- Results Summary -->
<div v-if="summary" class="mt-auto bg-gray-900/80 p-4 rounded-lg border border-gray-700 space-y-3 shadow-inner backdrop-blur-sm">
<div class="flex justify-between items-center pb-2 border-b border-gray-700">
<span class="text-gray-400 text-xs">总收益 (Total Profit)</span>
<span :class="summary.total_profit >= 0 ? 'text-success' : 'text-danger'" class="font-mono font-bold text-lg">
${ summary.total_profit >= 0 ? '+' : '' }${ summary.total_profit.toFixed(2) }
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-400 text-xs">网格套利 (Arbitrage)</span>
<span class="text-success font-mono text-sm">
+${ summary.arbitrage_profit.toFixed(2) }
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-400 text-xs">预估年化 (APY)</span>
<span class="text-blue-400 font-mono text-sm">
${ (summary.grid_yield * (365/dataConfig.days)).toFixed(2) }%
</span>
</div>
</div>
</aside>
<!-- Charts Area -->
<section class="flex-1 bg-gray-900 p-4 flex flex-col gap-4 overflow-hidden relative">
<!-- Main Chart -->
<div class="flex-1 bg-gray-800 rounded-lg border border-gray-700 p-4 flex flex-col relative shadow-lg">
<h3 class="text-sm font-semibold text-gray-400 mb-2 flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="w-1 h-4 bg-blue-500 rounded-sm"></span>
<span>价格走势与买卖点 (Price Action)</span>
</div>
<span v-if="trades.length" class="text-xs bg-gray-700 px-2 py-0.5 rounded text-white font-mono">Trades: ${ trades.length }</span>
</h3>
<div id="priceChart" class="flex-1 w-full min-h-0"></div>
<div v-if="!hasData" class="absolute inset-0 flex flex-col items-center justify-center text-gray-600 pointer-events-none gap-2">
<svg class="w-12 h-12 opacity-20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"></path></svg>
<span class="text-sm opacity-50">请点击左侧"生成模拟行情"开始</span>
</div>
</div>
<!-- Profit Chart -->
<div class="h-1/3 bg-gray-800 rounded-lg border border-gray-700 p-4 flex flex-col shadow-lg">
<h3 class="text-sm font-semibold text-gray-400 mb-2 flex items-center gap-2">
<span class="w-1 h-4 bg-green-500 rounded-sm"></span>
<span>收益曲线 (Profit Curve)</span>
</h3>
<div id="profitChart" class="flex-1 w-full min-h-0"></div>
</div>
</section>
</main>
<footer class="bg-gray-800 border-t border-gray-700 p-2 text-center text-xs text-gray-500">
&copy; 2024 Quant Grid Master. Powered by Flask & Vue 3.
</footer>
</div>
<script>
const { createApp, ref, onMounted, nextTick } = Vue
createApp({
delimiters: ['${', '}'],
setup() {
const loading = ref(false)
const hasData = ref(false)
const prices = ref([])
const trades = ref([])
const summary = ref(null)
const fileInput = ref(null)
const dataConfig = ref({
start_price: 1000,
days: 30,
volatility: 0.03,
trend: 0.000
})
const strategyConfig = ref({
lower_price: 800,
upper_price: 1200,
grid_num: 20,
investment: 10000
})
let priceChartInstance = null
let profitChartInstance = null
const initCharts = () => {
priceChartInstance = echarts.init(document.getElementById('priceChart'), 'dark', { backgroundColor: 'transparent' })
profitChartInstance = echarts.init(document.getElementById('profitChart'), 'dark', { backgroundColor: 'transparent' })
window.addEventListener('resize', () => {
priceChartInstance && priceChartInstance.resize()
profitChartInstance && profitChartInstance.resize()
})
}
const generateData = async () => {
loading.value = true
try {
const res = await fetch('/api/generate_data', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(dataConfig.value)
})
if (!res.ok) throw new Error(await res.text())
const data = await res.json()
prices.value = data.prices
hasData.value = true
// Auto-set reasonable grid range based on generated data
const priceValues = prices.value.map(p => p.price)
const minP = Math.min(...priceValues)
const maxP = Math.max(...priceValues)
strategyConfig.value.lower_price = Math.floor(minP * 0.95)
strategyConfig.value.upper_price = Math.ceil(maxP * 1.05)
renderPriceChart(prices.value)
// Clear previous results
summary.value = null
trades.value = []
profitChartInstance.clear()
} catch (e) {
console.error(e)
alert('Error generating data: ' + e.message)
} finally {
loading.value = false
}
}
const runSimulation = async () => {
if (!hasData.value) return
loading.value = true
try {
const res = await fetch('/api/simulate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
prices: prices.value,
params: strategyConfig.value
})
})
if (!res.ok) throw new Error(await res.text())
const data = await res.json()
trades.value = data.trades
summary.value = data.summary
renderPriceChart(prices.value, data.trades, strategyConfig.value)
renderProfitChart(data.results)
} catch (e) {
console.error(e)
alert('Simulation failed: ' + e.message)
} finally {
loading.value = false
}
}
// File Upload Logic
const triggerFileInput = () => {
fileInput.value.click()
}
const uploadConfig = async (event) => {
const file = event.target.files[0]
if (!file) return
// Client-side validation
if (file.size > 5 * 1024 * 1024) {
alert('File too large (Max 5MB)')
event.target.value = ''
return
}
const formData = new FormData()
formData.append('file', file)
loading.value = true
try {
const res = await fetch('/api/upload_config', {
method: 'POST',
body: formData
})
const result = await res.json()
if (!res.ok) {
throw new Error(result.error || 'Upload failed')
}
// Apply config
if (result.config) {
if (result.config.dataConfig) dataConfig.value = { ...dataConfig.value, ...result.config.dataConfig }
if (result.config.strategyConfig) strategyConfig.value = { ...strategyConfig.value, ...result.config.strategyConfig }
alert('配置导入成功 (Config Imported)')
}
} catch (e) {
alert(e.message)
} finally {
loading.value = false
event.target.value = '' // Reset input
}
}
const exportConfig = () => {
const config = {
dataConfig: dataConfig.value,
strategyConfig: strategyConfig.value,
exportDate: new Date().toISOString()
}
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'quant_grid_config.json'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
const renderPriceChart = (priceData, tradeData = [], config = null) => {
const dates = priceData.map(p => p.time)
const values = priceData.map(p => p.price)
const markPoints = []
tradeData.forEach(t => {
markPoints.push({
name: t.type,
coord: [t.time, t.price],
value: t.type,
itemStyle: {
color: t.type === 'BUY' ? '#40c057' : '#fa5252'
},
symbol: t.type === 'BUY' ? 'arrowUp' : 'arrowDown',
symbolSize: 10
})
})
const markLines = []
if (config) {
// Show Top/Bottom lines
markLines.push({ yAxis: config.lower_price, lineStyle: { color: '#fa5252', type: 'dashed' }, label: { formatter: 'Low' } })
markLines.push({ yAxis: config.upper_price, lineStyle: { color: '#fa5252', type: 'dashed' }, label: { formatter: 'High' } })
// Optional: Show internal grids if not too many
if (config.grid_num <= 30) {
const step = (config.upper_price - config.lower_price) / config.grid_num
for(let i=1; i<config.grid_num; i++) {
markLines.push({
yAxis: config.lower_price + i*step,
lineStyle: { color: '#373a40', width: 1, type: 'dotted' },
label: { show: false }
})
}
}
}
const option = {
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
grid: { left: '50', right: '20', top: '30', bottom: '30' },
xAxis: {
type: 'category',
data: dates,
boundaryGap: false,
axisLine: { lineStyle: { color: '#373a40' } },
axisLabel: { color: '#9ca3af' }
},
yAxis: {
type: 'value',
scale: true,
splitLine: { show: false },
axisLine: { show: false },
axisLabel: { color: '#9ca3af' }
},
series: [{
name: 'Price',
type: 'line',
data: values,
smooth: true,
showSymbol: false,
lineStyle: { width: 2, color: '#4dabf7' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(77, 171, 247, 0.3)' },
{ offset: 1, color: 'rgba(77, 171, 247, 0)' }
])
},
markPoint: { data: markPoints },
markLine: {
data: markLines,
symbol: ['none', 'none']
}
}],
dataZoom: [{ type: 'inside' }, { type: 'slider', bottom: 0, height: 20, borderColor: 'transparent', backgroundColor: '#25262b' }]
}
priceChartInstance.setOption(option)
}
const renderProfitChart = (results) => {
const dates = results.map(r => r.timestamp)
const totalValues = results.map(r => r.total_value)
const arbitrageProfits = results.map(r => r.arbitrage_profit)
const option = {
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
legend: { data: ['Total Assets Value', 'Arbitrage Profit'], textStyle: { color: '#9ca3af' } },
grid: { left: '50', right: '20', top: '30', bottom: '30' },
xAxis: {
type: 'category',
data: dates,
boundaryGap: false,
axisLine: { lineStyle: { color: '#373a40' } },
axisLabel: { color: '#9ca3af' }
},
yAxis: {
type: 'value',
scale: true,
splitLine: { show: true, lineStyle: { color: '#373a40' } },
axisLabel: { color: '#9ca3af' }
},
series: [
{
name: 'Total Assets Value',
type: 'line',
data: totalValues,
showSymbol: false,
itemStyle: { color: '#fff' }
},
{
name: 'Arbitrage Profit',
type: 'line',
data: arbitrageProfits,
showSymbol: false,
areaStyle: { opacity: 0.3 },
itemStyle: { color: '#40c057' }
}
]
}
profitChartInstance.setOption(option)
}
onMounted(() => {
initCharts()
})
return {
loading, hasData, dataConfig, strategyConfig,
generateData, runSimulation, summary, trades,
fileInput, triggerFileInput, uploadConfig, exportConfig
}
}
}).mount('#app')
</script>
</body>
</html>