This commit is contained in:
jackyu66git
2025-05-22 12:38:02 +08:00
parent fa3cb6c96b
commit a1e033432b
10 changed files with 577 additions and 12 deletions
+212
View File
@@ -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系统,必须使用虚拟环境方法。
+15
View File
@@ -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
+15
View File
@@ -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
Executable
+25
View File
@@ -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"
+32
View File
@@ -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"
Executable
+16
View File
@@ -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 "错误日志尚未创建"
+16
View File
@@ -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 "错误日志尚未创建"
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# 停止所有gunicorn进程
pkill gunicorn
echo "已停止所有gunicorn进程"
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# 找到并终止gunicorn进程
echo "停止gunicorn进程..."
pkill -f "venv/bin/gunicorn"
echo "已停止所有gunicorn进程"
+233 -12
View File
@@ -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;
}
/* 主次周期元素样式区分 */
</style>
</head>
<body>
@@ -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 = `<div style="font-weight:bold">时间: ${formattedTime}</div>` +
(priceInfo ? `<div>${priceInfo}</div>` : '');
crosshairTooltip.style.display = 'block';
crosshairTooltip.style.left = (param.point.x + 15) + 'px';
crosshairTooltip.style.top = (param.point.y - 30) + 'px';
}
if (markers.length > 0) {
// 有买卖点标记,显示自定义提示
const tooltips = markers.map(m => m.tooltip).join('<br>');
@@ -2967,15 +3101,35 @@
} else {
// 隐藏提示
tooltipElement.style.display = 'none';
crosshairTooltip.style.display = 'none';
}
});
// 处理图表缩放、平移等事件,隐藏提示
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
tooltipElement.style.display = 'none';
crosshairTooltip.style.display = 'none';
});
}
// 辅助函数:使用指定时区格式化时间戳
function formatTimeWithTimezone(timestamp, timezone) {
try {
return new Date(timestamp).toLocaleString('zh-CN', {
timeZone: timezone,
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(timestamp).toLocaleString();
}
}
function displayTradePoints() {
// 优先使用小周期数据,如果不存在则使用主周期数据
const tradePointsData = currentData.element_trade_points || currentData.trade_points;
@@ -3454,6 +3608,13 @@
// 页面加载时初始化
$(document).ready(function() {
// 从本地存储中恢复时区设置
const savedTimezone = localStorage.getItem('selectedTimezone');
if (savedTimezone) {
$('#timezone').val(savedTimezone);
console.log('从本地存储恢复时区设置:', savedTimezone);
}
// 初始化交易对下拉菜单
$('#symbol').val('BTC/USDT:USDT');
$('#timeframe').val('5m');
@@ -3485,6 +3646,66 @@
// 初始化自动刷新功能
initAutoRefresh();
// 确保在文档加载完成后初始化时区设置
// 默认设置为Shanghai时区
if (!$('#timezone').val()) {
$('#timezone').val('Asia/Shanghai');
}
// 记录当前时区设置
console.log('页面加载完成,当前时区设置:', $('#timezone').val());
// 添加自定义事件处理 - 让时区选择变更立即生效
$('#timezone').on('change', function() {
const newTimezone = $(this).val();
console.log('时区已更改为:', newTimezone);
// 保存到本地存储,下次访问时自动使用
localStorage.setItem('selectedTimezone', newTimezone);
// 如果已有数据,重新渲染图表和表格
if (currentData) {
// 先销毁现有图表实例
if (tvWidget.mainChart) {
try {
// 销毁主图表及其关联的线系列
tvWidget.mainChart = null;
tvWidget.volumeChart = null;
tvWidget.macdChart = null;
// 重置系列数据
tvWidget.series = {
candleSeries: null,
lineSeries: null,
volumeSeries: null,
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
tradePointSeries: []
};
} catch (e) {
console.error('销毁图表错误:', e);
}
}
// 使用新的时区重新初始化图表
initTradingView($('#symbol').val(), $('#timeframe').val());
// 重新渲染图表数据
renderChart();
// 更新表格
updateTables(currentData);
}
});
});
// 自动刷新相关变量