|
|
@@ -8,28 +8,89 @@ import sqlite3
|
|
|
import json
|
|
|
from datetime import datetime, timedelta
|
|
|
import os
|
|
|
+import glob
|
|
|
from typing import Dict, List, Any
|
|
|
|
|
|
class TradingDashboard:
|
|
|
- def __init__(self, db_path: str = "trading_data.db"):
|
|
|
+ def __init__(self, db_path: str = "trading_data.db", data_dir: str = None):
|
|
|
self.app = Flask(__name__)
|
|
|
self.db_path = db_path
|
|
|
+ # 设置数据目录,默认为项目根目录和data目录
|
|
|
+ self.data_dirs = []
|
|
|
+ if data_dir:
|
|
|
+ self.data_dirs.append(data_dir)
|
|
|
+
|
|
|
+ # 添加默认搜索路径
|
|
|
+ current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
+ project_root = os.path.dirname(os.path.dirname(current_dir))
|
|
|
+ self.data_dirs.extend([
|
|
|
+ project_root, # 项目根目录
|
|
|
+ os.path.join(project_root, 'data'), # data目录
|
|
|
+ os.path.join(project_root, 'src', 'leadlag') # leadlag目录
|
|
|
+ ])
|
|
|
+
|
|
|
self.setup_routes()
|
|
|
|
|
|
+ def get_available_databases(self) -> List[Dict[str, Any]]:
|
|
|
+ """获取所有可用的数据库文件"""
|
|
|
+ databases = []
|
|
|
+
|
|
|
+ for data_dir in self.data_dirs:
|
|
|
+ if not os.path.exists(data_dir):
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 搜索数据库文件
|
|
|
+ db_pattern = os.path.join(data_dir, "*.db")
|
|
|
+ db_files = glob.glob(db_pattern)
|
|
|
+
|
|
|
+ for db_file in db_files:
|
|
|
+ if os.path.getsize(db_file) > 0: # 只包含非空文件
|
|
|
+ file_name = os.path.basename(db_file)
|
|
|
+ file_size = os.path.getsize(db_file)
|
|
|
+ file_mtime = os.path.getmtime(db_file)
|
|
|
+
|
|
|
+ databases.append({
|
|
|
+ 'path': db_file,
|
|
|
+ 'name': file_name,
|
|
|
+ 'size': file_size,
|
|
|
+ 'modified_time': datetime.fromtimestamp(file_mtime).strftime('%Y-%m-%d %H:%M:%S'),
|
|
|
+ 'modified_timestamp': file_mtime
|
|
|
+ })
|
|
|
+
|
|
|
+ # 按修改时间排序,最新的在前面
|
|
|
+ databases.sort(key=lambda x: x['modified_timestamp'], reverse=True)
|
|
|
+ return databases
|
|
|
+
|
|
|
def setup_routes(self):
|
|
|
"""设置路由"""
|
|
|
@self.app.route('/')
|
|
|
def index():
|
|
|
return render_template('dashboard.html')
|
|
|
|
|
|
+ @self.app.route('/api/databases')
|
|
|
+ def get_databases():
|
|
|
+ """获取可用数据库列表API"""
|
|
|
+ try:
|
|
|
+ databases = self.get_available_databases()
|
|
|
+ return jsonify({
|
|
|
+ 'success': True,
|
|
|
+ 'data': databases
|
|
|
+ })
|
|
|
+ except Exception as e:
|
|
|
+ return jsonify({
|
|
|
+ 'success': False,
|
|
|
+ 'error': str(e)
|
|
|
+ }), 500
|
|
|
+
|
|
|
@self.app.route('/api/price_data')
|
|
|
def get_price_data():
|
|
|
"""获取价格数据API"""
|
|
|
hours = request.args.get('hours', 24, type=int)
|
|
|
symbol = request.args.get('symbol', '')
|
|
|
+ db_path = request.args.get('db_path', self.db_path)
|
|
|
|
|
|
try:
|
|
|
- data = self.get_price_data(hours, symbol)
|
|
|
+ data = self.get_price_data(hours, symbol, db_path)
|
|
|
return jsonify({
|
|
|
'success': True,
|
|
|
'data': data
|
|
|
@@ -45,9 +106,10 @@ class TradingDashboard:
|
|
|
"""获取交易事件API"""
|
|
|
hours = request.args.get('hours', 24, type=int)
|
|
|
symbol = request.args.get('symbol', '')
|
|
|
+ db_path = request.args.get('db_path', self.db_path)
|
|
|
|
|
|
try:
|
|
|
- events = self.get_trading_events(hours, symbol)
|
|
|
+ events = self.get_trading_events(hours, symbol, db_path)
|
|
|
return jsonify({
|
|
|
'success': True,
|
|
|
'data': events
|
|
|
@@ -58,11 +120,15 @@ class TradingDashboard:
|
|
|
'error': str(e)
|
|
|
}), 500
|
|
|
|
|
|
- @self.app.route('/api/stats')
|
|
|
- def get_stats():
|
|
|
+ @self.app.route('/api/statistics')
|
|
|
+ def get_statistics():
|
|
|
"""获取统计数据API"""
|
|
|
+ hours = request.args.get('hours', 24, type=int)
|
|
|
+ symbol = request.args.get('symbol', '')
|
|
|
+ db_path = request.args.get('db_path', self.db_path)
|
|
|
+
|
|
|
try:
|
|
|
- stats = self.get_statistics()
|
|
|
+ stats = self.get_statistics(hours, symbol, db_path)
|
|
|
return jsonify({
|
|
|
'success': True,
|
|
|
'data': stats
|
|
|
@@ -73,152 +139,198 @@ class TradingDashboard:
|
|
|
'error': str(e)
|
|
|
}), 500
|
|
|
|
|
|
- def get_price_data(self, hours: int = 24, symbol: str = '') -> List[Dict[str, Any]]:
|
|
|
+ def get_price_data(self, hours: int = 24, symbol: str = '', db_path: str = None) -> List[Dict[str, Any]]:
|
|
|
"""获取价格数据"""
|
|
|
- conn = sqlite3.connect(self.db_path)
|
|
|
- conn.row_factory = sqlite3.Row
|
|
|
-
|
|
|
- try:
|
|
|
- query = """
|
|
|
- SELECT timestamp, symbol, binance_price, lighter_price,
|
|
|
- lighter_bid, lighter_ask, spread_bps
|
|
|
- FROM price_data
|
|
|
- WHERE timestamp > datetime('now', '-{} hours')
|
|
|
- """.format(hours)
|
|
|
+ if db_path is None:
|
|
|
+ db_path = self.db_path
|
|
|
|
|
|
- if symbol:
|
|
|
- query += " AND symbol = ?"
|
|
|
- cursor = conn.execute(query, (symbol,))
|
|
|
- else:
|
|
|
- cursor = conn.execute(query)
|
|
|
+ if not os.path.exists(db_path):
|
|
|
+ return []
|
|
|
|
|
|
- rows = cursor.fetchall()
|
|
|
-
|
|
|
- data = []
|
|
|
- for row in rows:
|
|
|
- data.append({
|
|
|
- 'timestamp': row['timestamp'],
|
|
|
- 'symbol': row['symbol'],
|
|
|
- 'binance_price': row['binance_price'],
|
|
|
- 'lighter_price': row['lighter_price'],
|
|
|
- 'lighter_bid': row['lighter_bid'],
|
|
|
- 'lighter_ask': row['lighter_ask'],
|
|
|
- 'spread_bps': row['spread_bps']
|
|
|
- })
|
|
|
+ conn = sqlite3.connect(db_path)
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 计算时间范围
|
|
|
+ end_time = datetime.now()
|
|
|
+ start_time = end_time - timedelta(hours=hours)
|
|
|
+
|
|
|
+ # 构建查询
|
|
|
+ query = """
|
|
|
+ SELECT timestamp, symbol, binance_price, lighter_price, spread_bps
|
|
|
+ FROM price_data
|
|
|
+ WHERE timestamp >= ? AND timestamp <= ?
|
|
|
+ """
|
|
|
+ params = [start_time.isoformat(), end_time.isoformat()]
|
|
|
+
|
|
|
+ if symbol:
|
|
|
+ query += " AND symbol = ?"
|
|
|
+ params.append(symbol)
|
|
|
|
|
|
- return data
|
|
|
+ query += " ORDER BY timestamp ASC"
|
|
|
+
|
|
|
+ cursor.execute(query, params)
|
|
|
+ rows = cursor.fetchall()
|
|
|
+ conn.close()
|
|
|
|
|
|
- finally:
|
|
|
- conn.close()
|
|
|
+ # 转换为字典格式
|
|
|
+ data = []
|
|
|
+ for row in rows:
|
|
|
+ data.append({
|
|
|
+ 'timestamp': row[0],
|
|
|
+ 'symbol': row[1],
|
|
|
+ 'binance_price': float(row[2]) if row[2] else None,
|
|
|
+ 'lighter_price': float(row[3]) if row[3] else None,
|
|
|
+ 'spread_bps': float(row[4]) if row[4] else None
|
|
|
+ })
|
|
|
+
|
|
|
+ return data
|
|
|
|
|
|
- def get_trading_events(self, hours: int = 24, symbol: str = '') -> List[Dict[str, Any]]:
|
|
|
+ def get_trading_events(self, hours: int = 24, symbol: str = '', db_path: str = None) -> List[Dict[str, Any]]:
|
|
|
"""获取交易事件"""
|
|
|
- conn = sqlite3.connect(self.db_path)
|
|
|
- conn.row_factory = sqlite3.Row
|
|
|
-
|
|
|
- try:
|
|
|
- query = """
|
|
|
- SELECT timestamp, symbol, event_type, price, quantity,
|
|
|
- side, success, tx_hash, error_message, metadata
|
|
|
- FROM trading_events
|
|
|
- WHERE timestamp > datetime('now', '-{} hours')
|
|
|
- ORDER BY timestamp DESC
|
|
|
- """.format(hours)
|
|
|
-
|
|
|
- if symbol:
|
|
|
- query = query.replace("ORDER BY", "AND symbol = ? ORDER BY")
|
|
|
- cursor = conn.execute(query, (symbol,))
|
|
|
- else:
|
|
|
- cursor = conn.execute(query)
|
|
|
+ if db_path is None:
|
|
|
+ db_path = self.db_path
|
|
|
|
|
|
- rows = cursor.fetchall()
|
|
|
+ if not os.path.exists(db_path):
|
|
|
+ return []
|
|
|
|
|
|
- events = []
|
|
|
- for row in rows:
|
|
|
- metadata = json.loads(row['metadata']) if row['metadata'] else {}
|
|
|
- events.append({
|
|
|
- 'timestamp': row['timestamp'],
|
|
|
- 'symbol': row['symbol'],
|
|
|
- 'event_type': row['event_type'],
|
|
|
- 'price': row['price'],
|
|
|
- 'quantity': row['quantity'],
|
|
|
- 'side': row['side'],
|
|
|
- 'success': bool(row['success']),
|
|
|
- 'tx_hash': row['tx_hash'],
|
|
|
- 'error_message': row['error_message'],
|
|
|
- 'metadata': metadata
|
|
|
- })
|
|
|
+ conn = sqlite3.connect(db_path)
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 计算时间范围
|
|
|
+ end_time = datetime.now()
|
|
|
+ start_time = end_time - timedelta(hours=hours)
|
|
|
+
|
|
|
+ # 构建查询
|
|
|
+ query = """
|
|
|
+ SELECT timestamp, symbol, event_type, price, quantity, side,
|
|
|
+ strategy_state, spread_bps, lighter_price, binance_price,
|
|
|
+ tx_hash, error_message, metadata
|
|
|
+ FROM trading_events
|
|
|
+ WHERE timestamp >= ? AND timestamp <= ?
|
|
|
+ """
|
|
|
+ params = [start_time.isoformat(), end_time.isoformat()]
|
|
|
+
|
|
|
+ if symbol:
|
|
|
+ query += " AND symbol = ?"
|
|
|
+ params.append(symbol)
|
|
|
|
|
|
- return events
|
|
|
+ query += " ORDER BY timestamp DESC"
|
|
|
|
|
|
- finally:
|
|
|
- conn.close()
|
|
|
+ cursor.execute(query, params)
|
|
|
+ rows = cursor.fetchall()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ # 转换为字典格式
|
|
|
+ events = []
|
|
|
+ for row in rows:
|
|
|
+ metadata = {}
|
|
|
+ if row[12]: # metadata字段
|
|
|
+ try:
|
|
|
+ metadata = json.loads(row[12])
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ events.append({
|
|
|
+ 'timestamp': row[0],
|
|
|
+ 'symbol': row[1],
|
|
|
+ 'event_type': row[2],
|
|
|
+ 'price': float(row[3]) if row[3] else None,
|
|
|
+ 'quantity': float(row[4]) if row[4] else None,
|
|
|
+ 'side': row[5],
|
|
|
+ 'strategy_state': row[6],
|
|
|
+ 'spread_bps': float(row[7]) if row[7] else None,
|
|
|
+ 'lighter_price': float(row[8]) if row[8] else None,
|
|
|
+ 'binance_price': float(row[9]) if row[9] else None,
|
|
|
+ 'tx_hash': row[10],
|
|
|
+ 'error_message': row[11],
|
|
|
+ 'metadata': metadata
|
|
|
+ })
|
|
|
+
|
|
|
+ return events
|
|
|
|
|
|
- def get_statistics(self) -> Dict[str, Any]:
|
|
|
+ def get_statistics(self, hours: int = 24, symbol: str = '', db_path: str = None) -> Dict[str, Any]:
|
|
|
"""获取统计数据"""
|
|
|
- conn = sqlite3.connect(self.db_path)
|
|
|
- conn.row_factory = sqlite3.Row
|
|
|
-
|
|
|
- try:
|
|
|
- # 获取基本统计
|
|
|
- stats = {}
|
|
|
+ if db_path is None:
|
|
|
+ db_path = self.db_path
|
|
|
|
|
|
- # 价格数据统计
|
|
|
- cursor = conn.execute("""
|
|
|
- SELECT COUNT(*) as total_records,
|
|
|
- MIN(timestamp) as first_record,
|
|
|
- MAX(timestamp) as last_record
|
|
|
- FROM price_data
|
|
|
- """)
|
|
|
- price_stats = cursor.fetchone()
|
|
|
+ if not os.path.exists(db_path):
|
|
|
+ return {}
|
|
|
|
|
|
- # 交易事件统计
|
|
|
- cursor = conn.execute("""
|
|
|
- SELECT event_type, COUNT(*) as count
|
|
|
- FROM trading_events
|
|
|
- GROUP BY event_type
|
|
|
- """)
|
|
|
- event_stats = cursor.fetchall()
|
|
|
-
|
|
|
- # 成功率统计
|
|
|
- cursor = conn.execute("""
|
|
|
- SELECT
|
|
|
- SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful,
|
|
|
- COUNT(*) as total
|
|
|
- FROM trading_events
|
|
|
- WHERE event_type LIKE '%_success' OR event_type LIKE '%_failed'
|
|
|
- """)
|
|
|
- success_stats = cursor.fetchone()
|
|
|
-
|
|
|
- stats = {
|
|
|
- 'price_data': {
|
|
|
- 'total_records': price_stats['total_records'],
|
|
|
- 'first_record': price_stats['first_record'],
|
|
|
- 'last_record': price_stats['last_record']
|
|
|
- },
|
|
|
- 'trading_events': {
|
|
|
- event['event_type']: event['count']
|
|
|
- for event in event_stats
|
|
|
- },
|
|
|
- 'success_rate': {
|
|
|
- 'successful': success_stats['successful'] or 0,
|
|
|
- 'total': success_stats['total'] or 0,
|
|
|
- 'rate': (success_stats['successful'] / success_stats['total'] * 100)
|
|
|
- if success_stats['total'] > 0 else 0
|
|
|
- }
|
|
|
+ conn = sqlite3.connect(db_path)
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 计算时间范围
|
|
|
+ end_time = datetime.now()
|
|
|
+ start_time = end_time - timedelta(hours=hours)
|
|
|
+
|
|
|
+ stats = {}
|
|
|
+
|
|
|
+ # 价格数据统计
|
|
|
+ query = "SELECT COUNT(*) FROM price_data WHERE timestamp >= ? AND timestamp <= ?"
|
|
|
+ params = [start_time.isoformat(), end_time.isoformat()]
|
|
|
+ if symbol:
|
|
|
+ query += " AND symbol = ?"
|
|
|
+ params.append(symbol)
|
|
|
+
|
|
|
+ cursor.execute(query, params)
|
|
|
+ stats['price_data_count'] = cursor.fetchone()[0]
|
|
|
+
|
|
|
+ # 交易事件统计
|
|
|
+ query = "SELECT event_type, COUNT(*) FROM trading_events WHERE timestamp >= ? AND timestamp <= ?"
|
|
|
+ params = [start_time.isoformat(), end_time.isoformat()]
|
|
|
+ if symbol:
|
|
|
+ query += " AND symbol = ?"
|
|
|
+ params.append(symbol)
|
|
|
+ query += " GROUP BY event_type"
|
|
|
+
|
|
|
+ cursor.execute(query, params)
|
|
|
+ event_stats = {}
|
|
|
+ for row in cursor.fetchall():
|
|
|
+ event_stats[row[0]] = row[1]
|
|
|
+
|
|
|
+ stats['trading_events'] = event_stats
|
|
|
+
|
|
|
+ # 最新价格信息
|
|
|
+ query = """
|
|
|
+ SELECT symbol, binance_price, lighter_price, spread_bps, timestamp
|
|
|
+ FROM price_data
|
|
|
+ WHERE timestamp >= ? AND timestamp <= ?
|
|
|
+ """
|
|
|
+ params = [start_time.isoformat(), end_time.isoformat()]
|
|
|
+ if symbol:
|
|
|
+ query += " AND symbol = ?"
|
|
|
+ params.append(symbol)
|
|
|
+ query += " ORDER BY timestamp DESC LIMIT 1"
|
|
|
+
|
|
|
+ cursor.execute(query, params)
|
|
|
+ latest_price = cursor.fetchone()
|
|
|
+ if latest_price:
|
|
|
+ stats['latest_price'] = {
|
|
|
+ 'symbol': latest_price[0],
|
|
|
+ 'binance_price': float(latest_price[1]) if latest_price[1] else None,
|
|
|
+ 'lighter_price': float(latest_price[2]) if latest_price[2] else None,
|
|
|
+ 'spread_bps': float(latest_price[3]) if latest_price[3] else None,
|
|
|
+ 'timestamp': latest_price[4]
|
|
|
}
|
|
|
-
|
|
|
- return stats
|
|
|
|
|
|
- finally:
|
|
|
- conn.close()
|
|
|
+ conn.close()
|
|
|
+ return stats
|
|
|
|
|
|
def run(self, host='127.0.0.1', port=5000, debug=True):
|
|
|
"""启动Web服务器"""
|
|
|
- print(f"启动交易策略监控面板: http://{host}:{port}")
|
|
|
- self.app.run(host=host, port=port, debug=debug)
|
|
|
-
|
|
|
-if __name__ == '__main__':
|
|
|
- # 创建并启动dashboard
|
|
|
- dashboard = TradingDashboard()
|
|
|
- dashboard.run()
|
|
|
+ print(f"🚀 启动交易策略Web面板...")
|
|
|
+ print(f"📊 访问地址: http://{host}:{port}")
|
|
|
+ print(f"📁 数据库路径: {os.path.abspath(self.db_path)}")
|
|
|
+
|
|
|
+ # 显示可用的数据库文件
|
|
|
+ databases = self.get_available_databases()
|
|
|
+ if databases:
|
|
|
+ print(f"📋 发现 {len(databases)} 个数据库文件:")
|
|
|
+ for db in databases[:5]: # 只显示前5个
|
|
|
+ print(f" - {db['name']} ({db['size']} bytes, {db['modified_time']})")
|
|
|
+ if len(databases) > 5:
|
|
|
+ print(f" ... 还有 {len(databases) - 5} 个文件")
|
|
|
+ else:
|
|
|
+ print("⚠️ 未发现任何数据库文件")
|
|
|
+
|
|
|
+ self.app.run(host=host, port=port, debug=debug)
|