本文最后更新于 2025-11-13,文章内容可能已经过时。

来源:https://github.com/huojichuanqi/ds/tree/main

新容器在 /root/btc_deepseek/plus 目录下运行,需要确保以下文件和目录结构是完整的。

以下是 /root/btc_deepseek/plus 目录下所有必须的文件内容:

📁 目录结构

/root/btc_deepseek/plus/
├── Dockerfile              # Docker 镜像构建文件
├── requirements.txt        # Python 依赖清单
├── .env                    # 环境变量和 API Key (敏感信息)
├── bot_web_app.py          # 主 Python 应用 (已修复和增强)
├── templates/
│   └── index.html          # Web UI 界面 (HTML)
└── static/
    └── app.js              # Web UI 动态脚本 (JavaScript)

1. 📄 Dockerfile

(用于构建镜像的指令)

Dockerfile

# 使用官方 Python 基础镜像
FROM python:3.11-slim

# 设置工作目录
WORKDIR /app

# 复制依赖文件并安装依赖 (先安装依赖可以利用Docker缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制所有应用代码和前端文件
COPY . /app

# 暴露 Flask 应用的端口 (容器内部端口)
EXPOSE 5000

# 定义容器启动时执行的命令
CMD ["python", "bot_web_app.py"]

2. 📄 requirements.txt

(Python 依赖库列表)

Plaintext

flask
apscheduler
ccxt
openai
pandas
python-dotenv
requests

3. 📄 .env

(你的 API 密钥,请确保复制正确的密钥和密码)

Plaintext

DEEPSEEK_API_KEY=111111111111111111111111
# ... (其他不需要的密钥可以删除或留空)

OKX_API_KEY=1111111111111111111
OKX_SECRET=1111111111111111111111
OKX_PASSWORD=11111111111111111111111

4. 🐍 bot_web_app.py

import os
import time
import re
import json
import requests
import pandas as pd
import ccxt
from openai import OpenAI
from dotenv import load_dotenv
from datetime import datetime, timedelta
import traceback

# 核心导入
from flask import Flask, jsonify, request, render_template
from apscheduler.schedulers.background import BackgroundScheduler
import atexit

load_dotenv()

# --- 全局变量和初始化 ---

app = Flask(__name__, static_folder='static')
scheduler = BackgroundScheduler()
atexit.register(lambda: scheduler.shutdown())

# 核心状态存储 (新增日志和历史数据)
TRADE_LOGS = []
ORDER_HISTORY = [
    {'id': '12345', 'symbol': 'BTC/USDT', 'side': 'buy', 'amount': 0.01, 'price': 60000.0, 'profit': 15.5, 'status': 'closed', 'timestamp': (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')},
    {'id': '12346', 'symbol': 'BTC/USDT', 'side': 'sell', 'amount': 0.01, 'price': 62000.0, 'profit': -5.1, 'status': 'closed', 'timestamp': (datetime.now() - timedelta(hours=12)).strftime('%Y-%m-%d %H:%M:%S')}
]
ACCOUNT_BALANCE = {
    'equity': 0,
    'unrealized_pnl': 0,
    'total_pnl': 0,
    'usdt_balance': 0
}

BOT_STATUS = {
    'running': False,
    'last_run': 'N/A',
    'status_message': '初始化中...',
    'current_position': None,
    'last_signal': None,
    'config_ok': False
}

# 初始化DeepSeek客户端/OKX交易所
deepseek_client = OpenAI(
    api_key=os.getenv('DEEPSEEK_API_KEY'),
    base_url="https://api.deepseek.com"
)

exchange = ccxt.okx({
    'options': {
        'defaultType': 'swap',
    },
    'apiKey': os.getenv('OKX_API_KEY'),
    'secret': os.getenv('OKX_SECRET'),
    'password': os.getenv('OKX_PASSWORD'),
})

TRADE_CONFIG = {
    'symbol': 'BTC/USDT:USDT', 'leverage': 10, 'timeframe': '15m', 'test_mode': False,
    'data_points': 96, 'contract_size': 0.01, 'min_amount': 0.01,
    'analysis_periods': {'short_term': 20, 'medium_term': 50, 'long_term': 96},
    'position_management': {'enable_intelligent_position': True, 'base_usdt_amount': 100, 'high_confidence_multiplier': 1.5,
                            'medium_confidence_multiplier': 1.0, 'low_confidence_multiplier': 0.5, 'max_position_ratio': 10,
                            'trend_strength_multiplier': 1.2}
}


# --- 辅助函数 (已填充逻辑) ---

def log_trade_step(title, message, log_type='info'):
    """记录交易步骤日志,用于UI展示"""
    global TRADE_LOGS
    log_entry = {
        'time': datetime.now().strftime('%H:%M:%S'),
        'title': title,
        'message': message,
        'type': log_type
    }
    TRADE_LOGS.append(log_entry)
    if len(TRADE_LOGS) > 50:
        TRADE_LOGS.pop(0)

def fetch_account_summary():
    """获取账户余额和总览"""
    global ACCOUNT_BALANCE
    try:
        balance = exchange.fetch_balance()
        usdt_balance = balance['USDT']['free']
        total_equity = balance['total'].get('USDT', 0)
        
        positions = exchange.fetch_positions([TRADE_CONFIG['symbol']])
        unrealized_pnl = sum(float(pos['unrealizedPnl']) for pos in positions if 'unrealizedPnl' in pos)

        ACCOUNT_BALANCE.update({
            'equity': total_equity,
            'unrealized_pnl': unrealized_pnl,
            'total_pnl': sum(float(order['profit']) for order in ORDER_HISTORY),
            'usdt_balance': usdt_balance
        })

    except Exception as e:
        log_trade_step("❌ 账户信息获取失败", f"错误: {e}", log_type='error')

def setup_exchange():
    """设置交易所参数 - 强制全仓模式"""
    try:
        print("🔍 获取BTC合约规格...")
        markets = exchange.load_markets()
        btc_market = markets[TRADE_CONFIG['symbol']]
        TRADE_CONFIG['contract_size'] = float(btc_market['contractSize'])
        TRADE_CONFIG['min_amount'] = btc_market['limits']['amount']['min']
        
        # 检查并设置持仓模式和杠杆...
        exchange.set_position_mode(False, TRADE_CONFIG['symbol'])
        exchange.set_leverage(TRADE_CONFIG['leverage'], TRADE_CONFIG['symbol'], {'mgnMode': 'cross'})
        print(f"✅ 已设置全仓模式,杠杆倍数: {TRADE_CONFIG['leverage']}x")
        
        return True
    except Exception as e:
        print(f"❌ 交易所设置失败: {e}")
        BOT_STATUS['status_message'] = f"交易所设置失败: {e}"
        return False

def calculate_technical_indicators(df):
    """计算技术指标"""
    try:
        df['sma_5'] = df['close'].rolling(window=5, min_periods=1).mean()
        df['sma_20'] = df['close'].rolling(window=20, min_periods=1).mean()
        df['sma_50'] = df['close'].rolling(window=50, min_periods=1).mean()
        delta = df['close'].diff()
        gain = (delta.where(delta > 0, 0)).rolling(14).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
        rs = gain / loss
        df['rsi'] = 100 - (100 / (1 + rs))
        df = df.bfill().ffill()
        return df
    except Exception as e:
        print(f"技术指标计算失败: {e}")
        return df

def get_market_trend(df):
    """判断市场趋势"""
    current_price = df['close'].iloc[-1]
    trend_short = "上涨" if current_price > df['sma_20'].iloc[-1] else "下跌"
    trend_medium = "上涨" if current_price > df['sma_50'].iloc[-1] else "下跌"
    overall_trend = "强势上涨" if trend_short == "上涨" and trend_medium == "上涨" else \
                    "强势下跌" if trend_short == "下跌" and trend_medium == "下跌" else "震荡整理"
    return {'overall': overall_trend}

def get_btc_ohlcv_enhanced():
    """增强版:获取BTC K线数据并计算技术指标"""
    try:
        ohlcv = exchange.fetch_ohlcv(TRADE_CONFIG['symbol'], TRADE_CONFIG['timeframe'], limit=TRADE_CONFIG['data_points'])
        df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
        df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
        df = calculate_technical_indicators(df)
        
        current_data = df.iloc[-1]
        previous_data = df.iloc[-2]

        return {
            'price': current_data['close'],
            'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'price_change': ((current_data['close'] - previous_data['close']) / previous_data['close']) * 100,
            'trend_analysis': get_market_trend(df),
            'full_data': df
        }
    except Exception as e:
        print(f"获取增强K线数据失败: {e}")
        return None

def generate_technical_analysis_text(price_data):
    """生成技术分析文本(简化)"""
    trend = price_data.get('trend_analysis', {}).get('overall', 'N/A')
    return f"当前价格: ${price_data['price']:,.2f}。整体趋势分析为: {trend}"

def get_current_position():
    """获取当前持仓情况 - OKX版本"""
    try:
        positions = exchange.fetch_positions([TRADE_CONFIG['symbol']])
        for pos in positions:
            if pos['symbol'] == TRADE_CONFIG['symbol'] and float(pos['contracts']) > 0:
                return {
                    'side': pos['side'],
                    'size': float(pos['contracts']),
                    'entry_price': float(pos['entryPrice']),
                    'unrealized_pnl': float(pos['unrealizedPnl']),
                    'leverage': float(pos['leverage']),
                    'symbol': pos['symbol']
                }
        return None
    except Exception:
        return None

# --- 位于 bot_web_app.py 中间的辅助函数区域 ---

def safe_json_parse(json_str):
    """安全解析JSON,处理DeepSeek的Markdown代码块标记和格式不规范的情况"""
    
    # 1. 尝试移除 Markdown 代码块标记(修复 DeepSeek 返回带 ```json 的问题)
    # 检查是否以 ``` 开头和结尾
    if json_str.strip().startswith("```") and json_str.strip().endswith("```"):
        # 移除开头的 '```json' 或 '```',并移除结尾的 '```'
        json_str = json_str.strip().lstrip('```json').lstrip('```').rstrip('```').strip()
    
    # 2. 尝试标准解析
    try:
        return json.loads(json_str)
    except json.JSONDecodeError:
        # Fallback 尝试,处理其他不规范的格式(例如单引号)
        try:
            json_str = json_str.replace("'", '"')
            # 修正没有引号的键名(如果存在)
            json_str = re.sub(r'(\w+):', r'"\1":', json_str)
            # 修正末尾多余的逗号
            json_str = re.sub(r',\s*}', '}', json_str)
            json_str = re.sub(r',\s*]', ']', json_str)
            return json.loads(json_str)
        except json.JSONDecodeError as e:
            print(f"JSON解析最终失败,原始内容: {json_str}")
            return None

def create_fallback_signal(price_data):
    """创建备用交易信号"""
    return {
        "signal": "HOLD", "reason": "AI分析失败,采取保守策略", "confidence": "LOW", "is_fallback": True
    }

def analyze_with_deepseek(price_data):
    """使用DeepSeek分析市场并生成交易信号(已添加日志记录 Prompt 和 响应)"""
    try:
        technical_analysis = generate_technical_analysis_text(price_data)
        
        # 构建发送给DeepSeek的Prompt
        prompt = f"""
        你是一位专业的加密货币量化交易员。请根据提供的市场信息和技术分析,以最高的准确率生成一个交易信号。
        当前K线周期: {TRADE_CONFIG['timeframe']}。
        市场信息: {technical_analysis}。
        请严格以 JSON 格式返回结果,包含 'signal' (BUY/SELL/HOLD), 'reason' (中文解释), 'confidence' (HIGH/MEDIUM/LOW)。
        """
        
        # 🆕 日志记录:发送的Prompt内容
        log_trade_step(
            "🧠 步骤 2.1: DeepSeek Prompt (已发送)", 
            prompt.strip().replace('\n', ' '), 
            log_type='info'
        )
        
        response = deepseek_client.chat.completions.create(
            model="deepseek-chat",
            messages=[{"role": "system", "content": "你是一位专业的交易员,请严格按照用户要求的JSON格式输出交易信号。"},
                      {"role": "user", "content": prompt}],
            stream=False,
            temperature=0.1
        )
        result = response.choices[0].message.content
        
        # 🆕 日志记录:DeepSeek返回的原始JSON响应
        log_trade_step(
            "🧠 步骤 2.2: DeepSeek 原始响应 (已接收)", 
            result, 
            log_type='info'
        )

        signal_data = safe_json_parse(result)
        
        if signal_data is None:
             signal_data = create_fallback_signal(price_data)

        signal_data['timestamp'] = price_data['timestamp']
        return signal_data

    except Exception:
        # 如果API调用或处理失败,记录错误堆栈
        log_trade_step("❌ 步骤 2.2: DeepSeek API 失败", traceback.format_exc(), log_type='error')
        return create_fallback_signal(price_data)
    
def analyze_with_deepseek_with_retry(price_data, max_retries=2):
    """带重试的DeepSeek分析"""
    for attempt in range(max_retries):
        signal_data = analyze_with_deepseek(price_data)
        if signal_data and not signal_data.get('is_fallback', False):
            return signal_data
        time.sleep(1)
    return create_fallback_signal(price_data)

def calculate_intelligent_position(signal_data, price_data, current_position):
    """计算智能仓位大小(简化)"""
    config = TRADE_CONFIG['position_management']
    base_contracts = config['base_usdt_amount'] / (price_data['price'] * TRADE_CONFIG['contract_size'])
    
    confidence_multiplier = {'HIGH': 1.5, 'MEDIUM': 1.0, 'LOW': 0.5}.get(signal_data['confidence'], 1.0)
    
    final_contracts = round(base_contracts * confidence_multiplier, 2)
    return max(final_contracts, TRADE_CONFIG['min_amount'])

def execute_intelligent_trade(signal_data, price_data):
    """执行智能交易(简化)"""
    current_position = get_current_position()
    position_size = calculate_intelligent_position(signal_data, price_data, current_position)

    if signal_data['confidence'] == 'LOW' and not TRADE_CONFIG['test_mode']:
        return log_trade_step("⚠️ 步骤 3: 跳过", "低信心信号,跳过执行。", log_type='info')

    if TRADE_CONFIG['test_mode']:
        return log_trade_step("📝 步骤 3: 模拟", f"测试模式,模拟信号: {signal_data['signal']},仓位: {position_size}", log_type='info')

    try:
        side = 'buy' if signal_data['signal'] == 'BUY' else 'sell'
        
        if signal_data['signal'] == 'HOLD':
            return
            
        # 1. 尝试平仓
        if current_position:
             # 如果信号与持仓反向,先平仓
            if (side == 'buy' and current_position['side'] == 'short') or \
               (side == 'sell' and current_position['side'] == 'long'):
                reduce_side = 'sell' if current_position['side'] == 'long' else 'buy'
                exchange.create_market_order(TRADE_CONFIG['symbol'], reduce_side, current_position['size'], params={'reduceOnly': True})
                log_trade_step("🔄 步骤 3: 平仓", f"已平仓: {current_position['side']} {current_position['size']}", log_type='info')
                time.sleep(0.5)

        # 2. 开仓 (如果不是HOLD)
        if current_position is None or (side == 'buy' and current_position['side'] == 'short') or (side == 'sell' and current_position['side'] == 'long'):
            exchange.create_market_order(TRADE_CONFIG['symbol'], side, position_size)
            log_trade_step("🆕 步骤 3: 开仓", f"成功开仓: {side} {position_size}", log_type='success')

    except Exception as e:
        log_trade_step("❌ 交易执行失败", f"错误: {e}", log_type='error')


# --- 核心调度函数 ---

def trading_bot():
    """主交易机器人函数,由 APScheduler 调用"""
    global BOT_STATUS
    log_trade_step("🤖 任务开始", f"交易周期 {TRADE_CONFIG['timeframe']} 开始执行。")

    if not BOT_STATUS['config_ok']:
        log_trade_step("⚠️ 机器人未通过初始化检查,跳过本次运行。", log_type='error')
        return

    # 1. 获取增强版K线数据
    log_trade_step("📊 步骤 1: 获取行情数据", "正在从 OKX 获取 K线数据...")
    price_data = get_btc_ohlcv_enhanced()
    if not price_data:
        log_trade_step("❌ 步骤 1: 失败", "K线数据获取失败,跳过本次运行。", log_type='error')
        return
    log_trade_step("✅ 步骤 1: 成功", f"当前价格: ${price_data['price']:,.2f},数据就绪。")

    # 2. 使用DeepSeek分析(带重试)
    log_trade_step("🧠 步骤 2: AI分析", "正在调用 DeepSeek 进行智能分析...")
    signal_data = analyze_with_deepseek_with_retry(price_data)

    if signal_data.get('is_fallback', False):
         log_trade_step("⚠️ 步骤 2: 完成", f"AI分析失败,使用备用信号: {signal_data['signal']}", log_type='error')
    else:
         log_trade_step("✅ 步骤 2: 完成", f"AI信号: {signal_data['signal']} (置信度: {signal_data['confidence']})", log_type='success')

    # 3. 执行智能交易
    log_trade_step("💸 步骤 3: 交易执行", f"执行信号: {signal_data['signal']}...")
    execute_intelligent_trade(signal_data, price_data)
    log_trade_step("✅ 步骤 3: 交易完成", "已执行或跳过交易操作。", log_type='success')

    # 实时更新账户信息
    fetch_account_summary()
    
    # 状态更新
    current_pos = get_current_position()
    BOT_STATUS['last_run'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    BOT_STATUS['status_message'] = f"上次执行成功,信号: {signal_data['signal']},持仓: {current_pos['side'] if current_pos else '无'}。"
    BOT_STATUS['current_position'] = current_pos
    BOT_STATUS['last_signal'] = signal_data
    log_trade_step("🏁 任务结束", "交易任务周期结束,等待下一次运行。", log_type='info')


def init_bot():
    """机器人初始化和设置"""
    print("BTC/USDT OKX自动交易机器人启动成功!")
    if not setup_exchange():
        print("交易所初始化失败,程序退出")
        return False
    BOT_STATUS['status_message'] = "交易所设置成功,Web服务已就绪。请通过Web界面启动任务。"
    BOT_STATUS['config_ok'] = True
    fetch_account_summary()
    return True


# --- Flask Web 路由 ---

@app.route('/')
def index():
    """首页 - 渲染 Web UI"""
    return render_template('index.html')


@app.route('/api/data', methods=['GET'])
def get_dashboard_data():
    """获取动态数据 (状态、日志、持仓)"""
    fetch_account_summary()
    BOT_STATUS['current_position'] = get_current_position()
    
    return jsonify({
        'status': 'RUNNING' if BOT_STATUS['running'] else 'STOPPED',
        'details': BOT_STATUS,
        'logs': TRADE_LOGS,
        'balance': ACCOUNT_BALANCE,
        'config': {
            'symbol': TRADE_CONFIG['symbol'],
            'timeframe': TRADE_CONFIG['timeframe'],
            'leverage': TRADE_CONFIG['leverage'],
            'test_mode': TRADE_CONFIG['test_mode']
        }
    })

@app.route('/api/orders', methods=['GET'])
def get_historical_orders():
    """获取历史订单数据"""
    return jsonify(sorted(ORDER_HISTORY, key=lambda x: x['timestamp'], reverse=True))


@app.route('/control/<action>', methods=['POST'])
def control_bot(action):
    """控制交易任务的启动、暂停、停止、重启 (已修复 SyntaxError)"""
    global scheduler # <-- 修复点:确保 global 声明在函数体最前面

    if not BOT_STATUS['config_ok'] and action != 'restart':
        return jsonify({'message': '交易所配置失败,无法启动任务。', 'status': 'ERROR'}), 400

    try:
        if action == 'start':
            if not BOT_STATUS['running']:
                scheduler.add_job(func=trading_bot, trigger='interval', minutes=15, id='trade_job', next_run_time=datetime.now())
                scheduler.start()
                BOT_STATUS['running'] = True
                BOT_STATUS['status_message'] = '交易任务已启动,等待第一次执行...'
                log_trade_step("🟢 控制", "通过 Web API 启动任务。", log_type='success')
            return jsonify({'message': '交易任务已启动', 'status': 'OK'})
        
        elif action == 'stop' or action == 'pause':
            if BOT_STATUS['running']:
                scheduler.shutdown(wait=False) 
                BOT_STATUS['running'] = False
                BOT_STATUS['status_message'] = '交易任务已停止/暂停。'
                log_trade_step("🛑 控制", "通过 Web API 停止/暂停任务。", log_type='info')
            return jsonify({'message': '交易任务已停止/暂停', 'status': 'OK'})
        
        elif action == 'restart':
            if BOT_STATUS['running']:
                scheduler.shutdown(wait=False)
                BOT_STATUS['running'] = False
                log_trade_step("🔃 控制", "正在停止旧任务...", log_type='info')
            
            # 重新初始化调度器
            scheduler = BackgroundScheduler()
            atexit.register(lambda: scheduler.shutdown())

            # 启动
            scheduler.add_job(func=trading_bot, trigger='interval', minutes=15, id='trade_job', next_run_time=datetime.now())
            scheduler.start()
            BOT_STATUS['running'] = True
            BOT_STATUS['status_message'] = '交易任务已重启并启动,等待第一次执行...'
            log_trade_step("🟢 控制", "任务已成功重启并启动。", log_type='success')
            return jsonify({'message': '交易任务已重启', 'status': 'OK'})

        else:
            return jsonify({'message': f'未知操作: {action}'}), 400

    except Exception as e:
        log_trade_step("❌ 控制失败", f"控制操作失败: {e}", log_type='error')
        traceback.print_exc()
        return jsonify({'message': f'控制操作失败: {e}'}), 500


if __name__ == '__main__':
    if init_bot():
        print("\n🌐 Flask Web Server 正在启动...")
        app.run(host='0.0.0.0', port=5000) 
    else:
        print("Web应用启动失败,请解决交易所配置问题。")

5. 🌐 templates/index.html

(前端界面 HTML 文件)

HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DeepSeek 量化交易机器人仪表板</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 20px; background-color: #f4f6f8; color: #333; }
        .container { max-width: 1200px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
        h1 { color: #007bff; border-bottom: 2px solid #e9ecef; padding-bottom: 10px; margin-bottom: 20px; }
        .control-panel { display: flex; gap: 10px; margin-bottom: 20px; padding: 15px; background: #e9f7ff; border-radius: 6px; }
        .control-panel button { padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; transition: background-color 0.3s; }
        #start-btn { background-color: #28a745; color: white; }
        #stop-btn { background-color: #dc3545; color: white; }
        #restart-btn { background-color: #ffc107; color: #333; }
        .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
        .card { padding: 15px; border: 1px solid #dee2e6; border-radius: 6px; background: #fff; }
        .status-badge { padding: 5px 10px; border-radius: 12px; font-weight: bold; display: inline-block; margin-left: 10px; }
        .status-badge.RUNNING { background-color: #d4edda; color: #155724; }
        .status-badge.STOPPED { background-color: #f8d7da; color: #721c24; }
        .status-badge.ERROR { background-color: #fff3cd; color: #856404; }
        .log-list { max-height: 400px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px; border-radius: 4px; background: #fcfcfc; }
        .log-item { margin-bottom: 5px; padding: 5px; border-bottom: 1px dotted #eee; font-size: 0.9em; }
        .log-item span { display: block; }
        .log-item .time { color: #6c757d; font-weight: bold; }
        .log-item.error { background-color: #f8d7da; }
        .log-item.success { background-color: #d4edda; }
        .log-item.warning { background-color: #fff3cd; }
    </style>
</head>
<body>
    <div class="container">
        <h1>DeepSeek BTC 交易机器人 (Test:8088)</h1>

        <div class="control-panel">
            <button id="start-btn">启动 Start</button>
            <button id="stop-btn">暂停 Stop/Pause</button>
            <button id="restart-btn">重启 Restart</button>
            <p>任务状态: <span id="bot-status" class="status-badge">加载中...</span></p>
        </div>

        <div class="grid">
            <div class="card">
                <h2>📊 账户概览</h2>
                <p><strong>总资产净值:</strong> <span id="equity">$...</span> USDT</p>
                <p><strong>可用余额:</strong> <span id="usdt-balance">$...</span> USDT</p>
                <p><strong>未实现盈亏 (PNL):</strong> <span id="unrealized-pnl">$...</span> USDT</p>
                <p><strong>累计已实现PNL:</strong> <span id="total-pnl">$...</span> USDT</p>
            </div>
            <div class="card">
                <h2>📈 任务与持仓</h2>
                <p><strong>交易对:</strong> <span id="symbol">...</span></p>
                <p><strong>K线周期:</strong> <span id="timeframe">...</span></p>
                <p><strong>杠杆倍数:</strong> <span id="leverage">...</span>x</p>
                <p><strong>当前持仓:</strong> <span id="position">...</span></p>
                <p><strong>上次信号:</strong> <span id="last-signal">...</span></p>
                <p><strong>上次运行时间:</strong> <span id="last-run">...</span></p>
            </div>
        </div>

        <div class="card" style="margin-top: 20px;">
            <h2>📝 交易流程实时日志</h2>
            <div id="trade-logs" class="log-list">
                <p style="text-align: center; color: #6c757d;">等待机器人初始化...</p>
            </div>
        </div>

        <div class="card" style="margin-top: 20px;">
            <h2>📜 历史订单 (示例)</h2>
            <table style="width: 100%; border-collapse: collapse; font-size: 0.9em;">
                <thead>
                    <tr style="background-color: #f1f1f1;">
                        <th style="padding: 8px; border: 1px solid #ddd;">时间</th>
                        <th style="padding: 8px; border: 1px solid #ddd;">方向</th>
                        <th style="padding: 8px; border: 1px solid #ddd;">数量</th>
                        <th style="padding: 8px; border: 1px solid #ddd;">成交价</th>
                        <th style="padding: 8px; border: 1px solid #ddd;">盈亏 (USDT)</th>
                        <th style="padding: 8px; border: 1px solid #ddd;">状态</th>
                    </tr>
                </thead>
                <tbody id="order-history">
                    </tbody>
            </table>
        </div>
    </div>

    <script src="/static/app.js"></script>
</body>
</html>

6. ☕ static/app.js

(前端 JavaScript 文件)

JavaScript

document.addEventListener('DOMContentLoaded', function() {
    const STATUS_URL = '/api/data';
    const CONTROL_URL = '/control/';

    const statusMap = {
        'RUNNING': '运行中',
        'STOPPED': '已停止',
        'ERROR': '错误',
        'PENDING': '等待中'
    };

    const logTypeMap = {
        'info': 'log-item',
        'success': 'log-item success',
        'error': 'log-item error',
        'warning': 'log-item warning'
    };

    function updateDashboard() {
        fetch(STATUS_URL)
            .then(response => response.json())
            .then(data => {
                // 1. 更新控制面板状态
                const statusElement = document.getElementById('bot-status');
                statusElement.textContent = statusMap[data.status] || data.status;
                statusElement.className = `status-badge ${data.status}`;

                // 2. 更新账户概览
                document.getElementById('equity').textContent = formatCurrency(data.balance.equity);
                document.getElementById('usdt-balance').textContent = formatCurrency(data.balance.usdt_balance);
                document.getElementById('unrealized-pnl').textContent = formatCurrency(data.balance.unrealized_pnl, true);
                document.getElementById('total-pnl').textContent = formatCurrency(data.balance.total_pnl, true);

                // 3. 更新任务与持仓信息
                document.getElementById('symbol').textContent = data.config.symbol;
                document.getElementById('timeframe').textContent = data.config.timeframe;
                document.getElementById('leverage').textContent = data.config.leverage;
                document.getElementById('last-run').textContent = data.details.last_run;

                const position = data.details.current_position;
                document.getElementById('position').textContent = position ? `${position.side.toUpperCase()} ${position.size} BTC @ ${formatCurrency(position.entry_price)}` : '无持仓';

                const lastSignal = data.details.last_signal;
                document.getElementById('last-signal').textContent = lastSignal ? `${lastSignal.signal} (${lastSignal.confidence})` : 'N/A';

                // 4. 更新日志
                const logContainer = document.getElementById('trade-logs');
                // 只在日志发生变化时更新,并保持滚动到底部
                const newLogHtml = data.logs.reverse().map(log => `
                    <div class="${logTypeMap[log.type] || 'log-item'}">
                        <span class="time">[${log.time}]</span>
                        <span class="title" style="font-weight: 500;">${log.title}:</span>
                        <span class="message">${log.message}</span>
                    </div>
                `).join('');

                if (logContainer.innerHTML !== newLogHtml) {
                    logContainer.innerHTML = newLogHtml;
                    logContainer.scrollTop = logContainer.scrollHeight; // 自动滚动到底部
                }
            })
            .catch(error => console.error('Error fetching dashboard data:', error));

        // 5. 更新历史订单 (简化:使用API返回的示例)
        fetch('/api/orders')
            .then(response => response.json())
            .then(orders => {
                const orderBody = document.getElementById('order-history');
                orderBody.innerHTML = orders.map(order => `
                    <tr>
                        <td style="padding: 8px; border: 1px solid #ddd;">${order.timestamp.split(' ')[1]}</td>
                        <td style="padding: 8px; border: 1px solid #ddd; color: ${order.side === 'buy' ? 'green' : 'red'}; font-weight: bold;">${order.side.toUpperCase()}</td>
                        <td style="padding: 8px; border: 1px solid #ddd;">${order.amount}</td>
                        <td style="padding: 8px; border: 1px solid #ddd;">${formatCurrency(order.price)}</td>
                        <td style="padding: 8px; border: 1px solid #ddd; color: ${order.profit >= 0 ? 'green' : 'red'};">${formatCurrency(order.profit, true)}</td>
                        <td style="padding: 8px; border: 1px solid #ddd;">${order.status.toUpperCase()}</td>
                    </tr>
                `).join('');
            })
            .catch(error => console.error('Error fetching order history:', error));
    }

    function formatCurrency(amount, isPnl = false) {
        if (typeof amount !== 'number') return 'N/A';
        const sign = isPnl && amount > 0 ? '+' : '';
        return `${sign}$${amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`;
    }

    function controlBot(action) {
        fetch(CONTROL_URL + action, { method: 'POST' })
            .then(response => response.json())
            .then(data => {
                alert(data.message);
                updateDashboard(); // 立即刷新状态
            })
            .catch(error => {
                alert(`控制失败: ${error}`);
            });
    }

    document.getElementById('start-btn').addEventListener('click', () => controlBot('start'));
    document.getElementById('stop-btn').addEventListener('click', () => controlBot('stop'));
    document.getElementById('restart-btn').addEventListener('click', () => controlBot('restart'));

    // 初始加载和定时刷新
    updateDashboard();
    setInterval(updateDashboard, 3000); 
});

🚀 最终部署命令

请在你的服务器终端执行以下命令,完成部署:

1. 创建目录和子目录

Bash

mkdir -p /root/btc_deepseek/plus/templates
mkdir -p /root/btc_deepseek/plus/static

2. 将上述所有文件内容粘贴到对应路径。

3. 构建新的 Docker 镜像

Bash

cd /root/btc_deepseek/plus
docker build --no-cache -t btc-web-app:v5 . 

4. 运行新的容器(原容器不变)

Bash

docker run -d --name btc_web_test_plus_runner -p 8088:5000 btc-web-app:v5

5. 访问新的测试页面

在你的浏览器中访问:http://[你的服务器IP]:8088/