up
This commit is contained in:
Executable
+175
@@ -0,0 +1,175 @@
|
||||
// 股票历史K线数据代理 Edge Function
|
||||
// 主数据源:腾讯财经 fqkline 接口(前复权日K)
|
||||
// 降级数据源:新浪财经 getKLineData 接口(腾讯返回数据过少时启用,如北交所920开头新股)
|
||||
|
||||
function corsHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'content-type',
|
||||
};
|
||||
}
|
||||
|
||||
// 根据代码推断市场前缀
|
||||
function getMarketPrefix(code: string): string {
|
||||
if (code.startsWith('688') || code.startsWith('60')) return 'sh';
|
||||
if (code.startsWith('920') || code.startsWith('8') || code.startsWith('4')) return 'bj';
|
||||
return 'sz';
|
||||
}
|
||||
|
||||
// 从腾讯返回数据中提取K线原始数组
|
||||
function extractTencentKlines(result: any, stockCode: string): any[] {
|
||||
const stockData = result?.data?.[stockCode];
|
||||
if (!stockData) return [];
|
||||
const raw = stockData.qfqday || stockData.day || [];
|
||||
return Array.isArray(raw) ? raw : [];
|
||||
}
|
||||
|
||||
// 转换腾讯K线格式:[日期, 开, 收, 高, 低, 量, 额]
|
||||
function parseTencentKlines(rawKlines: any[]) {
|
||||
return rawKlines.map((record: any[]) => ({
|
||||
date: record[0],
|
||||
open: parseFloat(record[1]) || 0,
|
||||
close: parseFloat(record[2]) || 0,
|
||||
high: parseFloat(record[3]) || 0,
|
||||
low: parseFloat(record[4]) || 0,
|
||||
volume: parseInt(record[5]) || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
// 新浪财经日K线接口(降级数据源)
|
||||
// 返回格式:[{ day, open, high, low, close, volume }, ...]
|
||||
async function fetchSinaKlines(code: string, days: number): Promise<any[]> {
|
||||
const market = getMarketPrefix(code);
|
||||
const sinaSymbol = `${market}${code}`;
|
||||
const url = `https://money.finance.sina.com.cn/quotes_service/api/json_v2.php/CN_MarketData.getKLineData?symbol=${sinaSymbol}&scale=240&ma=no&datalen=${days}`;
|
||||
console.log(`[stock-history] 降级新浪接口: ${url}`);
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.error(`[stock-history] 新浪接口响应异常: ${resp.status}`);
|
||||
return [];
|
||||
}
|
||||
const text = await resp.text();
|
||||
if (!text || text.trim() === '' || text.trim() === 'null') return [];
|
||||
const arr = JSON.parse(text);
|
||||
if (!Array.isArray(arr)) return [];
|
||||
return arr.map((item: any) => ({
|
||||
date: item.day,
|
||||
open: parseFloat(item.open) || 0,
|
||||
close: parseFloat(item.close) || 0,
|
||||
high: parseFloat(item.high) || 0,
|
||||
low: parseFloat(item.low) || 0,
|
||||
volume: parseInt(item.volume) || 0,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('[stock-history] 新浪接口失败:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders() });
|
||||
}
|
||||
|
||||
const url = new URL(req.url);
|
||||
const code = url.searchParams.get('code') || '';
|
||||
const days = parseInt(url.searchParams.get('days') || '90');
|
||||
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
return new Response(JSON.stringify({ error: '股票代码格式错误,需为6位数字' }), {
|
||||
status: 400,
|
||||
headers: corsHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
const market = getMarketPrefix(code);
|
||||
const stockCode = `${market}${code}`;
|
||||
|
||||
try {
|
||||
console.log(`[stock-history] 请求股票: ${stockCode}, 天数: ${days}`);
|
||||
|
||||
// 1. 主数据源:腾讯财经日K线接口(前复权)
|
||||
const apiUrl = `https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=${stockCode},day,,,${days},qfq`;
|
||||
console.log(`[stock-history] 腾讯请求URL: ${apiUrl}`);
|
||||
|
||||
const resp = await fetch(apiUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': 'https://stockapp.finance.qq.com/',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`[stock-history] 腾讯响应状态: ${resp.status}`);
|
||||
|
||||
let tencentKlines: any[] = [];
|
||||
if (resp.ok) {
|
||||
const result = await resp.json();
|
||||
console.log(`[stock-history] 腾讯返回数据结构:`, JSON.stringify(result).substring(0, 500));
|
||||
const rawKlines = extractTencentKlines(result, stockCode);
|
||||
console.log(`[stock-history] 腾讯K线数据条数: ${rawKlines.length}`);
|
||||
if (rawKlines.length > 0) {
|
||||
console.log(`[stock-history] 腾讯首条:`, JSON.stringify(rawKlines[0]));
|
||||
console.log(`[stock-history] 腾讯末条:`, JSON.stringify(rawKlines[rawKlines.length - 1]));
|
||||
tencentKlines = parseTencentKlines(rawKlines);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 腾讯数据充足则直接返回
|
||||
if (tencentKlines.length >= 2) {
|
||||
console.log(`[stock-history] 腾讯数据充足,返回 ${tencentKlines.length} 条`);
|
||||
return new Response(JSON.stringify({
|
||||
data: tencentKlines,
|
||||
count: tencentKlines.length,
|
||||
source: 'tencent',
|
||||
}), { headers: corsHeaders() });
|
||||
}
|
||||
|
||||
// 3. 腾讯数据过少(新股、北交所920开头等),降级到新浪
|
||||
console.log(`[stock-history] 腾讯数据不足(${tencentKlines.length}条),降级新浪接口`);
|
||||
const sinaKlines = await fetchSinaKlines(code, days);
|
||||
console.log(`[stock-history] 新浪K线数据条数: ${sinaKlines.length}`);
|
||||
|
||||
if (sinaKlines.length > 0) {
|
||||
return new Response(JSON.stringify({
|
||||
data: sinaKlines,
|
||||
count: sinaKlines.length,
|
||||
source: 'sina',
|
||||
}), { headers: corsHeaders() });
|
||||
}
|
||||
|
||||
// 4. 新浪也无数据,若腾讯至少有1条则返回,否则报错
|
||||
if (tencentKlines.length > 0) {
|
||||
console.log(`[stock-history] 新浪无数据,返回腾讯的 ${tencentKlines.length} 条`);
|
||||
return new Response(JSON.stringify({
|
||||
data: tencentKlines,
|
||||
count: tencentKlines.length,
|
||||
source: 'tencent',
|
||||
}), { headers: corsHeaders() });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: '未获取到K线数据',
|
||||
data: [],
|
||||
count: 0,
|
||||
}), {
|
||||
status: 404,
|
||||
headers: corsHeaders(),
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
console.error('[stock-history] 错误:', msg);
|
||||
return new Response(JSON.stringify({
|
||||
error: `获取数据失败: ${msg}`,
|
||||
data: [],
|
||||
count: 0,
|
||||
}), {
|
||||
status: 500,
|
||||
headers: corsHeaders(),
|
||||
});
|
||||
}
|
||||
});
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
使用mootdx获取A股历史K线数据
|
||||
mootdx通过TCP协议直连通达信服务器,不依赖HTTP API,稳定性极高
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
def get_kline_data(code: str, days: int = 90):
|
||||
"""
|
||||
获取股票历史K线数据
|
||||
|
||||
Args:
|
||||
code: 6位股票代码
|
||||
days: 需要获取的天数
|
||||
|
||||
Returns:
|
||||
K线数据列表
|
||||
"""
|
||||
try:
|
||||
from mootdx.quotes import Quotes
|
||||
|
||||
# 判断市场
|
||||
market = 'sh' if code.startswith('6') else 'sz'
|
||||
|
||||
# 创建客户端
|
||||
client = Quotes.factory(market='std', multithread=True, heartbeat=True)
|
||||
|
||||
# 获取K线数据(日K)
|
||||
# category: 0=5分钟, 1=15分钟, 2=30分钟, 3=1小时, 4=日K, 5=周K, 6=月K
|
||||
klines = client.bars(symbol=code, market=market, category=4, count=days)
|
||||
|
||||
if not klines or len(klines) == 0:
|
||||
return {
|
||||
'error': '未获取到K线数据',
|
||||
'data': [],
|
||||
'count': 0
|
||||
}
|
||||
|
||||
# 转换为标准格式
|
||||
result = []
|
||||
for bar in klines:
|
||||
# mootdx返回的是namedtuple,包含: datetime, open, high, low, close, amount, vol
|
||||
result.append({
|
||||
'date': bar.datetime.strftime('%Y-%m-%d') if hasattr(bar.datetime, 'strftime') else str(bar.datetime)[:10],
|
||||
'open': float(bar.open),
|
||||
'close': float(bar.close),
|
||||
'high': float(bar.high),
|
||||
'low': float(bar.low),
|
||||
'volume': int(bar.vol) if hasattr(bar, 'vol') else 0,
|
||||
})
|
||||
|
||||
# 按日期排序(从旧到新)
|
||||
result.sort(key=lambda x: x['date'])
|
||||
|
||||
return {
|
||||
'data': result,
|
||||
'count': len(result),
|
||||
'source': 'mootdx'
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
return {
|
||||
'error': 'mootdx库未安装',
|
||||
'data': [],
|
||||
'count': 0
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'error': f'获取失败: {str(e)}',
|
||||
'data': [],
|
||||
'count': 0
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 从命令行参数获取股票代码和天数
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({'error': '请提供股票代码'}))
|
||||
sys.exit(1)
|
||||
|
||||
code = sys.argv[1]
|
||||
days = int(sys.argv[2]) if len(sys.argv) > 2 else 90
|
||||
|
||||
result = get_kline_data(code, days)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
Reference in New Issue
Block a user