Add A share to display
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
A股数据获取测试脚本
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'web'))
|
||||
|
||||
from web.cn_stock_data import ChinaStockData
|
||||
import pandas as pd
|
||||
|
||||
def test_a_stock_data():
|
||||
"""测试A股数据获取功能"""
|
||||
print("开始测试A股数据获取功能...")
|
||||
|
||||
# 初始化A股数据获取器
|
||||
china_stock = ChinaStockData()
|
||||
|
||||
# 测试获取热门股票列表
|
||||
print("\n1. 测试获取热门股票列表:")
|
||||
popular_stocks = china_stock.get_popular_stocks()
|
||||
print(f"热门股票数量: {len(popular_stocks)}")
|
||||
for i, stock in enumerate(popular_stocks[:5]):
|
||||
print(f" {i+1}. {stock['symbol']} - {stock['name']}")
|
||||
|
||||
# 测试获取股票K线数据
|
||||
print("\n2. 测试获取股票K线数据:")
|
||||
test_symbols = ['000001', '600519', '000858'] # 平安银行、贵州茅台、五粮液
|
||||
|
||||
for symbol in test_symbols:
|
||||
print(f"\n测试股票: {symbol}")
|
||||
|
||||
# 测试日线数据
|
||||
print(" 获取日线数据...")
|
||||
try:
|
||||
df_daily = china_stock.get_kl_data(symbol, '1d', limit=100)
|
||||
if df_daily is not None:
|
||||
print(f" 成功获取 {len(df_daily)} 条日线数据")
|
||||
print(f" 时间范围: {df_daily['date'].min()} 到 {df_daily['date'].max()}")
|
||||
print(f" 最新价格: {df_daily['close'].iloc[-1]:.2f}")
|
||||
else:
|
||||
print(" 获取日线数据失败")
|
||||
except Exception as e:
|
||||
print(f" 获取日线数据出错: {e}")
|
||||
|
||||
# 测试分钟数据
|
||||
print(" 获取5分钟数据...")
|
||||
try:
|
||||
df_5m = china_stock.get_kl_data(symbol, '5m', limit=50)
|
||||
if df_5m is not None:
|
||||
print(f" 成功获取 {len(df_5m)} 条5分钟数据")
|
||||
print(f" 时间范围: {df_5m['date'].min()} 到 {df_5m['date'].max()}")
|
||||
else:
|
||||
print(" 获取5分钟数据失败")
|
||||
except Exception as e:
|
||||
print(f" 获取5分钟数据出错: {e}")
|
||||
|
||||
# 测试获取股票列表
|
||||
print("\n3. 测试获取股票列表:")
|
||||
try:
|
||||
stock_list = china_stock.get_stock_list()
|
||||
if stock_list:
|
||||
print(f"成功获取 {len(stock_list)} 只股票")
|
||||
print("前5只股票:")
|
||||
for i, stock in enumerate(stock_list[:5]):
|
||||
print(f" {i+1}. {stock['symbol']} - {stock['name']} - 价格: {stock['price']} - 涨跌幅: {stock['change_pct']}%")
|
||||
else:
|
||||
print("获取股票列表失败")
|
||||
except Exception as e:
|
||||
print(f"获取股票列表出错: {e}")
|
||||
|
||||
print("\nA股数据获取测试完成!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_a_stock_data()
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
测试分批次数据获取功能
|
||||
验证A股和加密货币数据的大时间范围获取
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append('web')
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from cn_stock_data import ChinaStockData
|
||||
import ccxt
|
||||
|
||||
def test_a_stock_batch_data():
|
||||
"""测试A股分批次数据获取"""
|
||||
print("=== 测试A股分批次数据获取 ===")
|
||||
|
||||
china_stock = ChinaStockData()
|
||||
|
||||
# 测试获取更长时间范围的数据
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=180) # 6个月数据
|
||||
|
||||
print(f"测试时间范围: {start_date.strftime('%Y-%m-%d')} 到 {end_date.strftime('%Y-%m-%d')}")
|
||||
|
||||
# 测试不同时间周期
|
||||
test_cases = [
|
||||
('600519', '1d', '日线数据'),
|
||||
('600519', '1h', '1小时数据'),
|
||||
('600519', '15m', '15分钟数据'),
|
||||
]
|
||||
|
||||
for symbol, timeframe, description in test_cases:
|
||||
print(f"\n测试 {description}: {symbol} {timeframe}")
|
||||
|
||||
try:
|
||||
df = china_stock.get_kl_data(
|
||||
symbol=symbol,
|
||||
timeframe=timeframe,
|
||||
start_date=start_date.strftime('%Y-%m-%d'),
|
||||
end_date=end_date.strftime('%Y-%m-%d'),
|
||||
limit=5000
|
||||
)
|
||||
|
||||
if df is not None:
|
||||
print(f"✅ 成功获取 {len(df)} 条记录")
|
||||
print(f" 时间范围: {df['date'].min()} 到 {df['date'].max()}")
|
||||
print(f" 数据列: {list(df.columns)}")
|
||||
else:
|
||||
print(f"❌ 获取失败")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {e}")
|
||||
|
||||
def test_crypto_batch_data():
|
||||
"""测试加密货币分批次数据获取"""
|
||||
print("\n=== 测试加密货币分批次数据获取 ===")
|
||||
|
||||
# 初始化交易所
|
||||
exchange = ccxt.binance({
|
||||
'enableRateLimit': True,
|
||||
})
|
||||
|
||||
# 测试获取更长时间范围的数据
|
||||
end_time = datetime.now()
|
||||
start_time = end_time - timedelta(days=30) # 30天数据
|
||||
|
||||
print(f"测试时间范围: {start_time} 到 {end_time}")
|
||||
|
||||
# 转换为时间戳
|
||||
start_timestamp = int(start_time.timestamp() * 1000)
|
||||
end_timestamp = int(end_time.timestamp() * 1000)
|
||||
|
||||
# 测试不同时间周期
|
||||
test_cases = [
|
||||
('BTC/USDT:USDT', '1d', '日线数据'),
|
||||
('BTC/USDT:USDT', '1h', '1小时数据'),
|
||||
('BTC/USDT:USDT', '5m', '5分钟数据'),
|
||||
]
|
||||
|
||||
for symbol, timeframe, description in test_cases:
|
||||
print(f"\n测试 {description}: {symbol} {timeframe}")
|
||||
|
||||
try:
|
||||
# 模拟分批次获取逻辑
|
||||
all_ohlcv = []
|
||||
current_since = start_timestamp
|
||||
request_count = 0
|
||||
max_requests = 10
|
||||
|
||||
batch_size = 500 if timeframe in ['1m', '5m'] else 1000
|
||||
|
||||
while request_count < max_requests and current_since < end_timestamp:
|
||||
request_count += 1
|
||||
print(f" 批次 {request_count}: 获取数据...")
|
||||
|
||||
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=batch_size)
|
||||
|
||||
if not ohlcv or len(ohlcv) == 0:
|
||||
break
|
||||
|
||||
all_ohlcv.extend(ohlcv)
|
||||
last_timestamp = ohlcv[-1][0]
|
||||
|
||||
if last_timestamp >= end_timestamp:
|
||||
break
|
||||
|
||||
if len(ohlcv) < batch_size:
|
||||
break
|
||||
|
||||
current_since = last_timestamp + 1
|
||||
|
||||
# 防止请求过频
|
||||
import time
|
||||
time.sleep(0.3)
|
||||
|
||||
if all_ohlcv:
|
||||
print(f"✅ 成功获取 {len(all_ohlcv)} 条记录 (共 {request_count} 个批次)")
|
||||
|
||||
# 时间范围检查
|
||||
first_time = datetime.fromtimestamp(all_ohlcv[0][0] / 1000)
|
||||
last_time = datetime.fromtimestamp(all_ohlcv[-1][0] / 1000)
|
||||
print(f" 时间范围: {first_time} 到 {last_time}")
|
||||
else:
|
||||
print(f"❌ 获取失败")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {e}")
|
||||
|
||||
def test_data_quality():
|
||||
"""测试数据质量"""
|
||||
print("\n=== 测试数据质量 ===")
|
||||
|
||||
china_stock = ChinaStockData()
|
||||
|
||||
# 获取一小段数据进行质量检查
|
||||
df = china_stock.get_kl_data(
|
||||
symbol='600519',
|
||||
timeframe='1d',
|
||||
limit=100
|
||||
)
|
||||
|
||||
if df is not None:
|
||||
print(f"数据行数: {len(df)}")
|
||||
print(f"数据列: {list(df.columns)}")
|
||||
|
||||
# 检查缺失值
|
||||
missing_values = df.isnull().sum()
|
||||
print(f"缺失值统计:")
|
||||
for col, count in missing_values.items():
|
||||
if count > 0:
|
||||
print(f" {col}: {count}")
|
||||
|
||||
# 检查数据类型
|
||||
print(f"数据类型:")
|
||||
for col, dtype in df.dtypes.items():
|
||||
print(f" {col}: {dtype}")
|
||||
|
||||
# 检查时间连续性
|
||||
if len(df) > 1:
|
||||
time_diffs = df['date'].diff().dropna()
|
||||
print(f"时间间隔统计:")
|
||||
print(f" 最小间隔: {time_diffs.min()}")
|
||||
print(f" 最大间隔: {time_diffs.max()}")
|
||||
print(f" 平均间隔: {time_diffs.mean()}")
|
||||
|
||||
# 检查价格合理性
|
||||
price_cols = ['open', 'high', 'low', 'close']
|
||||
for col in price_cols:
|
||||
if col in df.columns:
|
||||
print(f"{col} 价格范围: {df[col].min():.2f} - {df[col].max():.2f}")
|
||||
|
||||
print("✅ 数据质量检查完成")
|
||||
else:
|
||||
print("❌ 无法获取数据进行质量检查")
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("开始测试分批次数据获取功能...\n")
|
||||
|
||||
# 测试A股数据
|
||||
test_a_stock_batch_data()
|
||||
|
||||
# 测试加密货币数据
|
||||
test_crypto_batch_data()
|
||||
|
||||
# 测试数据质量
|
||||
test_data_quality()
|
||||
|
||||
print("\n测试完成!")
|
||||
+232
-50
@@ -13,11 +13,13 @@ import time
|
||||
import traceback
|
||||
from pytz import timezone
|
||||
import talib.abstract as ta
|
||||
import numpy as np
|
||||
# 添加父目录到系统路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from ChanLun import ChanLun
|
||||
from ChanEnum import Chan_BI_DIR, Chan_SEG_DIR, Chan_KLC_FX, Chan_FX_TYPE
|
||||
from cn_stock_data import ChinaStockData
|
||||
|
||||
# 添加买卖点枚举类型
|
||||
class TRADE_POINT_TYPE:
|
||||
@@ -35,6 +37,9 @@ exchange = ccxt.binance({
|
||||
'enableRateLimit': True,
|
||||
})
|
||||
|
||||
# 初始化A股数据获取器
|
||||
china_stock = ChinaStockData()
|
||||
|
||||
# 时间周期映射
|
||||
TIMEFRAMES = {
|
||||
'1m': '1分钟',
|
||||
@@ -54,8 +59,32 @@ SYMBOLS = [
|
||||
'ADA/USDT:USDT', 'DOGE/USDT:USDT', 'AVAX/USDT:USDT', 'DOT/USDT:USDT', 'MATIC/USDT:USDT'
|
||||
]
|
||||
|
||||
# A股热门股票
|
||||
A_STOCK_SYMBOLS = china_stock.get_popular_stocks()
|
||||
|
||||
def detect_symbol_type(symbol):
|
||||
"""检测交易对类型:crypto 或 a_stock"""
|
||||
if '/' in symbol and 'USDT' in symbol:
|
||||
return 'crypto'
|
||||
elif len(symbol) == 6 and symbol.isdigit():
|
||||
return 'a_stock'
|
||||
else:
|
||||
return 'unknown'
|
||||
|
||||
def get_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
|
||||
"""获取K线数据,支持分页加载确保获取指定时间范围内的所有数据"""
|
||||
"""获取K线数据,支持加密货币和A股"""
|
||||
symbol_type = detect_symbol_type(symbol)
|
||||
|
||||
if symbol_type == 'crypto':
|
||||
return get_crypto_kl_data(symbol, timeframe, limit, start_time, end_time)
|
||||
elif symbol_type == 'a_stock':
|
||||
return get_a_stock_kl_data(symbol, timeframe, limit, start_time, end_time)
|
||||
else:
|
||||
print(f"未知的交易对类型: {symbol}")
|
||||
return None
|
||||
|
||||
def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
|
||||
"""获取加密货币K线数据,支持分页加载确保获取指定时间范围内的所有数据"""
|
||||
try:
|
||||
# 初始化参数
|
||||
since = None
|
||||
@@ -73,42 +102,73 @@ def get_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
|
||||
except ValueError:
|
||||
print(f"无效的结束时间: {end_time}")
|
||||
|
||||
# 根据时间周期调整每次请求的数据量
|
||||
batch_size = 1000 # 默认批次大小
|
||||
if timeframe in ['1m', '3m', '5m']:
|
||||
batch_size = 500 # 分钟级数据减少批次大小
|
||||
elif timeframe in ['15m', '30m', '1h']:
|
||||
batch_size = 1000
|
||||
else:
|
||||
batch_size = 1500 # 日线及以上可以获取更多
|
||||
|
||||
# 初始化存储所有K线数据的列表
|
||||
all_ohlcv = []
|
||||
|
||||
# 初始化当前查询的开始时间
|
||||
current_since = since
|
||||
|
||||
# 添加请求计数和最大限制
|
||||
request_count = 0
|
||||
max_requests = 50 # 最大请求次数,防止无限循环
|
||||
|
||||
print(f"开始分批获取加密货币数据: {symbol}, {timeframe}")
|
||||
|
||||
# 分页加载数据
|
||||
while True:
|
||||
print(f"获取数据: {symbol}, {timeframe}, limit={limit}, since={current_since}")
|
||||
while request_count < max_requests:
|
||||
request_count += 1
|
||||
|
||||
# 获取当前页的数据
|
||||
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=limit)
|
||||
print(f"批次 {request_count}: 获取数据 since={current_since}, limit={batch_size}")
|
||||
|
||||
# 如果没有获取到数据,结束循环
|
||||
if not ohlcv or len(ohlcv) == 0:
|
||||
break
|
||||
try:
|
||||
# 获取当前页的数据
|
||||
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=batch_size)
|
||||
|
||||
# 将获取到的数据添加到总列表中
|
||||
all_ohlcv.extend(ohlcv)
|
||||
|
||||
# 获取最后一条数据的时间戳
|
||||
last_timestamp = ohlcv[-1][0]
|
||||
|
||||
# 如果已达到结束时间,结束循环
|
||||
if until and last_timestamp >= until:
|
||||
break
|
||||
# 如果没有获取到数据,结束循环
|
||||
if not ohlcv or len(ohlcv) == 0:
|
||||
print(f"批次 {request_count}: 未获取到数据,结束")
|
||||
break
|
||||
|
||||
# 将获取到的数据添加到总列表中
|
||||
all_ohlcv.extend(ohlcv)
|
||||
print(f"批次 {request_count}: 获取到 {len(ohlcv)} 条记录")
|
||||
|
||||
# 如果获取的数据条数小于限制数,说明已经获取完所有数据
|
||||
if len(ohlcv) < limit:
|
||||
break
|
||||
# 获取最后一条数据的时间戳
|
||||
last_timestamp = ohlcv[-1][0]
|
||||
|
||||
# 更新下一页的开始时间(加1毫秒避免重复)
|
||||
current_since = last_timestamp + 1
|
||||
# 如果已达到结束时间,结束循环
|
||||
if until and last_timestamp >= until:
|
||||
print(f"批次 {request_count}: 已达到结束时间,结束")
|
||||
break
|
||||
|
||||
# 如果获取的数据条数小于限制数,说明已经获取完所有数据
|
||||
if len(ohlcv) < batch_size:
|
||||
print(f"批次 {request_count}: 数据不足批次大小,已获取完所有数据")
|
||||
break
|
||||
|
||||
# 更新下一页的开始时间(加1毫秒避免重复)
|
||||
current_since = last_timestamp + 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"批次 {request_count} 获取失败: {e}")
|
||||
# 如果单个批次失败,继续尝试下一个批次
|
||||
if current_since:
|
||||
# 尝试增加时间跳过可能的问题时间点
|
||||
current_since += 60000 # 跳过1分钟
|
||||
else:
|
||||
break
|
||||
|
||||
# 防止API请求过于频繁
|
||||
time.sleep(0.5) # 等待0.5秒
|
||||
time.sleep(0.3) # 减少到0.3秒提高效率
|
||||
|
||||
# 数据为空的情况
|
||||
if not all_ohlcv or len(all_ohlcv) == 0:
|
||||
@@ -128,33 +188,76 @@ def get_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
|
||||
|
||||
# 按时间排序
|
||||
df = df.sort_values('timestamp')
|
||||
"""
|
||||
chan = ChanLun()
|
||||
klc_list = chan.get_klc_list(df)
|
||||
klc_index = 0
|
||||
df_copy = df.copy()
|
||||
ret_df = pd.DataFrame(columns=['timestamp', 'open', 'high', 'low', 'close', 'volume', 'date'])
|
||||
for index in range(0, len(df)):
|
||||
if klc_index >= len(klc_list):
|
||||
klc_index = len(klc_list) - 1
|
||||
klc = klc_list[klc_index]
|
||||
if index == klc.start_klu.index:
|
||||
ret_df.loc[klc_index] = df_copy.loc[index]
|
||||
klc_index += 1
|
||||
"""
|
||||
|
||||
# 限制数据条数的逻辑 - 优先考虑时间范围
|
||||
if start_time and end_time:
|
||||
# 如果指定了明确的时间范围,返回该时间范围内的所有数据
|
||||
print(f"用户指定了时间范围,返回完整数据 {len(df)} 条记录")
|
||||
if len(df) > 10000: # 防止数据量过大,设置一个合理的上限
|
||||
print(f"警告:数据量过大({len(df)}条),为保证性能将限制为最新的10000条记录")
|
||||
df = df.tail(10000).reset_index(drop=True)
|
||||
elif limit and len(df) > limit:
|
||||
# 如果没有指定明确时间范围,使用默认的limit限制
|
||||
print(f"未指定明确时间范围,应用默认限制,返回最新的 {limit} 条记录")
|
||||
df = df.tail(limit).reset_index(drop=True)
|
||||
|
||||
df = add_indicators(df)
|
||||
|
||||
# 如果过滤后没有数据,返回None
|
||||
if len(df) == 0:
|
||||
print("过滤后无数据")
|
||||
return None
|
||||
|
||||
print(f"获取到总共 {len(df)} 条数据")
|
||||
print(f"成功获取加密货币数据: {len(df)} 条记录 (共 {request_count} 个批次)")
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取数据错误: {e}")
|
||||
print(f"获取加密货币数据错误: {e}")
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def get_a_stock_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
|
||||
"""获取A股K线数据"""
|
||||
try:
|
||||
# 处理时间戳参数转换为日期字符串
|
||||
start_date = None
|
||||
end_date = None
|
||||
|
||||
if start_time:
|
||||
try:
|
||||
start_timestamp = int(start_time)
|
||||
start_date = datetime.fromtimestamp(start_timestamp / 1000).strftime('%Y-%m-%d')
|
||||
except:
|
||||
start_date = start_time
|
||||
|
||||
if end_time:
|
||||
try:
|
||||
end_timestamp = int(end_time)
|
||||
end_date = datetime.fromtimestamp(end_timestamp / 1000).strftime('%Y-%m-%d')
|
||||
except:
|
||||
end_date = end_time
|
||||
|
||||
# 如果用户指定了时间范围,优先获取该范围内的所有数据
|
||||
actual_limit = limit
|
||||
if start_date and end_date:
|
||||
print(f"用户指定了时间范围 {start_date} 到 {end_date},将获取该范围内的所有数据")
|
||||
actual_limit = None # 不限制数据条数,获取完整时间范围数据
|
||||
|
||||
# 调用A股数据获取器
|
||||
df = china_stock.get_kl_data(symbol, timeframe, start_date, end_date, actual_limit)
|
||||
|
||||
if df is None:
|
||||
print(f"未获取到A股数据: {symbol}")
|
||||
return None
|
||||
|
||||
print(f"获取到A股数据: {len(df)} 条记录")
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取A股数据错误: {e}")
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def add_indicators(df):
|
||||
fast = 8
|
||||
slow = 16
|
||||
@@ -178,12 +281,17 @@ def add_indicators(df):
|
||||
df['ma250'] = df['ma250'].fillna(0)
|
||||
df['rsi'] = df['rsi'].fillna(0)
|
||||
df['avg_volume'] = df['volume'].rolling(10).mean()
|
||||
# 计算量比
|
||||
# 计算量比,避免产生Infinity值
|
||||
df['volume_ratio'] = df['volume'] / df['avg_volume']
|
||||
# 填充缺失值(前N根K线)
|
||||
df['volume_ratio'] = df['volume_ratio'].fillna(1.0)
|
||||
df['avg_volume'] = df['avg_volume'].fillna(0)
|
||||
|
||||
# 处理Infinity和-Infinity值
|
||||
df['volume_ratio'] = df['volume_ratio'].replace([float('inf'), float('-inf')], 1.0)
|
||||
|
||||
return df
|
||||
|
||||
def calculate_macd(df):
|
||||
"""计算MACD指标"""
|
||||
exp1 = df['close'].ewm(span=12, adjust=False).mean()
|
||||
@@ -287,14 +395,10 @@ def identify_trade_points(bi_list, seg_list, zs_list):
|
||||
|
||||
# 辅助函数,转换缠论方向枚举为整数
|
||||
def convert_direction(direction):
|
||||
"""将缠论方向枚举转换为整数"""
|
||||
if direction == Chan_BI_DIR.UP:
|
||||
"""转换方向枚举为数字"""
|
||||
if direction == Chan_BI_DIR.UP or direction == Chan_SEG_DIR.UP:
|
||||
return 1
|
||||
elif direction == Chan_BI_DIR.DOWN:
|
||||
return -1
|
||||
elif direction == Chan_SEG_DIR.UP:
|
||||
return 1
|
||||
elif direction == Chan_SEG_DIR.DOWN:
|
||||
elif direction == Chan_BI_DIR.DOWN or direction == Chan_SEG_DIR.DOWN:
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
@@ -380,10 +484,34 @@ def is_smaller_or_equal_timeframe(tf1, tf2):
|
||||
# 返回tf1是否小于等于tf2
|
||||
return tf1_value <= tf2_value
|
||||
|
||||
def clean_dataframe_for_json(df):
|
||||
"""清理DataFrame中的NaN值,确保JSON序列化正常"""
|
||||
# 创建副本以避免修改原数据
|
||||
df_clean = df.copy()
|
||||
|
||||
# 将NaN、inf、-inf替换为None
|
||||
df_clean = df_clean.replace([np.nan, np.inf, -np.inf], None)
|
||||
|
||||
# 处理数值列,确保值为有限数字或None
|
||||
numeric_columns = df_clean.select_dtypes(include=[np.number]).columns
|
||||
for col in numeric_columns:
|
||||
# 确保所有数值都是有限的
|
||||
df_clean[col] = df_clean[col].apply(lambda x: x if (x is not None and np.isfinite(x)) else None)
|
||||
|
||||
# 处理时间列,确保格式正确
|
||||
datetime_columns = df_clean.select_dtypes(include=['datetime64']).columns
|
||||
for col in datetime_columns:
|
||||
df_clean[col] = df_clean[col].dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
return df_clean
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""主页"""
|
||||
return render_template('index.html', timeframes=TIMEFRAMES, symbols=SYMBOLS)
|
||||
return render_template('index.html',
|
||||
timeframes=TIMEFRAMES,
|
||||
symbols=SYMBOLS,
|
||||
a_stock_symbols=A_STOCK_SYMBOLS)
|
||||
|
||||
@app.route('/api/analyze')
|
||||
def analyze():
|
||||
@@ -448,7 +576,7 @@ def analyze():
|
||||
|
||||
# 添加主周期分析结果到返回数据
|
||||
result.update({
|
||||
'kline_data': df.to_dict('records'),
|
||||
'kline_data': clean_dataframe_for_json(df).to_dict('records'),
|
||||
'bi_list': [{
|
||||
'start_time': bi.start_klc.end_time if isinstance(bi.start_klc.end_time, str) else bi.start_klc.end_time.astimezone(client_tz).isoformat(),
|
||||
'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None,
|
||||
@@ -522,7 +650,7 @@ def analyze():
|
||||
} for bi in element_analysis['bi_list'] if bi.end_klc]
|
||||
|
||||
# 添加小周期K线数据
|
||||
result['element_kline_data'] = element_df.to_dict('records')
|
||||
result['element_kline_data'] = clean_dataframe_for_json(element_df).to_dict('records')
|
||||
|
||||
result['element_seg_list'] = [{
|
||||
'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(),
|
||||
@@ -584,5 +712,59 @@ def get_symbols():
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)})
|
||||
|
||||
@app.route('/api/a_stocks')
|
||||
def get_a_stocks():
|
||||
"""获取A股股票列表"""
|
||||
try:
|
||||
stock_list = china_stock.get_stock_list()
|
||||
return jsonify(stock_list)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)})
|
||||
|
||||
@app.route('/api/popular_a_stocks')
|
||||
def get_popular_a_stocks():
|
||||
"""获取热门A股股票"""
|
||||
try:
|
||||
return jsonify(china_stock.get_popular_stocks())
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)})
|
||||
|
||||
@app.route('/api/sectors')
|
||||
def get_sectors():
|
||||
"""获取所有行业分类"""
|
||||
try:
|
||||
sectors = china_stock.get_all_sectors()
|
||||
return jsonify(sectors)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)})
|
||||
|
||||
@app.route('/api/stocks_by_sector')
|
||||
def get_stocks_by_sector():
|
||||
"""根据行业获取股票"""
|
||||
try:
|
||||
sector = request.args.get('sector')
|
||||
if sector:
|
||||
stocks = china_stock.get_stock_by_sector(sector)
|
||||
return jsonify(stocks)
|
||||
else:
|
||||
# 返回所有行业的股票分组
|
||||
all_sectors = china_stock.get_stock_by_sector()
|
||||
return jsonify(all_sectors)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)})
|
||||
|
||||
@app.route('/api/search_stock')
|
||||
def search_stock():
|
||||
"""搜索股票 - 增强版"""
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
if not keyword:
|
||||
return jsonify({'error': '搜索关键词不能为空'})
|
||||
|
||||
results = china_stock.search_stock(keyword)
|
||||
return jsonify(results)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=8123)
|
||||
@@ -0,0 +1,749 @@
|
||||
import akshare as ak
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta, time
|
||||
import time as time_module
|
||||
import traceback
|
||||
from pytz import timezone
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
class ChinaStockData:
|
||||
"""A股数据获取类"""
|
||||
|
||||
def __init__(self):
|
||||
self.tz = timezone('Asia/Shanghai')
|
||||
# A股交易时间配置
|
||||
self.trading_hours = {
|
||||
'morning': {'start': '09:30', 'end': '11:30'},
|
||||
'afternoon': {'start': '13:00', 'end': '15:00'}
|
||||
}
|
||||
|
||||
def get_stock_list(self):
|
||||
"""获取A股股票列表"""
|
||||
try:
|
||||
# 获取沪深A股实时行情
|
||||
stock_info = ak.stock_zh_a_spot_em()
|
||||
# 增加到前2000只股票,提供更多选择
|
||||
stock_list = []
|
||||
for index, row in stock_info.head(2000).iterrows():
|
||||
# 过滤掉ST股票和停牌股票
|
||||
stock_name = str(row['名称'])
|
||||
if 'ST' not in stock_name and '*' not in stock_name:
|
||||
stock_list.append({
|
||||
'symbol': row['代码'],
|
||||
'name': row['名称'],
|
||||
'price': float(row['最新价']) if pd.notna(row['最新价']) else 0.0,
|
||||
'change_pct': float(row['涨跌幅']) if pd.notna(row['涨跌幅']) else 0.0,
|
||||
'volume': float(row['成交量']) if pd.notna(row['成交量']) else 0.0,
|
||||
'amount': float(row['成交额']) if pd.notna(row['成交额']) else 0.0
|
||||
})
|
||||
|
||||
# 按成交金额排序,优先显示活跃股票
|
||||
stock_list.sort(key=lambda x: x['amount'], reverse=True)
|
||||
return stock_list
|
||||
except Exception as e:
|
||||
print(f"获取股票列表失败: {e}")
|
||||
return []
|
||||
|
||||
def get_popular_stocks(self):
|
||||
"""获取热门A股股票代码列表 - 扩展版本,按行业分类"""
|
||||
return [
|
||||
# 银行股
|
||||
{'symbol': '600036', 'name': '招商银行', 'sector': '银行'},
|
||||
{'symbol': '000001', 'name': '平安银行', 'sector': '银行'},
|
||||
{'symbol': '600000', 'name': '浦发银行', 'sector': '银行'},
|
||||
{'symbol': '002142', 'name': '宁波银行', 'sector': '银行'},
|
||||
{'symbol': '600016', 'name': '民生银行', 'sector': '银行'},
|
||||
{'symbol': '601288', 'name': '农业银行', 'sector': '银行'},
|
||||
{'symbol': '601398', 'name': '工商银行', 'sector': '银行'},
|
||||
{'symbol': '601328', 'name': '交通银行', 'sector': '银行'},
|
||||
|
||||
# 白酒股
|
||||
{'symbol': '600519', 'name': '贵州茅台', 'sector': '白酒'},
|
||||
{'symbol': '000858', 'name': '五粮液', 'sector': '白酒'},
|
||||
{'symbol': '002304', 'name': '洋河股份', 'sector': '白酒'},
|
||||
{'symbol': '000596', 'name': '古井贡酒', 'sector': '白酒'},
|
||||
{'symbol': '603369', 'name': '今世缘', 'sector': '白酒'},
|
||||
{'symbol': '000799', 'name': '酒鬼酒', 'sector': '白酒'},
|
||||
{'symbol': '600809', 'name': '山西汾酒', 'sector': '白酒'},
|
||||
|
||||
# 科技股
|
||||
{'symbol': '002415', 'name': '海康威视', 'sector': '科技'},
|
||||
{'symbol': '000063', 'name': '中兴通讯', 'sector': '科技'},
|
||||
{'symbol': '002475', 'name': '立讯精密', 'sector': '科技'},
|
||||
{'symbol': '300059', 'name': '东方财富', 'sector': '科技'},
|
||||
{'symbol': '000725', 'name': '京东方A', 'sector': '科技'},
|
||||
{'symbol': '002230', 'name': '科大讯飞', 'sector': '科技'},
|
||||
{'symbol': '300433', 'name': '蓝思科技', 'sector': '科技'},
|
||||
{'symbol': '002236', 'name': '大华股份', 'sector': '科技'},
|
||||
|
||||
# 新能源
|
||||
{'symbol': '300750', 'name': '宁德时代', 'sector': '新能源'},
|
||||
{'symbol': '002594', 'name': '比亚迪', 'sector': '新能源'},
|
||||
{'symbol': '300274', 'name': '阳光电源', 'sector': '新能源'},
|
||||
{'symbol': '002460', 'name': '赣锋锂业', 'sector': '新能源'},
|
||||
{'symbol': '300014', 'name': '亿纬锂能', 'sector': '新能源'},
|
||||
{'symbol': '600884', 'name': '杉杉股份', 'sector': '新能源'},
|
||||
{'symbol': '002812', 'name': '恩捷股份', 'sector': '新能源'},
|
||||
|
||||
# 房地产
|
||||
{'symbol': '000002', 'name': '万科A', 'sector': '房地产'},
|
||||
{'symbol': '000858', 'name': '五粮液', 'sector': '房地产'},
|
||||
{'symbol': '600048', 'name': '保利发展', 'sector': '房地产'},
|
||||
{'symbol': '001979', 'name': '招商蛇口', 'sector': '房地产'},
|
||||
{'symbol': '600606', 'name': '绿地控股', 'sector': '房地产'},
|
||||
|
||||
# 消费股
|
||||
{'symbol': '600887', 'name': '伊利股份', 'sector': '消费'},
|
||||
{'symbol': '000568', 'name': '泸州老窖', 'sector': '消费'},
|
||||
{'symbol': '600600', 'name': '青岛啤酒', 'sector': '消费'},
|
||||
{'symbol': '000895', 'name': '双汇发展', 'sector': '消费'},
|
||||
{'symbol': '002304', 'name': '洋河股份', 'sector': '消费'},
|
||||
{'symbol': '600779', 'name': '水井坊', 'sector': '消费'},
|
||||
|
||||
# 医药股
|
||||
{'symbol': '600196', 'name': '复星医药', 'sector': '医药'},
|
||||
{'symbol': '000661', 'name': '长春高新', 'sector': '医药'},
|
||||
{'symbol': '300015', 'name': '爱尔眼科', 'sector': '医药'},
|
||||
{'symbol': '002821', 'name': '凯莱英', 'sector': '医药'},
|
||||
{'symbol': '300760', 'name': '迈瑞医疗', 'sector': '医药'},
|
||||
{'symbol': '600276', 'name': '恒瑞医药', 'sector': '医药'},
|
||||
|
||||
# 证券股
|
||||
{'symbol': '000776', 'name': '广发证券', 'sector': '证券'},
|
||||
{'symbol': '600030', 'name': '中信证券', 'sector': '证券'},
|
||||
{'symbol': '000166', 'name': '申万宏源', 'sector': '证券'},
|
||||
{'symbol': '601688', 'name': '华泰证券', 'sector': '证券'},
|
||||
{'symbol': '600837', 'name': '海通证券', 'sector': '证券'},
|
||||
|
||||
# 化工股
|
||||
{'symbol': '600309', 'name': '万华化学', 'sector': '化工'},
|
||||
{'symbol': '002352', 'name': '顺丰控股', 'sector': '化工'},
|
||||
{'symbol': '600346', 'name': '恒力石化', 'sector': '化工'},
|
||||
{'symbol': '000792', 'name': '盐湖股份', 'sector': '化工'},
|
||||
|
||||
# 汽车股
|
||||
{'symbol': '600104', 'name': '上汽集团', 'sector': '汽车'},
|
||||
{'symbol': '000625', 'name': '长安汽车', 'sector': '汽车'},
|
||||
{'symbol': '601633', 'name': '长城汽车', 'sector': '汽车'},
|
||||
{'symbol': '002049', 'name': '紫光国微', 'sector': '汽车'},
|
||||
|
||||
# 军工股
|
||||
{'symbol': '002179', 'name': '中航光电', 'sector': '军工'},
|
||||
{'symbol': '600893', 'name': '航发动力', 'sector': '军工'},
|
||||
{'symbol': '000768', 'name': '中航飞机', 'sector': '军工'},
|
||||
|
||||
# 基建股
|
||||
{'symbol': '601186', 'name': '中国铁建', 'sector': '基建'},
|
||||
{'symbol': '601390', 'name': '中国中铁', 'sector': '基建'},
|
||||
{'symbol': '000001', 'name': '平安银行', 'sector': '基建'},
|
||||
|
||||
# 煤炭股
|
||||
{'symbol': '601225', 'name': '陕西煤业', 'sector': '煤炭'},
|
||||
{'symbol': '600188', 'name': '兖矿能源', 'sector': '煤炭'},
|
||||
{'symbol': '601898', 'name': '中煤能源', 'sector': '煤炭'},
|
||||
|
||||
# 钢铁股
|
||||
{'symbol': '000717', 'name': '韶钢松山', 'sector': '钢铁'},
|
||||
{'symbol': '600019', 'name': '宝钢股份', 'sector': '钢铁'},
|
||||
{'symbol': '000708', 'name': '中信特钢', 'sector': '钢铁'},
|
||||
]
|
||||
|
||||
def timeframe_to_period(self, timeframe):
|
||||
"""将时间周期转换为akshare的period参数"""
|
||||
mapping = {
|
||||
'1m': '1', # 1分钟
|
||||
'5m': '5', # 5分钟
|
||||
'15m': '15', # 15分钟
|
||||
'30m': '30', # 30分钟
|
||||
'1h': '60', # 60分钟
|
||||
'1d': 'daily', # 日线
|
||||
'1w': 'weekly',# 周线
|
||||
'1M': 'monthly'# 月线
|
||||
}
|
||||
return mapping.get(timeframe, 'daily')
|
||||
|
||||
def get_kl_data(self, symbol, timeframe='1d', start_date=None, end_date=None, limit=1000):
|
||||
"""
|
||||
获取A股K线数据 - 支持分批次获取突破单次限制
|
||||
:param symbol: 股票代码,如 '000001'
|
||||
:param timeframe: 时间周期,如 '1d', '1h', '5m'
|
||||
:param start_date: 开始日期,格式 'YYYY-MM-DD'
|
||||
:param end_date: 结束日期,格式 'YYYY-MM-DD'
|
||||
:param limit: 数据条数限制
|
||||
:return: DataFrame
|
||||
"""
|
||||
try:
|
||||
period = self.timeframe_to_period(timeframe)
|
||||
|
||||
# 处理时间参数
|
||||
if start_date is None:
|
||||
# 默认获取最近一年的数据
|
||||
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
|
||||
else:
|
||||
# 将 YYYY-MM-DD 格式转换为 YYYYMMDD
|
||||
if '-' in start_date:
|
||||
start_date = start_date.replace('-', '')
|
||||
|
||||
if end_date is None:
|
||||
end_date = datetime.now().strftime('%Y%m%d')
|
||||
else:
|
||||
if '-' in end_date:
|
||||
end_date = end_date.replace('-', '')
|
||||
|
||||
print(f"获取A股数据: {symbol}, 周期: {timeframe}, 开始: {start_date}, 结束: {end_date}")
|
||||
|
||||
# 分批次获取数据以突破单次限制
|
||||
all_data = []
|
||||
current_start = start_date
|
||||
|
||||
# 计算时间间隔(根据时间周期调整批次大小)
|
||||
if period in ['1', '5', '15', '30']:
|
||||
# 分钟级数据,每次获取7天
|
||||
batch_days = 7
|
||||
elif period == '60':
|
||||
# 小时级数据,每次获取30天
|
||||
batch_days = 30
|
||||
else:
|
||||
# 日线及以上,每次获取365天
|
||||
batch_days = 365
|
||||
|
||||
max_iterations = 20 # 最大迭代次数,防止无限循环
|
||||
iteration_count = 0
|
||||
|
||||
while current_start <= end_date and iteration_count < max_iterations:
|
||||
iteration_count += 1
|
||||
|
||||
# 计算当前批次的结束时间
|
||||
current_start_dt = datetime.strptime(current_start, '%Y%m%d')
|
||||
current_end_dt = current_start_dt + timedelta(days=batch_days)
|
||||
current_end = min(current_end_dt.strftime('%Y%m%d'), end_date)
|
||||
|
||||
print(f"批次 {iteration_count}: 获取 {current_start} 到 {current_end} 的数据")
|
||||
|
||||
try:
|
||||
# 根据时间周期选择不同的API
|
||||
df_batch = None
|
||||
if period in ['1', '5', '15', '30', '60']:
|
||||
# 分钟级数据
|
||||
df_batch = ak.stock_zh_a_hist_min_em(symbol=symbol, period=period,
|
||||
start_date=current_start, end_date=current_end)
|
||||
if df_batch is not None and len(df_batch) > 0:
|
||||
# 重命名列
|
||||
df_batch = df_batch.rename(columns={
|
||||
'时间': 'date',
|
||||
'开盘': 'open',
|
||||
'收盘': 'close',
|
||||
'最高': 'high',
|
||||
'最低': 'low',
|
||||
'成交量': 'volume'
|
||||
})
|
||||
else:
|
||||
# 日线、周线、月线数据
|
||||
df_batch = ak.stock_zh_a_hist(symbol=symbol, period=period,
|
||||
start_date=current_start, end_date=current_end)
|
||||
if df_batch is not None and len(df_batch) > 0:
|
||||
# 重命名列
|
||||
df_batch = df_batch.rename(columns={
|
||||
'日期': 'date',
|
||||
'开盘': 'open',
|
||||
'收盘': 'close',
|
||||
'最高': 'high',
|
||||
'最低': 'low',
|
||||
'成交量': 'volume'
|
||||
})
|
||||
|
||||
if df_batch is not None and len(df_batch) > 0:
|
||||
# 转换时间格式
|
||||
df_batch['date'] = pd.to_datetime(df_batch['date'])
|
||||
|
||||
# 根据A股交易时间调整时间戳
|
||||
df_batch = self.adjust_timestamp_for_trading_hours(df_batch, timeframe)
|
||||
|
||||
all_data.append(df_batch)
|
||||
print(f"批次 {iteration_count}: 获取到 {len(df_batch)} 条记录")
|
||||
else:
|
||||
print(f"批次 {iteration_count}: 未获取到数据")
|
||||
|
||||
except Exception as e:
|
||||
print(f"批次 {iteration_count} 获取失败: {e}")
|
||||
# 继续下一个批次
|
||||
|
||||
# 更新下一批次的开始时间
|
||||
current_start = (current_end_dt + timedelta(days=1)).strftime('%Y%m%d')
|
||||
|
||||
# 防止API请求过于频繁
|
||||
time_module.sleep(0.5)
|
||||
|
||||
# 合并所有批次的数据
|
||||
if not all_data:
|
||||
print(f"未获取到任何数据: {symbol}")
|
||||
return None
|
||||
|
||||
# 合并DataFrame
|
||||
df = pd.concat(all_data, ignore_index=True)
|
||||
|
||||
# 数据清洗和格式化
|
||||
df = df.dropna() # 删除空值
|
||||
df = df.drop_duplicates(subset=['date']) # 删除重复数据
|
||||
df = df.sort_values('date').reset_index(drop=True) # 按时间排序
|
||||
|
||||
# A股特有的数据清理和时间处理
|
||||
df = self.clean_a_stock_data(df, timeframe)
|
||||
|
||||
# 限制数据条数 - 只有在没有指定明确时间范围时才应用
|
||||
# 如果用户指定了start_date和end_date,应该返回该时间范围内的所有数据
|
||||
if limit is not None and len(df) > limit:
|
||||
# 检查是否指定了明确的时间范围
|
||||
if start_date and end_date:
|
||||
# 如果指定了时间范围,优先返回完整的时间范围数据
|
||||
print(f"用户指定了时间范围 {start_date} 到 {end_date},返回完整数据 {len(df)} 条记录")
|
||||
if len(df) > 10000: # 防止数据量过大,设置一个合理的上限
|
||||
print(f"警告:数据量过大({len(df)}条),为保证性能将限制为最新的10000条记录")
|
||||
df = df.tail(10000).reset_index(drop=True)
|
||||
else:
|
||||
# 如果没有指定时间范围,使用默认的limit限制
|
||||
print(f"未指定明确时间范围,应用默认限制,返回最新的 {limit} 条记录")
|
||||
df = df.tail(limit).reset_index(drop=True)
|
||||
elif limit is None and len(df) > 10000:
|
||||
# 即使没有limit限制,也要防止数据量过大影响性能
|
||||
print(f"无limit限制但数据量过大({len(df)}条),为保证性能将限制为最新的10000条记录")
|
||||
df = df.tail(10000).reset_index(drop=True)
|
||||
|
||||
# 添加技术指标
|
||||
df = self.add_indicators(df)
|
||||
|
||||
# 最终数据验证 - 确保没有NaN值
|
||||
import numpy as np
|
||||
|
||||
# 检查并处理任何剩余的NaN值
|
||||
if df.isnull().any().any():
|
||||
print("警告:发现NaN值,正在清理...")
|
||||
# 对于数值列,用0填充NaN
|
||||
numeric_cols = df.select_dtypes(include=[np.number]).columns
|
||||
for col in numeric_cols:
|
||||
if col in ['volume_ratio']:
|
||||
df[col] = df[col].fillna(1.0)
|
||||
else:
|
||||
df[col] = df[col].fillna(0)
|
||||
|
||||
# 删除仍然包含NaN的行
|
||||
df = df.dropna()
|
||||
|
||||
# 确保所有数值都是有限的
|
||||
for col in df.select_dtypes(include=[np.number]).columns:
|
||||
df[col] = df[col].replace([np.inf, -np.inf], 0 if col != 'volume_ratio' else 1.0)
|
||||
|
||||
print(f"成功获取A股数据: {len(df)} 条记录 (共 {len(all_data)} 个批次)")
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取A股数据失败: {e}")
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def add_indicators(self, df):
|
||||
"""添加技术指标"""
|
||||
try:
|
||||
import talib.abstract as ta
|
||||
import numpy as np
|
||||
|
||||
# MACD指标
|
||||
fast = 8
|
||||
slow = 16
|
||||
period = 6
|
||||
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
||||
|
||||
df['macd'] = macd['macd'].fillna(0)
|
||||
df['macdsignal'] = macd['macdsignal'].fillna(0)
|
||||
df['macdhist'] = macd['macdhist'].fillna(0)
|
||||
|
||||
# 移动平均线
|
||||
df['ma5'] = ta.MA(df, timeperiod=5).fillna(0)
|
||||
df['ma10'] = ta.MA(df, timeperiod=10).fillna(0)
|
||||
df['ma30'] = ta.EMA(df, timeperiod=30).fillna(0)
|
||||
df['ma250'] = ta.MA(df, timeperiod=250).fillna(0)
|
||||
|
||||
# RSI指标
|
||||
df['rsi'] = ta.RSI(df, timeperiod=14).fillna(0)
|
||||
|
||||
# 成交量指标
|
||||
df['avg_volume'] = df['volume'].rolling(10).mean().fillna(0)
|
||||
df['volume_ratio'] = (df['volume'] / df['avg_volume']).fillna(1.0)
|
||||
|
||||
# 处理Infinity和-Infinity值
|
||||
df['volume_ratio'] = df['volume_ratio'].replace([float('inf'), float('-inf')], 1.0)
|
||||
|
||||
# 确保所有指标列都不包含NaN或无限值
|
||||
indicator_columns = ['macd', 'macdsignal', 'macdhist', 'ma5', 'ma10', 'ma30', 'ma250', 'rsi', 'avg_volume', 'volume_ratio']
|
||||
for col in indicator_columns:
|
||||
if col in df.columns:
|
||||
# 替换NaN、inf、-inf为合理的默认值
|
||||
df[col] = df[col].replace([np.nan, np.inf, -np.inf], 0 if col != 'volume_ratio' else 1.0)
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
print(f"添加指标失败: {e}")
|
||||
return df
|
||||
|
||||
def search_stock(self, keyword):
|
||||
"""搜索股票 - 支持代码和名称模糊搜索"""
|
||||
try:
|
||||
if not keyword or len(keyword.strip()) == 0:
|
||||
return []
|
||||
|
||||
keyword = keyword.strip().upper()
|
||||
results = []
|
||||
|
||||
# 从热门股票中搜索
|
||||
popular_stocks = self.get_popular_stocks()
|
||||
for stock in popular_stocks:
|
||||
if (keyword in stock['symbol'] or
|
||||
keyword.lower() in stock['name'].lower() or
|
||||
stock['symbol'].startswith(keyword)):
|
||||
results.append({
|
||||
'symbol': stock['symbol'],
|
||||
'name': stock['name'],
|
||||
'sector': stock.get('sector', ''),
|
||||
'source': '热门股票'
|
||||
})
|
||||
|
||||
# 如果热门股票中找到的结果少于10个,从完整股票列表中搜索
|
||||
if len(results) < 10:
|
||||
try:
|
||||
# 获取完整股票列表进行搜索
|
||||
stock_info = ak.stock_zh_a_spot_em()
|
||||
|
||||
# 搜索前1000只活跃股票
|
||||
for index, row in stock_info.head(1000).iterrows():
|
||||
stock_code = str(row['代码'])
|
||||
stock_name = str(row['名称'])
|
||||
|
||||
# 过滤ST股票
|
||||
if 'ST' in stock_name or '*' in stock_name:
|
||||
continue
|
||||
|
||||
# 检查是否已经在结果中
|
||||
if any(r['symbol'] == stock_code for r in results):
|
||||
continue
|
||||
|
||||
# 搜索匹配
|
||||
if (keyword in stock_code or
|
||||
keyword.lower() in stock_name.lower() or
|
||||
stock_code.startswith(keyword)):
|
||||
results.append({
|
||||
'symbol': stock_code,
|
||||
'name': stock_name,
|
||||
'price': float(row['最新价']) if pd.notna(row['最新价']) else 0.0,
|
||||
'change_pct': float(row['涨跌幅']) if pd.notna(row['涨跌幅']) else 0.0,
|
||||
'source': '全市场搜索'
|
||||
})
|
||||
|
||||
# 限制结果数量
|
||||
if len(results) >= 30:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"全市场搜索失败: {e}")
|
||||
|
||||
# 排序:优先显示代码匹配的结果
|
||||
def sort_key(item):
|
||||
if item['symbol'].startswith(keyword):
|
||||
return (0, item['symbol']) # 代码开头匹配优先级最高
|
||||
elif keyword in item['symbol']:
|
||||
return (1, item['symbol']) # 代码包含匹配次之
|
||||
else:
|
||||
return (2, item['symbol']) # 名称匹配最后
|
||||
|
||||
results.sort(key=sort_key)
|
||||
|
||||
# 限制返回结果数量
|
||||
return results[:20]
|
||||
|
||||
except Exception as e:
|
||||
print(f"搜索股票失败: {e}")
|
||||
return []
|
||||
|
||||
def get_stock_by_sector(self, sector=None):
|
||||
"""根据行业获取股票列表"""
|
||||
try:
|
||||
popular_stocks = self.get_popular_stocks()
|
||||
if sector:
|
||||
return [stock for stock in popular_stocks if stock.get('sector', '') == sector]
|
||||
else:
|
||||
# 按行业分组
|
||||
sectors = {}
|
||||
for stock in popular_stocks:
|
||||
sector_name = stock.get('sector', '其他')
|
||||
if sector_name not in sectors:
|
||||
sectors[sector_name] = []
|
||||
sectors[sector_name].append(stock)
|
||||
return sectors
|
||||
except Exception as e:
|
||||
print(f"获取行业股票失败: {e}")
|
||||
return {} if sector is None else []
|
||||
|
||||
def get_all_sectors(self):
|
||||
"""获取所有行业分类"""
|
||||
try:
|
||||
popular_stocks = self.get_popular_stocks()
|
||||
sectors = set()
|
||||
for stock in popular_stocks:
|
||||
sector = stock.get('sector', '其他')
|
||||
sectors.add(sector)
|
||||
return sorted(list(sectors))
|
||||
except Exception as e:
|
||||
print(f"获取行业分类失败: {e}")
|
||||
return []
|
||||
|
||||
def is_trading_day(self, date):
|
||||
"""判断是否为交易日(排除周末和节假日)"""
|
||||
try:
|
||||
# 将日期转换为datetime对象
|
||||
if isinstance(date, str):
|
||||
date = datetime.strptime(date.split()[0], '%Y-%m-%d')
|
||||
elif isinstance(date, pd.Timestamp):
|
||||
date = date.to_pydatetime()
|
||||
|
||||
# 周末不是交易日
|
||||
if date.weekday() >= 5: # 5=周六, 6=周日
|
||||
return False
|
||||
|
||||
# 这里可以进一步添加节假日判断
|
||||
# 目前暂时只过滤周末
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"判断交易日失败: {e}")
|
||||
return True # 默认返回True,避免过度过滤
|
||||
|
||||
def is_trading_time(self, dt):
|
||||
"""判断是否为交易时间"""
|
||||
try:
|
||||
if isinstance(dt, str):
|
||||
dt = pd.to_datetime(dt)
|
||||
|
||||
time_str = dt.strftime('%H:%M')
|
||||
|
||||
# 上午交易时间:09:30-11:30
|
||||
morning_start = self.trading_hours['morning']['start']
|
||||
morning_end = self.trading_hours['morning']['end']
|
||||
|
||||
# 下午交易时间:13:00-15:00
|
||||
afternoon_start = self.trading_hours['afternoon']['start']
|
||||
afternoon_end = self.trading_hours['afternoon']['end']
|
||||
|
||||
return ((morning_start <= time_str <= morning_end) or
|
||||
(afternoon_start <= time_str <= afternoon_end))
|
||||
except Exception as e:
|
||||
print(f"判断交易时间失败: {e}")
|
||||
return True # 默认返回True,避免过度过滤
|
||||
|
||||
def adjust_timestamp_for_trading_hours(self, df, timeframe):
|
||||
"""根据A股交易时间调整时间戳"""
|
||||
try:
|
||||
if df is None or len(df) == 0:
|
||||
return df
|
||||
|
||||
# 确保date列是datetime类型
|
||||
if 'date' in df.columns:
|
||||
df['date'] = pd.to_datetime(df['date'])
|
||||
|
||||
# 对于日线数据,设置为收盘时间(15:00)
|
||||
if timeframe == '1d':
|
||||
df['date'] = df['date'].dt.normalize() + pd.Timedelta(hours=15)
|
||||
|
||||
# 对于分钟级数据,过滤非交易时间的数据
|
||||
elif timeframe in ['1m', '5m', '15m', '30m', '1h']:
|
||||
# 过滤交易日
|
||||
df = df[df['date'].apply(self.is_trading_day)]
|
||||
|
||||
# 过滤交易时间(只在有足够数据时进行)
|
||||
if len(df) > 10: # 避免过度过滤导致数据不足
|
||||
df = df[df['date'].apply(self.is_trading_time)]
|
||||
|
||||
# 重新计算时间戳
|
||||
if 'date' in df.columns:
|
||||
# 将时间转换为上海时区
|
||||
df['date'] = df['date'].dt.tz_localize('Asia/Shanghai', ambiguous='infer', nonexistent='shift_forward')
|
||||
# 转换为毫秒时间戳
|
||||
df['timestamp'] = df['date'].astype('int64') // 10**6
|
||||
|
||||
return df.reset_index(drop=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"调整A股时间戳失败: {e}")
|
||||
traceback.print_exc()
|
||||
return df
|
||||
|
||||
def get_trading_calendar(self, start_date, end_date):
|
||||
"""获取交易日历(简化版本)"""
|
||||
try:
|
||||
# 使用akshare获取交易日历
|
||||
trading_calendar = ak.tool_trade_date_hist_sina()
|
||||
|
||||
# 过滤指定日期范围
|
||||
start_dt = pd.to_datetime(start_date)
|
||||
end_dt = pd.to_datetime(end_date)
|
||||
|
||||
trading_days = []
|
||||
for _, row in trading_calendar.iterrows():
|
||||
trade_date = pd.to_datetime(row['trade_date'])
|
||||
if start_dt <= trade_date <= end_dt:
|
||||
trading_days.append(trade_date.strftime('%Y-%m-%d'))
|
||||
|
||||
return trading_days
|
||||
except Exception as e:
|
||||
print(f"获取交易日历失败: {e}")
|
||||
# 如果获取失败,生成简单的工作日列表(排除周末)
|
||||
trading_days = []
|
||||
current = pd.to_datetime(start_date)
|
||||
end = pd.to_datetime(end_date)
|
||||
|
||||
while current <= end:
|
||||
if current.weekday() < 5: # 周一到周五
|
||||
trading_days.append(current.strftime('%Y-%m-%d'))
|
||||
current += timedelta(days=1)
|
||||
|
||||
return trading_days
|
||||
|
||||
def fill_trading_gaps(self, df, timeframe):
|
||||
"""填补A股交易时间间隙,确保图表连续性"""
|
||||
try:
|
||||
if df is None or len(df) == 0:
|
||||
return df
|
||||
|
||||
# 对于日线数据,不需要填补间隙,因为本来就是每日一个数据点
|
||||
if timeframe == '1d':
|
||||
return df
|
||||
|
||||
# 对于分钟级数据,创建完整的交易时间序列
|
||||
if timeframe in ['1m', '5m', '15m', '30m', '1h']:
|
||||
# 获取数据的开始和结束时间
|
||||
start_date = df['date'].min().date()
|
||||
end_date = df['date'].max().date()
|
||||
|
||||
# 创建完整的交易时间序列
|
||||
complete_times = []
|
||||
current_date = start_date
|
||||
|
||||
# 获取时间间隔(分钟)
|
||||
freq_map = {'1m': 1, '5m': 5, '15m': 15, '30m': 30, '1h': 60}
|
||||
freq_minutes = freq_map.get(timeframe, 5)
|
||||
|
||||
while current_date <= end_date:
|
||||
# 只处理交易日
|
||||
if self.is_trading_day(current_date):
|
||||
# 上午交易时间 - 使用datetime.time而不是pd.Time
|
||||
morning_start = pd.Timestamp.combine(current_date, time(9, 30))
|
||||
morning_end = pd.Timestamp.combine(current_date, time(11, 30))
|
||||
|
||||
# 下午交易时间
|
||||
afternoon_start = pd.Timestamp.combine(current_date, time(13, 0))
|
||||
afternoon_end = pd.Timestamp.combine(current_date, time(15, 0))
|
||||
|
||||
# 生成上午时间序列
|
||||
current_time = morning_start
|
||||
while current_time <= morning_end:
|
||||
complete_times.append(current_time)
|
||||
current_time += pd.Timedelta(minutes=freq_minutes)
|
||||
|
||||
# 生成下午时间序列
|
||||
current_time = afternoon_start
|
||||
while current_time <= afternoon_end:
|
||||
complete_times.append(current_time)
|
||||
current_time += pd.Timedelta(minutes=freq_minutes)
|
||||
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
# 创建完整时间序列的DataFrame
|
||||
if complete_times:
|
||||
complete_df = pd.DataFrame({'date': complete_times})
|
||||
complete_df['date'] = complete_df['date'].dt.tz_localize('Asia/Shanghai')
|
||||
complete_df['timestamp'] = complete_df['date'].astype('int64') // 10**6
|
||||
|
||||
# 将原始数据合并到完整时间序列
|
||||
# 使用时间戳进行合并,避免时区问题
|
||||
df_merged = pd.merge(complete_df, df, on='timestamp', how='left', suffixes=('', '_orig'))
|
||||
|
||||
# 保持原有date列
|
||||
df_merged['date'] = df_merged['date']
|
||||
|
||||
# 对于缺失的OHLCV数据,使用前向填充
|
||||
price_cols = ['open', 'high', 'low', 'close']
|
||||
for col in price_cols:
|
||||
if col in df_merged.columns:
|
||||
df_merged[col] = df_merged[col].ffill()
|
||||
|
||||
# 成交量缺失时设为0
|
||||
if 'volume' in df_merged.columns:
|
||||
df_merged['volume'] = df_merged['volume'].fillna(0)
|
||||
|
||||
# 删除辅助列
|
||||
cols_to_drop = [col for col in df_merged.columns if col.endswith('_orig')]
|
||||
df_merged = df_merged.drop(columns=cols_to_drop)
|
||||
|
||||
return df_merged
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
print(f"填补交易时间间隙失败: {e}")
|
||||
traceback.print_exc()
|
||||
return df
|
||||
|
||||
def clean_a_stock_data(self, df, timeframe):
|
||||
"""清理A股数据,处理异常值和时间问题"""
|
||||
try:
|
||||
if df is None or len(df) == 0:
|
||||
return df
|
||||
|
||||
import numpy as np
|
||||
|
||||
# 首先删除所有包含NaN的行
|
||||
df = df.dropna()
|
||||
|
||||
# 删除价格异常的数据
|
||||
price_cols = ['open', 'high', 'low', 'close']
|
||||
for col in price_cols:
|
||||
if col in df.columns:
|
||||
# 删除价格为0、负数、NaN、inf的记录
|
||||
df = df[df[col] > 0]
|
||||
df = df[np.isfinite(df[col])]
|
||||
|
||||
# 检查OHLC逻辑合理性
|
||||
if all(col in df.columns for col in price_cols):
|
||||
# high应该是最高价
|
||||
df = df[df['high'] >= df['open']]
|
||||
df = df[df['high'] >= df['close']]
|
||||
# low应该是最低价
|
||||
df = df[df['low'] <= df['open']]
|
||||
df = df[df['low'] <= df['close']]
|
||||
# high应该大于等于low
|
||||
df = df[df['high'] >= df['low']]
|
||||
|
||||
# 删除成交量异常的数据
|
||||
if 'volume' in df.columns:
|
||||
# 删除成交量为负数、NaN、inf的记录
|
||||
df = df[df['volume'] >= 0]
|
||||
df = df[np.isfinite(df['volume'])]
|
||||
|
||||
# 确保所有数值列都不包含NaN或无限值
|
||||
numeric_cols = df.select_dtypes(include=[np.number]).columns
|
||||
for col in numeric_cols:
|
||||
# 替换NaN、inf、-inf为0(除了价格列,价格列的异常值已经被过滤掉了)
|
||||
if col not in price_cols:
|
||||
df[col] = df[col].replace([np.nan, np.inf, -np.inf], 0)
|
||||
|
||||
# 确保时间序列连续性(仅对分钟级数据)
|
||||
if timeframe in ['1m', '5m', '15m', '30m', '1h']:
|
||||
df = self.fill_trading_gaps(df, timeframe)
|
||||
|
||||
# 最后再次检查并清理任何剩余的NaN值
|
||||
df = df.dropna()
|
||||
|
||||
return df.reset_index(drop=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"清理A股数据失败: {e}")
|
||||
return df
|
||||
@@ -10,4 +10,5 @@ xgboost>=1.5.0
|
||||
scikit-learn>=1.0.1
|
||||
ta-lib>=0.4.19
|
||||
bootstrap-flask>=2.0.0
|
||||
gunicorn>=20.1.0
|
||||
gunicorn>=20.1.0
|
||||
akshare>=1.12.0
|
||||
+465
-164
@@ -250,7 +250,14 @@
|
||||
|
||||
<div class="controls">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-2">
|
||||
<label for="dataSource" class="form-label">数据源:</label>
|
||||
<select id="dataSource" class="form-select">
|
||||
<option value="crypto" selected>加密货币</option>
|
||||
<option value="a_stock">A股</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3" id="cryptoSymbolContainer">
|
||||
<label for="symbol" class="form-label">交易对:</label>
|
||||
<select id="symbol" class="form-select">
|
||||
{% for symbol in symbols %}
|
||||
@@ -258,6 +265,14 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3" id="astockSymbolContainer" style="display:none;">
|
||||
<label for="astockSymbol" class="form-label">A股股票:</label>
|
||||
<select id="astockSymbol" class="form-select">
|
||||
{% for stock in a_stock_symbols %}
|
||||
<option value="{{ stock.symbol }}" {% if stock.symbol == '000001' %}selected{% endif %}>{{ stock.symbol }} - {{ stock.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label for="timeframe" class="form-label">时间周期:</label>
|
||||
<select id="timeframe" class="form-select">
|
||||
@@ -277,11 +292,11 @@
|
||||
<option value="Asia/Tokyo">Asia/Tokyo (UTC+9)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-1">
|
||||
<label for="start_time" class="form-label">开始时间:</label>
|
||||
<input type="datetime-local" id="start_time" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-1">
|
||||
<label for="end_time" class="form-label">结束时间:</label>
|
||||
<input type="datetime-local" id="end_time" class="form-control">
|
||||
</div>
|
||||
@@ -1094,17 +1109,25 @@
|
||||
$('#refreshLoadingSpinner').show();
|
||||
|
||||
// 获取参数
|
||||
const symbol = $('#symbol').val() || 'BTC/USDT:USDT';
|
||||
const dataSource = $('#dataSource').val() || 'crypto';
|
||||
let symbol;
|
||||
if (dataSource === 'crypto') {
|
||||
symbol = $('#symbol').val() || 'BTC/USDT:USDT';
|
||||
} else {
|
||||
symbol = $('#astockSymbol').val() || '000001';
|
||||
}
|
||||
|
||||
const timeframe = $('#timeframe').val() || '5m';
|
||||
const timezone = $('#timezone').val() || 'Asia/Shanghai';
|
||||
const elementTimeframe = $('#elementTimeframe').val() || '1m';
|
||||
|
||||
// 确保时区参数有效
|
||||
console.log('更新图表使用时区:', timezone);
|
||||
console.log('数据源:', dataSource, '交易对/股票:', symbol);
|
||||
|
||||
// 如果symbol为空,不发送请求
|
||||
if (!symbol) {
|
||||
console.error('交易对不能为空');
|
||||
console.error('交易对/股票代码不能为空');
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
return;
|
||||
}
|
||||
@@ -1162,7 +1185,75 @@
|
||||
// 初始化图表
|
||||
function initTradingView(symbol, timeframe) {
|
||||
try {
|
||||
console.log('初始化图表:', symbol, timeframe);
|
||||
console.log('初始化TradingView图表:', symbol, timeframe);
|
||||
|
||||
// 获取当前交易对的配置
|
||||
const symbolConfig = getSymbolConfig(symbol);
|
||||
console.log('交易对配置:', symbolConfig);
|
||||
|
||||
// 检查数据是否存在
|
||||
if (!currentData || !currentData.kline_data) {
|
||||
console.error('数据加载失败或不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否使用小周期K线数据
|
||||
const useElementPeriod = $('#useElementPeriod').is(':checked') &&
|
||||
currentData.element_kline_data &&
|
||||
Array.isArray(currentData.element_kline_data);
|
||||
|
||||
// 输出K线周期选择状态
|
||||
console.log('K线周期选择:', useElementPeriod ? '小周期' : '主周期');
|
||||
console.log('当前选择时区:', $('#timezone').val());
|
||||
console.log('交易对类型:', symbolConfig.type);
|
||||
|
||||
let candles = [];
|
||||
|
||||
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),
|
||||
high: parseFloat(kline.high),
|
||||
low: parseFloat(kline.low),
|
||||
close: parseFloat(kline.close),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// 使用主周期K线数据 - 检查数据是否存在
|
||||
if (!currentData.kline_data || !Array.isArray(currentData.kline_data)) {
|
||||
console.error('主周期K线数据不存在或不是数组:', currentData.kline_data);
|
||||
return;
|
||||
}
|
||||
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),
|
||||
high: parseFloat(kline.high),
|
||||
low: parseFloat(kline.low),
|
||||
close: parseFloat(kline.close),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 根据交易对类型过滤数据(仅用于显示优化)
|
||||
if (symbolConfig.type === 'a_stock' && timeframe.includes('m')) {
|
||||
// 对于A股分钟级数据,过滤非交易时间
|
||||
const originalLength = candles.length;
|
||||
candles = filterTradingHours(candles, symbolConfig);
|
||||
console.log(`A股数据过滤: ${originalLength} -> ${candles.length} 条记录`);
|
||||
}
|
||||
|
||||
// 清除图表容器
|
||||
document.getElementById('tradingview_chart').innerHTML = '';
|
||||
@@ -1253,96 +1344,129 @@
|
||||
let syncInProgress = false;
|
||||
|
||||
// 创建统一的图表选项
|
||||
const createChartOptions = (showTimeScale = true) => ({
|
||||
width: mainChartContainer.clientWidth,
|
||||
height: mainChartContainer.clientHeight,
|
||||
layout: {
|
||||
background: { color: '#ffffff' },
|
||||
textColor: '#333',
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: '#f0f0f0' },
|
||||
horzLines: { color: '#f0f0f0' },
|
||||
},
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode.Normal,
|
||||
// 添加十字线工具提示本地化配置
|
||||
horzLine: {
|
||||
labelVisible: true,
|
||||
const createChartOptions = (showTimeScale = true) => {
|
||||
const baseOptions = {
|
||||
width: mainChartContainer.clientWidth,
|
||||
height: mainChartContainer.clientHeight,
|
||||
layout: {
|
||||
background: { color: '#ffffff' },
|
||||
textColor: '#333',
|
||||
},
|
||||
vertLine: {
|
||||
labelVisible: true,
|
||||
// 自定义时间格式化
|
||||
labelFormatter: (time) => {
|
||||
grid: {
|
||||
vertLines: { color: '#f0f0f0' },
|
||||
horzLines: { color: '#f0f0f0' },
|
||||
},
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode.Normal,
|
||||
// 添加十字线工具提示本地化配置
|
||||
horzLine: {
|
||||
labelVisible: true,
|
||||
},
|
||||
vertLine: {
|
||||
labelVisible: true,
|
||||
// 自定义时间格式化
|
||||
labelFormatter: (time) => {
|
||||
const selectedTimezone = $('#timezone').val();
|
||||
try {
|
||||
const date = new Date(time * 1000);
|
||||
if (symbolConfig.type === 'a_stock') {
|
||||
// A股使用中国时区格式
|
||||
return date.toLocaleString('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
} else {
|
||||
return date.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', {
|
||||
const date = new Date(time * 1000);
|
||||
if (symbolConfig.type === 'a_stock') {
|
||||
// A股使用中国时区格式
|
||||
return date.toLocaleString('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
} else {
|
||||
return date.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,
|
||||
visible: showTimeScale,
|
||||
borderColor: '#ddd',
|
||||
barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10,
|
||||
tickMarkFormatter: (time) => {
|
||||
const selectedTimezone = symbolConfig.type === 'a_stock' ? 'Asia/Shanghai' : $('#timezone').val();
|
||||
try {
|
||||
// 使用完整的配置确保时区正确应用
|
||||
const date = new Date(time * 1000);
|
||||
console.log('格式化时间:', time, '转换为:', date.toISOString(), '时区:', selectedTimezone);
|
||||
|
||||
return date.toLocaleString('zh-CN', {
|
||||
timeZone: selectedTimezone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('十字线时间格式化错误:', 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,
|
||||
visible: showTimeScale,
|
||||
borderColor: '#ddd',
|
||||
barSpacing: 10,
|
||||
tickMarkFormatter: (time) => {
|
||||
const selectedTimezone = $('#timezone').val();
|
||||
try {
|
||||
// 使用完整的配置确保时区正确应用
|
||||
const date = new Date(time * 1000);
|
||||
console.log('格式化时间:', time, '转换为:', date.toISOString(), '时区:', selectedTimezone);
|
||||
|
||||
return date.toLocaleString('zh-CN', {
|
||||
timeZone: selectedTimezone,
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('时间格式化错误:', e);
|
||||
// 如果时区格式化失败,返回简单格式
|
||||
return new Date(time * 1000).toLocaleString();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 根据交易对类型调整配置
|
||||
return adjustChartForSymbolType(baseOptions, symbolConfig);
|
||||
};
|
||||
|
||||
// 创建主图表
|
||||
const mainChart = LightweightCharts.createChart(mainChartContainer, createChartOptions(true));
|
||||
@@ -1356,50 +1480,6 @@
|
||||
macdChart = LightweightCharts.createChart(macdChartContainer, createChartOptions(false));
|
||||
}
|
||||
|
||||
// 转换K线数据 - 根据选中的周期使用主周期或小周期数据
|
||||
let candles;
|
||||
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
|
||||
currentData.element_timeframe &&
|
||||
currentData.element_kline_data;
|
||||
|
||||
// 输出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),
|
||||
high: parseFloat(kline.high),
|
||||
low: parseFloat(kline.low),
|
||||
close: parseFloat(kline.close),
|
||||
};
|
||||
});
|
||||
} 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),
|
||||
high: parseFloat(kline.high),
|
||||
low: parseFloat(kline.low),
|
||||
close: parseFloat(kline.close),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 创建蜡烛图系列并设置数据
|
||||
if (showOriginalKline) {
|
||||
const candleSeries = mainChart.addCandlestickSeries({
|
||||
@@ -1432,17 +1512,9 @@
|
||||
}
|
||||
|
||||
// 转换成交量数据 - 始终使用主K线周期数据
|
||||
const volumes = useElementPeriod ?
|
||||
currentData.element_kline_data.map(kline => {
|
||||
// 创建日期对象并获取时间戳,不手动调整时区
|
||||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||||
return {
|
||||
time: timestamp,
|
||||
value: parseFloat(kline.volume),
|
||||
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
|
||||
};
|
||||
}) :
|
||||
currentData.kline_data.map(kline => {
|
||||
let volumes = [];
|
||||
if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) {
|
||||
volumes = currentData.element_kline_data.map(kline => {
|
||||
// 创建日期对象并获取时间戳,不手动调整时区
|
||||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||||
return {
|
||||
@@ -1451,6 +1523,17 @@
|
||||
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
|
||||
};
|
||||
});
|
||||
} else if (currentData.kline_data && Array.isArray(currentData.kline_data)) {
|
||||
volumes = currentData.kline_data.map(kline => {
|
||||
// 创建日期对象并获取时间戳,不手动调整时区
|
||||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||||
return {
|
||||
time: timestamp,
|
||||
value: parseFloat(kline.volume),
|
||||
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 添加成交量图表
|
||||
const volumeSeries = volumeChart.addHistogramSeries({
|
||||
@@ -1464,7 +1547,7 @@
|
||||
tvWidget.series.volumeSeries = volumeSeries;
|
||||
|
||||
// 添加MACD图表 - 始终使用主K线周期的MACD数据
|
||||
if (showMacd && currentData.macd) {
|
||||
if (showMacd && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data)) {
|
||||
// 创建MACD线
|
||||
const macdLineSeries = macdChart.addLineSeries({
|
||||
color: '#2962FF',
|
||||
@@ -2945,6 +3028,12 @@
|
||||
try {
|
||||
console.log('增量更新图表数据');
|
||||
|
||||
// 检查 currentData 是否存在
|
||||
if (!currentData) {
|
||||
console.error('currentData为空,无法更新图表');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存当前的可视范围
|
||||
if (tvWidget.mainChart) {
|
||||
tvWidget.state.visibleRange = tvWidget.mainChart.timeScale().getVisibleRange();
|
||||
@@ -2957,10 +3046,11 @@
|
||||
// 检查是否使用小周期数据
|
||||
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
|
||||
currentData.element_timeframe &&
|
||||
currentData.element_kline_data;
|
||||
currentData.element_kline_data &&
|
||||
Array.isArray(currentData.element_kline_data);
|
||||
|
||||
// 转换K线数据
|
||||
let candles;
|
||||
let candles = [];
|
||||
if (useElementPeriod) {
|
||||
console.log('使用小周期K线数据');
|
||||
candles = currentData.element_kline_data.map((kline) => {
|
||||
@@ -2974,7 +3064,7 @@
|
||||
close: parseFloat(kline.close),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
} else if (currentData.kline_data && Array.isArray(currentData.kline_data)) {
|
||||
console.log('使用主周期K线数据');
|
||||
candles = currentData.kline_data.map((kline) => {
|
||||
const date = new Date(kline.date);
|
||||
@@ -3003,16 +3093,9 @@
|
||||
}
|
||||
|
||||
// 更新成交量数据
|
||||
const volumes = useElementPeriod ?
|
||||
currentData.element_kline_data.map(kline => {
|
||||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||||
return {
|
||||
time: timestamp,
|
||||
value: parseFloat(kline.volume),
|
||||
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
|
||||
};
|
||||
}) :
|
||||
currentData.kline_data.map(kline => {
|
||||
let volumes = [];
|
||||
if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) {
|
||||
volumes = currentData.element_kline_data.map(kline => {
|
||||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||||
return {
|
||||
time: timestamp,
|
||||
@@ -3020,13 +3103,23 @@
|
||||
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
|
||||
};
|
||||
});
|
||||
} else if (currentData.kline_data && Array.isArray(currentData.kline_data)) {
|
||||
volumes = currentData.kline_data.map(kline => {
|
||||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||||
return {
|
||||
time: timestamp,
|
||||
value: parseFloat(kline.volume),
|
||||
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (tvWidget.series.volumeSeries) {
|
||||
tvWidget.series.volumeSeries.setData(volumes);
|
||||
}
|
||||
|
||||
// 更新MACD数据
|
||||
if ($('#showMacd').is(':checked') && currentData.macd && tvWidget.series.macdLineSeries) {
|
||||
if ($('#showMacd').is(':checked') && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data) && tvWidget.series.macdLineSeries) {
|
||||
// 提取MACD数据
|
||||
const macdData = [];
|
||||
const signalData = [];
|
||||
@@ -3521,8 +3614,11 @@
|
||||
tables.kline.clear().destroy();
|
||||
}
|
||||
|
||||
// 检查数据是否存在且为数组
|
||||
const klineData = (data.kline_data && Array.isArray(data.kline_data)) ? data.kline_data : [];
|
||||
|
||||
tables.kline = $('#klineTable').DataTable({
|
||||
data: data.kline_data,
|
||||
data: klineData,
|
||||
order: [[0, 'desc']],
|
||||
pageLength: 25,
|
||||
columns: [
|
||||
@@ -3659,15 +3755,18 @@
|
||||
tables.macd.clear().destroy();
|
||||
}
|
||||
|
||||
const macdData = data.kline_data.map((item, index) => {
|
||||
return {
|
||||
time: item.date,
|
||||
close: item.close,
|
||||
macd: data.macd.macd[index],
|
||||
signal: data.macd.signal[index],
|
||||
histogram: data.macd.histogram[index]
|
||||
};
|
||||
});
|
||||
let macdData = [];
|
||||
if (data.kline_data && Array.isArray(data.kline_data) && data.macd && data.macd.macd) {
|
||||
macdData = data.kline_data.map((item, index) => {
|
||||
return {
|
||||
time: item.date,
|
||||
close: item.close,
|
||||
macd: data.macd.macd[index],
|
||||
signal: data.macd.signal[index],
|
||||
histogram: data.macd.histogram[index]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
tables.macd = $('#macdTable').DataTable({
|
||||
data: macdData,
|
||||
@@ -3786,8 +3885,36 @@
|
||||
console.log('从本地存储恢复时区设置:', savedTimezone);
|
||||
}
|
||||
|
||||
// 初始化数据源切换
|
||||
$('#dataSource').on('change', function() {
|
||||
const dataSource = $(this).val();
|
||||
if (dataSource === 'crypto') {
|
||||
$('#cryptoSymbolContainer').show();
|
||||
$('#astockSymbolContainer').hide();
|
||||
// 停止A股状态更新器
|
||||
if (window.astockStatusInterval) {
|
||||
clearInterval(window.astockStatusInterval);
|
||||
window.astockStatusInterval = null;
|
||||
}
|
||||
} else if (dataSource === 'a_stock') {
|
||||
$('#cryptoSymbolContainer').hide();
|
||||
$('#astockSymbolContainer').show();
|
||||
// 加载A股数据时,如果还没有加载股票列表,可以在这里触发加载
|
||||
loadAStockSymbols();
|
||||
// 启动A股交易时间状态更新器
|
||||
startAStockStatusUpdater();
|
||||
}
|
||||
});
|
||||
|
||||
// 检查初始数据源设置
|
||||
const initialDataSource = $('#dataSource').val();
|
||||
if (initialDataSource === 'a_stock') {
|
||||
startAStockStatusUpdater();
|
||||
}
|
||||
|
||||
// 初始化交易对下拉菜单
|
||||
$('#symbol').val('BTC/USDT:USDT');
|
||||
$('#astockSymbol').val('000001');
|
||||
$('#timeframe').val('5m');
|
||||
$('#elementTimeframe').val('1m');
|
||||
|
||||
@@ -4802,6 +4929,180 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取A股股票列表
|
||||
function loadAStockSymbols() {
|
||||
$.get('/api/popular_a_stocks', function(data) {
|
||||
if (Array.isArray(data)) {
|
||||
const $select = $('#astockSymbol');
|
||||
const currentSymbol = $select.val(); // 保存当前选中的值
|
||||
$select.empty();
|
||||
|
||||
data.forEach(function(stock) {
|
||||
$select.append($('<option>', {
|
||||
value: stock.symbol,
|
||||
text: stock.symbol + ' - ' + stock.name
|
||||
}));
|
||||
});
|
||||
|
||||
// 如果有保存的选中值,恢复它
|
||||
if (currentSymbol && data.some(stock => stock.symbol === currentSymbol)) {
|
||||
$select.val(currentSymbol);
|
||||
} else {
|
||||
// 设置默认值为平安银行
|
||||
$select.val('000001');
|
||||
}
|
||||
}
|
||||
}).fail(function() {
|
||||
console.error('加载A股股票列表失败');
|
||||
});
|
||||
}
|
||||
|
||||
// 检测交易对类型并返回相应的配置
|
||||
function getSymbolConfig(symbol) {
|
||||
const isAStock = symbol && symbol.length === 6 && /^\d+$/.test(symbol);
|
||||
|
||||
if (isAStock) {
|
||||
return {
|
||||
type: 'a_stock',
|
||||
displayName: symbol,
|
||||
tradingSessions: [
|
||||
// A股交易时间配置
|
||||
{ start: '09:30', end: '11:30' }, // 上午
|
||||
{ start: '13:00', end: '15:00' } // 下午
|
||||
],
|
||||
timezone: 'Asia/Shanghai',
|
||||
// A股的交易日配置(周一到周五,除节假日)
|
||||
tradingDays: [1, 2, 3, 4, 5] // 1=周一, 7=周日
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'crypto',
|
||||
displayName: symbol,
|
||||
tradingSessions: [
|
||||
{ start: '00:00', end: '23:59' } // 24小时交易
|
||||
],
|
||||
timezone: 'UTC',
|
||||
tradingDays: [1, 2, 3, 4, 5, 6, 7] // 7天交易
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 根据交易对类型调整图表配置
|
||||
function adjustChartForSymbolType(chartOptions, symbolConfig) {
|
||||
if (symbolConfig.type === 'a_stock') {
|
||||
// A股特殊配置
|
||||
chartOptions.timeScale = {
|
||||
...chartOptions.timeScale,
|
||||
// 禁用非交易时间的显示
|
||||
borderVisible: true,
|
||||
borderColor: '#ddd',
|
||||
// 自定义时间格式化,只显示交易时间
|
||||
timeVisible: true,
|
||||
// 添加A股特定的时间范围限制
|
||||
rightOffset: 12,
|
||||
barSpacing: 6,
|
||||
minBarSpacing: 3,
|
||||
};
|
||||
|
||||
// 添加A股交易时间提示
|
||||
chartOptions.layout = {
|
||||
...chartOptions.layout,
|
||||
fontSize: 12,
|
||||
fontFamily: 'Arial, sans-serif'
|
||||
};
|
||||
}
|
||||
|
||||
return chartOptions;
|
||||
}
|
||||
|
||||
// 过滤非交易时间的数据(仅用于显示优化)
|
||||
function filterTradingHours(data, symbolConfig) {
|
||||
if (symbolConfig.type !== 'a_stock') {
|
||||
return data; // 非A股数据不需要过滤
|
||||
}
|
||||
|
||||
return data.filter(item => {
|
||||
const date = new Date(item.time * 1000);
|
||||
const hour = date.getHours();
|
||||
const minute = date.getMinutes();
|
||||
const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
||||
|
||||
// 检查是否在交易时间内
|
||||
return symbolConfig.tradingSessions.some(session => {
|
||||
return timeStr >= session.start && timeStr <= session.end;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新A股交易时间状态
|
||||
function updateAStockTradingStatus() {
|
||||
const now = new Date();
|
||||
const chinaTime = new Date(now.toLocaleString("en-US", {timeZone: "Asia/Shanghai"}));
|
||||
const hour = chinaTime.getHours();
|
||||
const minute = chinaTime.getMinutes();
|
||||
const dayOfWeek = chinaTime.getDay(); // 0=周日, 1=周一, ..., 6=周六
|
||||
|
||||
const statusElement = document.getElementById('tradingTimeStatus');
|
||||
if (!statusElement) return;
|
||||
|
||||
// 检查是否为交易日(周一到周五)
|
||||
const isTradingDay = dayOfWeek >= 1 && dayOfWeek <= 5;
|
||||
|
||||
if (!isTradingDay) {
|
||||
statusElement.className = 'badge bg-secondary';
|
||||
statusElement.textContent = '非交易日';
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否在交易时间内
|
||||
const currentTime = hour * 60 + minute; // 转换为分钟
|
||||
const morningStart = 9 * 60 + 30; // 09:30
|
||||
const morningEnd = 11 * 60 + 30; // 11:30
|
||||
const afternoonStart = 13 * 60; // 13:00
|
||||
const afternoonEnd = 15 * 60; // 15:00
|
||||
|
||||
let status = '';
|
||||
let className = '';
|
||||
|
||||
if (currentTime >= morningStart && currentTime <= morningEnd) {
|
||||
status = '上午交易中';
|
||||
className = 'badge bg-success';
|
||||
} else if (currentTime >= afternoonStart && currentTime <= afternoonEnd) {
|
||||
status = '下午交易中';
|
||||
className = 'badge bg-success';
|
||||
} else if (currentTime > morningEnd && currentTime < afternoonStart) {
|
||||
status = '午间休市';
|
||||
className = 'badge bg-warning';
|
||||
} else if (currentTime < morningStart) {
|
||||
status = '开盘前';
|
||||
className = 'badge bg-info';
|
||||
} else if (currentTime > afternoonEnd) {
|
||||
status = '收盘后';
|
||||
className = 'badge bg-dark';
|
||||
} else {
|
||||
status = '非交易时间';
|
||||
className = 'badge bg-secondary';
|
||||
}
|
||||
|
||||
statusElement.className = className;
|
||||
statusElement.textContent = status;
|
||||
}
|
||||
|
||||
// 启动A股交易时间状态更新
|
||||
function startAStockStatusUpdater() {
|
||||
// 如果已经有定时器在运行,先清除
|
||||
if (window.astockStatusInterval) {
|
||||
clearInterval(window.astockStatusInterval);
|
||||
}
|
||||
|
||||
// 立即更新一次
|
||||
updateAStockTradingStatus();
|
||||
|
||||
// 每30秒更新一次
|
||||
window.astockStatusInterval = setInterval(updateAStockTradingStatus, 30000);
|
||||
console.log('A股交易时间状态更新器已启动');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user