diff --git a/web/DEPLOY_GUIDE.md b/web/DEPLOY_GUIDE.md new file mode 100644 index 0000000..14d42ec --- /dev/null +++ b/web/DEPLOY_GUIDE.md @@ -0,0 +1,212 @@ +# 缠论Web应用部署指南 + +本指南提供在服务器上直接部署缠论Web应用的步骤,无需使用Nginx等反向代理。 + +## 注意:Ubuntu 22.04+系统的特殊处理 + +最新版本的Ubuntu系统对Python包管理有额外限制,不允许直接用pip在系统级别安装包。因此,我们提供了使用虚拟环境的部署方法。 + +## 方法一:使用虚拟环境部署脚本(适用于Ubuntu 22.04+,推荐) + +这是最适合现代Ubuntu系统的方法: + +1. 登录到服务器 +2. 进入应用目录: + ``` + cd /path/to/user_data/Chan/web + ``` +3. 给脚本添加执行权限: + ``` + chmod +x deploy_venv.sh stop_venv.sh status_venv.sh + ``` +4. 启动服务: + ``` + ./deploy_venv.sh + ``` +5. 检查服务状态: + ``` + ./status_venv.sh + ``` +6. 如需停止服务: + ``` + ./stop_venv.sh + ``` + +## 方法二:标准部署脚本(适用于旧版Ubuntu或其他系统) + +如果您的系统允许直接安装Python包: + +1. 登录到服务器 +2. 进入应用目录: + ``` + cd /path/to/user_data/Chan/web + ``` +3. 给脚本添加执行权限: + ``` + chmod +x deploy.sh stop.sh status.sh + ``` +4. 启动服务: + ``` + ./deploy.sh + ``` +5. 检查服务状态: + ``` + ./status.sh + ``` +6. 如需停止服务: + ``` + ./stop.sh + ``` + +## 方法三:系统服务(systemd) + +如果希望应用随系统启动自动运行,可以设置systemd服务: + +### 使用虚拟环境的systemd服务(推荐) + +1. 编辑服务文件中的用户名、用户组和路径: + ``` + vi chanlun-web-venv.service + ``` + 修改以下行: + - `User=<用户名>` - 改为您的用户名 + - `Group=<用户组>` - 改为您的用户组 + - 所有包含`/path/to/`的路径 + +2. 将服务文件复制到systemd目录: + ``` + sudo cp chanlun-web-venv.service /etc/systemd/system/ + ``` + +3. 重新加载systemd配置: + ``` + sudo systemctl daemon-reload + ``` + +4. 启用并启动服务: + ``` + sudo systemctl enable chanlun-web-venv + sudo systemctl start chanlun-web-venv + ``` + +5. 检查服务状态: + ``` + sudo systemctl status chanlun-web-venv + ``` + +### 使用系统Python的systemd服务(仅适用于旧版系统) + +1. 编辑服务文件: + ``` + vi chanlun-web.service + ``` + +2. 按照上述相同的步骤配置和启动服务。 + +## 方法四:手动启动 + +如果只需要临时运行,可以手动启动: + +### 使用虚拟环境(推荐) + +1. 创建并激活虚拟环境: + ``` + python3 -m venv venv + source venv/bin/activate + ``` + +2. 安装依赖: + ``` + pip install -r requirements.txt + ``` + +3. 使用gunicorn启动(前台运行): + ``` + venv/bin/gunicorn -w 4 -b 0.0.0.0:8123 app:app + ``` + + 或后台运行: + ``` + venv/bin/gunicorn -w 4 -b 0.0.0.0:8123 --access-logfile logs/access.log --error-logfile logs/error.log --daemon app:app + ``` + +### 直接使用系统Python(仅适用于旧版系统) + +1. 安装依赖: + ``` + pip install -r requirements.txt + ``` + +2. 使用gunicorn启动(前台运行): + ``` + gunicorn -w 4 -b 0.0.0.0:8123 app:app + ``` + +## 防火墙设置 + +请确保服务器防火墙开放了8123端口: + +``` +# 对于使用ufw的系统(如Ubuntu) +sudo ufw allow 8123/tcp + +# 对于使用firewalld的系统(如CentOS) +sudo firewall-cmd --permanent --add-port=8123/tcp +sudo firewall-cmd --reload +``` + +## 访问应用 + +部署完成后,可以通过以下URL访问应用: + +``` +http://服务器IP:8123 +``` + +## 可选:使用screen运行 + +如果SSH连接可能不稳定,可以使用screen在后台会话中运行: + +### 使用虚拟环境的screen会话(推荐) + +``` +screen -S chanlun-web +cd /path/to/user_data/Chan/web +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +python app.py +``` + +然后按下`Ctrl+A`然后按`D`来分离会话。要重新连接会话: + +``` +screen -r chanlun-web +``` + +## 故障排除 + +1. 如果服务不能启动,检查日志: + ``` + cat logs/error.log + ``` + +2. 确保安装了所有依赖: + ``` + # 在虚拟环境中 + source venv/bin/activate + pip install -r requirements.txt + ``` + +3. 检查端口是否被占用: + ``` + netstat -tulpn | grep 8123 + ``` + +4. 如果gunicorn无法找到,确保使用完整路径: + ``` + # 对于虚拟环境 + ./venv/bin/gunicorn -w 4 -b 0.0.0.0:8123 app:app + ``` + +5. 如果出现"externally-managed-environment"错误,这表明您正在使用最新的Ubuntu系统,必须使用虚拟环境方法。 \ No newline at end of file diff --git a/web/chanlun-web-venv.service b/web/chanlun-web-venv.service new file mode 100644 index 0000000..3e8a427 --- /dev/null +++ b/web/chanlun-web-venv.service @@ -0,0 +1,15 @@ +[Unit] +Description=ChanLun Web Application +After=network.target + +[Service] +User=<用户名> +Group=<用户组> +WorkingDirectory=/path/to/user_data/Chan/web +ExecStart=/path/to/user_data/Chan/web/venv/bin/gunicorn -w 4 -b 0.0.0.0:8123 app:app +Restart=on-failure +StandardOutput=file:/path/to/user_data/Chan/web/logs/output.log +StandardError=file:/path/to/user_data/Chan/web/logs/error.log + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/web/chanlun-web.service b/web/chanlun-web.service new file mode 100644 index 0000000..4e39e8e --- /dev/null +++ b/web/chanlun-web.service @@ -0,0 +1,15 @@ +[Unit] +Description=ChanLun Web Application +After=network.target + +[Service] +User=<用户名> +Group=<用户组> +WorkingDirectory=/path/to/user_data/Chan/web +ExecStart=/usr/local/bin/gunicorn -w 4 -b 0.0.0.0:8123 app:app +Restart=on-failure +StandardOutput=file:/path/to/user_data/Chan/web/logs/output.log +StandardError=file:/path/to/user_data/Chan/web/logs/error.log + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/web/deploy.sh b/web/deploy.sh new file mode 100755 index 0000000..ac82536 --- /dev/null +++ b/web/deploy.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# 创建日志目录(如果不存在) +mkdir -p logs + +# 激活虚拟环境(如果使用) +# source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt + +# 使用gunicorn启动应用 +# 参数说明: +# -w 4: 使用4个工作进程 +# -b 0.0.0.0:8123: 绑定到所有接口的8123端口 +# --access-logfile: 访问日志文件路径 +# --error-logfile: 错误日志文件路径 +# --daemon: 以守护进程(后台)方式运行 +# app:app: app.py中的app变量 + +gunicorn -w 4 -b 0.0.0.0:8123 --access-logfile logs/access.log --error-logfile logs/error.log --daemon app:app + +echo "服务已启动在 http://服务器IP:8123" +echo "查看进程状态: ps aux | grep gunicorn" +echo "停止服务: pkill gunicorn" \ No newline at end of file diff --git a/web/deploy_venv.sh b/web/deploy_venv.sh new file mode 100755 index 0000000..5118eb8 --- /dev/null +++ b/web/deploy_venv.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# 确保安装了虚拟环境工具 +echo "检查并安装虚拟环境工具..." +sudo apt update +sudo apt install -y python3-venv python3-full + +# 创建日志目录(如果不存在) +mkdir -p logs + +# 创建虚拟环境(如果不存在) +if [ ! -d "venv" ]; then + echo "创建虚拟环境..." + python3 -m venv venv +fi + +# 激活虚拟环境 +echo "激活虚拟环境..." +source venv/bin/activate + +# 安装依赖 +echo "安装依赖..." +pip install --upgrade pip +pip install -r requirements.txt + +# 使用gunicorn启动应用 +echo "启动应用..." +venv/bin/gunicorn -w 4 -b 0.0.0.0:8123 --access-logfile logs/access.log --error-logfile logs/error.log --daemon app:app + +echo "服务已启动在 http://服务器IP:8123" +echo "查看进程状态: ps aux | grep gunicorn" +echo "停止服务: ./stop_venv.sh" \ No newline at end of file diff --git a/web/status.sh b/web/status.sh new file mode 100755 index 0000000..768342e --- /dev/null +++ b/web/status.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# 检查gunicorn进程 +echo "Gunicorn进程状态:" +ps aux | grep gunicorn | grep -v grep + +# 检查端口 +echo -e "\n端口8123状态:" +netstat -tulpn 2>/dev/null | grep 8123 || echo "端口8123未被占用" + +# 检查日志文件最后几行 +echo -e "\n访问日志(最后5行):" +tail -n 5 logs/access.log 2>/dev/null || echo "访问日志尚未创建" + +echo -e "\n错误日志(最后5行):" +tail -n 5 logs/error.log 2>/dev/null || echo "错误日志尚未创建" \ No newline at end of file diff --git a/web/status_venv.sh b/web/status_venv.sh new file mode 100755 index 0000000..cf1eabf --- /dev/null +++ b/web/status_venv.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# 检查虚拟环境中的gunicorn进程 +echo "Gunicorn进程状态:" +ps aux | grep "venv/bin/gunicorn" | grep -v grep + +# 检查端口 +echo -e "\n端口8123状态:" +netstat -tulpn 2>/dev/null | grep 8123 || echo "端口8123未被占用" + +# 检查日志文件最后几行 +echo -e "\n访问日志(最后5行):" +tail -n 5 logs/access.log 2>/dev/null || echo "访问日志尚未创建" + +echo -e "\n错误日志(最后5行):" +tail -n 5 logs/error.log 2>/dev/null || echo "错误日志尚未创建" \ No newline at end of file diff --git a/web/stop.sh b/web/stop.sh new file mode 100755 index 0000000..ccac724 --- /dev/null +++ b/web/stop.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# 停止所有gunicorn进程 +pkill gunicorn + +echo "已停止所有gunicorn进程" \ No newline at end of file diff --git a/web/stop_venv.sh b/web/stop_venv.sh new file mode 100755 index 0000000..70c218f --- /dev/null +++ b/web/stop_venv.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# 找到并终止gunicorn进程 +echo "停止gunicorn进程..." +pkill -f "venv/bin/gunicorn" + +echo "已停止所有gunicorn进程" \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html index 577a972..f0245e3 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -224,6 +224,22 @@ border-radius: 4px; overflow: hidden; } + + .crosshair-tooltip { + position: absolute; + background: rgba(40, 40, 40, 0.9); + color: white; + padding: 8px 12px; + border-radius: 4px; + font-size: 12px; + z-index: 1000; + pointer-events: none; + max-width: 300px; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + display: none; + } + + /* 主次周期元素样式区分 */
@@ -729,9 +745,16 @@ } // 从日期字符串获取时间戳,应用时区偏移 - function getAdjustedTimestamp(dateStr) { - // 基础时间戳(秒)- 直接使用UTC时间 - return Math.floor(new Date(dateStr).getTime() / 1000); + function getAdjustedTimestamp(dateStr, applyOffset = true) { + const date = new Date(dateStr); + const timestamp = Math.floor(date.getTime() / 1000); + + if (!applyOffset) { + return timestamp; + } + + // 不再手动调整时区偏移,使用JavaScript的内置时区支持 + return timestamp; } // 添加时区选择器变更事件 @@ -1029,6 +1052,10 @@ const timezone = $('#timezone').val(); const date = new Date(timeStr); + // 添加调试信息 + console.debug('表格时间格式化:', timeStr, '->', + date.toISOString(), '使用时区:', timezone); + // 使用toLocaleString带时区参数 return date.toLocaleString('zh-CN', { timeZone: timezone, @@ -1041,13 +1068,8 @@ }); } catch (e) { console.error('时间格式化错误:', e, timeStr); - // 如果出错,尝试使用简单格式化 - try { - return new Date(timeStr).toLocaleString(); - } catch (e2) { - // 如果还是出错,返回原始字符串 - return timeStr; - } + // 如果格式化失败,返回原始时间字符串 + return timeStr; } } @@ -1077,6 +1099,9 @@ const timezone = $('#timezone').val() || 'Asia/Shanghai'; const elementTimeframe = $('#elementTimeframe').val() || '1m'; + // 确保时区参数有效 + console.log('更新图表使用时区:', timezone); + // 如果symbol为空,不发送请求 if (!symbol) { console.error('交易对不能为空'); @@ -1084,7 +1109,7 @@ return; } - console.log(`更新图表: symbol=${symbol}, timeframe=${timeframe}, elementTimeframe=${elementTimeframe}`); + console.log(`更新图表: symbol=${symbol}, timeframe=${timeframe}, elementTimeframe=${elementTimeframe}, timezone=${timezone}`); // 获取开始和结束时间(如果已设置) let startTimeMs = null; @@ -1117,6 +1142,9 @@ // 保存当前数据 currentData = data; + // 检查和记录服务器返回的时区 + console.log('服务器返回的时区:', data.timezone || '未指定'); + // 刷新图表 refreshChart(data); }, @@ -1238,10 +1266,55 @@ }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal, + // 添加十字线工具提示本地化配置 + horzLine: { + labelVisible: true, + }, + vertLine: { + labelVisible: true, + // 自定义时间格式化 + labelFormatter: (time) => { + const selectedTimezone = $('#timezone').val(); + try { + return new Date(time * 1000).toLocaleString('zh-CN', { + timeZone: selectedTimezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + } catch (e) { + console.error('十字线时间格式化错误:', e); + return new Date(time * 1000).toLocaleString(); + } + }, + }, }, rightPriceScale: { borderColor: '#ddd', }, + // 添加本地化选项,确保所有时间显示都使用选定的时区 + localization: { + timeFormatter: (time) => { + const selectedTimezone = $('#timezone').val(); + try { + return new Date(time * 1000).toLocaleString('zh-CN', { + timeZone: selectedTimezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + } catch (e) { + console.error('全局时间格式化错误:', e); + return new Date(time * 1000).toLocaleString(); + } + } + }, timeScale: { timeVisible: true, secondsVisible: false, @@ -1251,7 +1324,11 @@ tickMarkFormatter: (time) => { const selectedTimezone = $('#timezone').val(); try { - return new Date(time * 1000).toLocaleString('zh-CN', { + // 使用完整的配置确保时区正确应用 + const date = new Date(time * 1000); + console.log('格式化时间:', time, '转换为:', date.toISOString(), '时区:', selectedTimezone); + + return date.toLocaleString('zh-CN', { timeZone: selectedTimezone, month: 'numeric', day: 'numeric', @@ -1287,12 +1364,16 @@ // 输出K线周期选择状态 console.log('K线周期选择:', useElementPeriod ? '小周期' : '主周期'); + console.log('当前选择时区:', $('#timezone').val()); if (useElementPeriod) { // 使用小周期K线数据 candles = currentData.element_kline_data.map((kline) => { + // 使用原始日期字符串创建Date对象 const date = new Date(kline.date); + // 获取时间戳(秒)- 不手动调整时区 const timestamp = date.getTime() / 1000; + return { time: timestamp, open: parseFloat(kline.open), @@ -1304,8 +1385,11 @@ } else { // 使用主周期K线数据 candles = currentData.kline_data.map((kline) => { + // 使用原始日期字符串创建Date对象 const date = new Date(kline.date); + // 获取时间戳(秒)- 不手动调整时区 const timestamp = date.getTime() / 1000; + return { time: timestamp, open: parseFloat(kline.open), @@ -1350,6 +1434,7 @@ // 转换成交量数据 - 始终使用主K线周期数据 const volumes = useElementPeriod ? currentData.element_kline_data.map(kline => { + // 创建日期对象并获取时间戳,不手动调整时区 const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); return { time: timestamp, @@ -1358,6 +1443,7 @@ }; }) : currentData.kline_data.map(kline => { + // 创建日期对象并获取时间戳,不手动调整时区 const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); return { time: timestamp, @@ -2942,17 +3028,65 @@ } function setupTooltip() { + // 调试变量 + window.debugMode = true; + // 添加买卖点悬浮提示元素 const tooltipElement = document.createElement('div'); tooltipElement.className = 'point-tooltip'; document.body.appendChild(tooltipElement); + // 添加自定义十字线信息显示 + const crosshairTooltip = document.createElement('div'); + crosshairTooltip.className = 'crosshair-tooltip'; + crosshairTooltip.style.position = 'absolute'; + crosshairTooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; + crosshairTooltip.style.color = 'white'; + crosshairTooltip.style.padding = '5px 10px'; + crosshairTooltip.style.borderRadius = '4px'; + crosshairTooltip.style.fontSize = '12px'; + crosshairTooltip.style.zIndex = '1000'; + crosshairTooltip.style.pointerEvents = 'none'; + crosshairTooltip.style.display = 'none'; + document.body.appendChild(crosshairTooltip); + // 添加鼠标悬停事件显示提示 mainChart.subscribeCrosshairMove(param => { if (param.time && param.point) { const timeStr = param.time; const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr); + // 显示时区调试信息 + if (window.debugMode) { + const timezone = $('#timezone').val(); + const formattedTime = formatTimeWithTimezone(timeStr * 1000, timezone); + + // 获取当前价格 - 通过param.seriesPrices获取 + let priceInfo = ''; + if (param.seriesPrices && param.seriesPrices.size > 0) { + // 尝试从蜡烛图系列获取价格 + if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) { + const price = param.seriesPrices.get(tvWidget.series.candleSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } + // 如果没有蜡烛图系列价格,尝试从线图系列获取 + else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) { + const price = param.seriesPrices.get(tvWidget.series.lineSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } + } + + // 仅记录最简短的调试信息 + console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`); + + // 显示自定义时区工具提示,包含价格信息 + crosshairTooltip.innerHTML = `