Compare commits
3 Commits
fff937a7d5
...
72836fa5d4
| Author | SHA1 | Date | |
|---|---|---|---|
| 72836fa5d4 | |||
| 17479a7362 | |||
| f5c46ae71b |
189
app.py
189
app.py
@@ -5514,7 +5514,7 @@ def remove_from_watchlist(stock_code):
|
|||||||
|
|
||||||
@app.route('/api/account/watchlist/realtime', methods=['GET'])
|
@app.route('/api/account/watchlist/realtime', methods=['GET'])
|
||||||
def get_watchlist_realtime():
|
def get_watchlist_realtime():
|
||||||
"""获取自选股实时行情数据(基于分钟线)"""
|
"""获取自选股实时行情数据(基于分钟线)- 优化为批量查询"""
|
||||||
try:
|
try:
|
||||||
if 'user_id' not in session:
|
if 'user_id' not in session:
|
||||||
return jsonify({'success': False, 'error': '未登录'}), 401
|
return jsonify({'success': False, 'error': '未登录'}), 401
|
||||||
@@ -5524,103 +5524,127 @@ def get_watchlist_realtime():
|
|||||||
if not watchlist:
|
if not watchlist:
|
||||||
return jsonify({'success': True, 'data': []})
|
return jsonify({'success': True, 'data': []})
|
||||||
|
|
||||||
# 获取股票代码列表
|
# 获取股票代码列表并标准化
|
||||||
stock_codes = []
|
code_mapping = {} # code6 -> full_code 映射
|
||||||
|
full_codes = []
|
||||||
for item in watchlist:
|
for item in watchlist:
|
||||||
code6, _ = _normalize_stock_input(item.stock_code)
|
code6, _ = _normalize_stock_input(item.stock_code)
|
||||||
# 统一内部查询代码
|
|
||||||
normalized = code6 or str(item.stock_code).strip().upper()
|
normalized = code6 or str(item.stock_code).strip().upper()
|
||||||
stock_codes.append(normalized)
|
|
||||||
|
|
||||||
# 使用现有的分钟线接口获取最新行情
|
# 转换为带后缀的完整代码
|
||||||
client = get_clickhouse_client()
|
if '.' in normalized:
|
||||||
quotes_data = {}
|
full_code = normalized
|
||||||
|
elif normalized.startswith('6'):
|
||||||
# 获取最新交易日
|
full_code = f"{normalized}.SH"
|
||||||
today = datetime.now().date()
|
elif normalized.startswith(('8', '9', '4')):
|
||||||
|
full_code = f"{normalized}.BJ"
|
||||||
# 获取每只股票的最新价格
|
|
||||||
for code in stock_codes:
|
|
||||||
raw_code = str(code).strip().upper()
|
|
||||||
if '.' in raw_code:
|
|
||||||
stock_code_full = raw_code
|
|
||||||
elif raw_code.startswith('6'):
|
|
||||||
stock_code_full = f"{raw_code}.SH" # 上海
|
|
||||||
elif raw_code.startswith(('8', '9', '4')):
|
|
||||||
stock_code_full = f"{raw_code}.BJ" # 北交所
|
|
||||||
else:
|
else:
|
||||||
stock_code_full = f"{raw_code}.SZ" # 深圳
|
full_code = f"{normalized}.SZ"
|
||||||
|
|
||||||
# 获取最新分钟线数据(先查近7天,若无数据再兜底倒序取最近一条)
|
code_mapping[normalized] = full_code
|
||||||
query = """
|
full_codes.append(full_code)
|
||||||
SELECT
|
|
||||||
close, timestamp, high, low, volume, amt
|
|
||||||
FROM stock_minute
|
|
||||||
WHERE code = %(code)s
|
|
||||||
AND timestamp >= %(start)s
|
|
||||||
ORDER BY timestamp DESC
|
|
||||||
LIMIT 1 \
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 获取最近7天的分钟数据
|
if not full_codes:
|
||||||
start_date = today - timedelta(days=7)
|
return jsonify({'success': True, 'data': []})
|
||||||
|
|
||||||
result = client.execute(query, {
|
# 使用批量查询获取最新行情(单次查询)
|
||||||
'code': stock_code_full,
|
client = get_clickhouse_client()
|
||||||
'start': datetime.combine(start_date, dt_time(9, 30))
|
today = datetime.now().date()
|
||||||
})
|
start_date = today - timedelta(days=7)
|
||||||
|
|
||||||
# 若近7天无数据,兜底直接取最近一条
|
# 批量查询:获取每只股票的最新一条分钟数据
|
||||||
if not result:
|
batch_query = """
|
||||||
fallback_query = """
|
WITH latest AS (
|
||||||
SELECT
|
SELECT
|
||||||
close, timestamp, high, low, volume, amt
|
code,
|
||||||
FROM stock_minute
|
close,
|
||||||
WHERE code = %(code)s
|
timestamp,
|
||||||
ORDER BY timestamp DESC
|
high,
|
||||||
LIMIT 1 \
|
low,
|
||||||
"""
|
volume,
|
||||||
result = client.execute(fallback_query, {'code': stock_code_full})
|
amt,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp DESC) as rn
|
||||||
|
FROM stock_minute
|
||||||
|
WHERE code IN %(codes)s
|
||||||
|
AND timestamp >= %(start)s
|
||||||
|
)
|
||||||
|
SELECT code, close, timestamp, high, low, volume, amt
|
||||||
|
FROM latest
|
||||||
|
WHERE rn = 1
|
||||||
|
"""
|
||||||
|
|
||||||
if result:
|
result = client.execute(batch_query, {
|
||||||
latest_data = result[0]
|
'codes': full_codes,
|
||||||
latest_ts = latest_data[1]
|
'start': datetime.combine(start_date, dt_time(9, 30))
|
||||||
|
})
|
||||||
|
|
||||||
# 获取该bar所属交易日前一个交易日的收盘价
|
# 构建最新价格映射
|
||||||
prev_close_query = """
|
latest_data_map = {}
|
||||||
SELECT close
|
for row in result:
|
||||||
FROM stock_minute
|
code, close, ts, high, low, volume, amt = row
|
||||||
WHERE code = %(code)s
|
latest_data_map[code] = {
|
||||||
AND timestamp \
|
'close': float(close),
|
||||||
< %(start)s
|
'timestamp': ts,
|
||||||
ORDER BY timestamp DESC
|
'high': float(high),
|
||||||
LIMIT 1 \
|
'low': float(low),
|
||||||
"""
|
'volume': int(volume),
|
||||||
|
'amount': float(amt)
|
||||||
|
}
|
||||||
|
|
||||||
prev_result = client.execute(prev_close_query, {
|
# 批量查询前收盘价(使用 ea_trade 表,更准确)
|
||||||
'code': stock_code_full,
|
prev_close_map = {}
|
||||||
'start': datetime.combine(latest_ts.date(), dt_time(9, 30))
|
if latest_data_map:
|
||||||
})
|
# 获取前一交易日
|
||||||
|
prev_trading_day = None
|
||||||
|
for td in reversed(trading_days):
|
||||||
|
if td < today:
|
||||||
|
prev_trading_day = td
|
||||||
|
break
|
||||||
|
|
||||||
prev_close = float(prev_result[0][0]) if prev_result else float(latest_data[0])
|
if prev_trading_day:
|
||||||
|
base_codes = [code.split('.')[0] for code in full_codes]
|
||||||
|
prev_day_str = prev_trading_day.strftime('%Y%m%d')
|
||||||
|
|
||||||
# 计算涨跌幅
|
with engine.connect() as conn:
|
||||||
change = float(latest_data[0]) - prev_close
|
placeholders = ','.join([f':code{i}' for i in range(len(base_codes))])
|
||||||
change_percent = (change / prev_close * 100) if prev_close > 0 else 0.0
|
params = {f'code{i}': code for i, code in enumerate(base_codes)}
|
||||||
|
params['trade_date'] = prev_day_str
|
||||||
|
|
||||||
quotes_data[code] = {
|
prev_result = conn.execute(text(f"""
|
||||||
'price': float(latest_data[0]),
|
SELECT SECCODE, F007N as close_price
|
||||||
'prev_close': float(prev_close),
|
FROM ea_trade
|
||||||
'change': float(change),
|
WHERE SECCODE IN ({placeholders})
|
||||||
'change_percent': float(change_percent),
|
AND TRADEDATE = :trade_date
|
||||||
'high': float(latest_data[2]),
|
"""), params).fetchall()
|
||||||
'low': float(latest_data[3]),
|
|
||||||
'volume': int(latest_data[4]),
|
for row in prev_result:
|
||||||
'amount': float(latest_data[5]),
|
base_code, close_price = row[0], row[1]
|
||||||
'update_time': latest_ts.strftime('%H:%M:%S')
|
if close_price:
|
||||||
}
|
prev_close_map[base_code] = float(close_price)
|
||||||
|
|
||||||
# 构建响应数据
|
# 构建响应数据
|
||||||
|
quotes_data = {}
|
||||||
|
for code6, full_code in code_mapping.items():
|
||||||
|
latest = latest_data_map.get(full_code)
|
||||||
|
if latest:
|
||||||
|
base_code = full_code.split('.')[0]
|
||||||
|
prev_close = prev_close_map.get(base_code, latest['close'])
|
||||||
|
|
||||||
|
change = latest['close'] - prev_close
|
||||||
|
change_percent = (change / prev_close * 100) if prev_close > 0 else 0.0
|
||||||
|
|
||||||
|
quotes_data[code6] = {
|
||||||
|
'price': latest['close'],
|
||||||
|
'prev_close': prev_close,
|
||||||
|
'change': change,
|
||||||
|
'change_percent': change_percent,
|
||||||
|
'high': latest['high'],
|
||||||
|
'low': latest['low'],
|
||||||
|
'volume': latest['volume'],
|
||||||
|
'amount': latest['amount'],
|
||||||
|
'update_time': latest['timestamp'].strftime('%H:%M:%S')
|
||||||
|
}
|
||||||
|
|
||||||
response_data = []
|
response_data = []
|
||||||
for item in watchlist:
|
for item in watchlist:
|
||||||
code6, _ = _normalize_stock_input(item.stock_code)
|
code6, _ = _normalize_stock_input(item.stock_code)
|
||||||
@@ -5637,7 +5661,6 @@ def get_watchlist_realtime():
|
|||||||
'volume': quote.get('volume', 0),
|
'volume': quote.get('volume', 0),
|
||||||
'amount': quote.get('amount', 0),
|
'amount': quote.get('amount', 0),
|
||||||
'update_time': quote.get('update_time', ''),
|
'update_time': quote.get('update_time', ''),
|
||||||
# industry 字段在 Watchlist 模型中不存在,先不返回该字段
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -5647,6 +5670,8 @@ def get_watchlist_realtime():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"获取实时行情失败: {str(e)}")
|
print(f"获取实时行情失败: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': '获取实时行情失败'}), 500
|
return jsonify({'success': False, 'error': '获取实时行情失败'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,24 +7,44 @@
|
|||||||
// 数据基础路径
|
// 数据基础路径
|
||||||
const DATA_BASE_URL = '/data/zt';
|
const DATA_BASE_URL = '/data/zt';
|
||||||
|
|
||||||
// 内存缓存
|
// 缓存过期时间(毫秒)- dates.json 缓存5分钟,daily数据缓存30分钟
|
||||||
|
const CACHE_TTL = {
|
||||||
|
dates: 5 * 60 * 1000, // 5分钟
|
||||||
|
daily: 30 * 60 * 1000, // 30分钟
|
||||||
|
stocksJsonl: 60 * 60 * 1000, // 1小时
|
||||||
|
};
|
||||||
|
|
||||||
|
// 内存缓存(带过期时间)
|
||||||
const cache = {
|
const cache = {
|
||||||
dates: null,
|
dates: null,
|
||||||
|
datesTimestamp: 0,
|
||||||
daily: new Map(),
|
daily: new Map(),
|
||||||
stocksJsonl: null, // 缓存 stocks.jsonl 数据
|
dailyTimestamps: new Map(),
|
||||||
|
stocksJsonl: null,
|
||||||
|
stocksJsonlTimestamp: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查缓存是否过期
|
||||||
|
*/
|
||||||
|
const isCacheExpired = (timestamp, ttl) => {
|
||||||
|
return Date.now() - timestamp > ttl;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取可用日期列表
|
* 获取可用日期列表
|
||||||
|
* @param {boolean} forceRefresh - 是否强制刷新缓存
|
||||||
*/
|
*/
|
||||||
export const fetchAvailableDates = async () => {
|
export const fetchAvailableDates = async (forceRefresh = false) => {
|
||||||
try {
|
try {
|
||||||
// 使用缓存
|
// 使用缓存(未过期且非强制刷新)
|
||||||
if (cache.dates) {
|
if (!forceRefresh && cache.dates && !isCacheExpired(cache.datesTimestamp, CACHE_TTL.dates)) {
|
||||||
return { success: true, events: cache.dates };
|
console.log('[ztStaticService] fetchAvailableDates: using cache');
|
||||||
|
return { success: true, events: cache.dates, from_cache: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${DATA_BASE_URL}/dates.json`);
|
console.log('[ztStaticService] fetchAvailableDates: fetching from server');
|
||||||
|
const response = await fetch(`${DATA_BASE_URL}/dates.json?t=${Date.now()}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
throw new Error(`HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -44,8 +64,9 @@ export const fetchAvailableDates = async () => {
|
|||||||
|
|
||||||
// 缓存结果
|
// 缓存结果
|
||||||
cache.dates = events;
|
cache.dates = events;
|
||||||
|
cache.datesTimestamp = Date.now();
|
||||||
|
|
||||||
return { success: true, events, total: events.length };
|
return { success: true, events, total: events.length, from_cache: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ztStaticService] fetchAvailableDates error:', error);
|
console.error('[ztStaticService] fetchAvailableDates error:', error);
|
||||||
return { success: false, error: error.message, events: [] };
|
return { success: false, error: error.message, events: [] };
|
||||||
@@ -54,15 +75,18 @@ export const fetchAvailableDates = async () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定日期的分析数据
|
* 获取指定日期的分析数据
|
||||||
|
* @param {string} date - 日期(YYYYMMDD格式)
|
||||||
|
* @param {boolean} forceRefresh - 是否强制刷新缓存
|
||||||
*/
|
*/
|
||||||
export const fetchDailyAnalysis = async (date) => {
|
export const fetchDailyAnalysis = async (date, forceRefresh = false) => {
|
||||||
try {
|
try {
|
||||||
// 使用缓存
|
// 使用缓存(未过期且非强制刷新)
|
||||||
if (cache.daily.has(date)) {
|
const cachedTimestamp = cache.dailyTimestamps.get(date);
|
||||||
|
if (!forceRefresh && cache.daily.has(date) && cachedTimestamp && !isCacheExpired(cachedTimestamp, CACHE_TTL.daily)) {
|
||||||
return { success: true, data: cache.daily.get(date), from_cache: true };
|
return { success: true, data: cache.daily.get(date), from_cache: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${DATA_BASE_URL}/daily/${date}.json`);
|
const response = await fetch(`${DATA_BASE_URL}/daily/${date}.json?t=${Date.now()}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
return { success: false, error: `日期 ${date} 的数据不存在` };
|
return { success: false, error: `日期 ${date} 的数据不存在` };
|
||||||
@@ -102,6 +126,7 @@ export const fetchDailyAnalysis = async (date) => {
|
|||||||
|
|
||||||
// 缓存结果
|
// 缓存结果
|
||||||
cache.daily.set(date, data);
|
cache.daily.set(date, data);
|
||||||
|
cache.dailyTimestamps.set(date, Date.now());
|
||||||
|
|
||||||
return { success: true, data, from_cache: false };
|
return { success: true, data, from_cache: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -348,8 +373,12 @@ export const fetchStocksBatchDetail = async (codes, date) => {
|
|||||||
*/
|
*/
|
||||||
export const clearCache = () => {
|
export const clearCache = () => {
|
||||||
cache.dates = null;
|
cache.dates = null;
|
||||||
|
cache.datesTimestamp = 0;
|
||||||
cache.daily.clear();
|
cache.daily.clear();
|
||||||
|
cache.dailyTimestamps.clear();
|
||||||
cache.stocksJsonl = null;
|
cache.stocksJsonl = null;
|
||||||
|
cache.stocksJsonlTimestamp = 0;
|
||||||
|
console.log('[ztStaticService] Cache cleared');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
Reference in New Issue
Block a user