Spaces:
Sleeping
Sleeping
| <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"> | |
| © 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> | |