@@ -599,6 +609,7 @@ ADMIN_HTML = '''
if (pageName === 'tests') {
loadAPITestList();
+ loadInternalTestList();
loadWSTestList();
} else if (pageName === 'config') {
loadConfigList();
@@ -611,6 +622,7 @@ ADMIN_HTML = '''
switchTestTab = function(tab) {
_originalSwitchTestTab(tab);
if (tab === 'history') loadTestHistory();
+ if (tab === 'internal') loadInternalTestList();
};
// ============ API 测试 ============
@@ -736,6 +748,131 @@ ADMIN_HTML = '''
});
}
+ // ============ 对内接口测试 ============
+ let internalTestCases = [];
+
+ async function loadInternalTestList() {
+ try {
+ const response = await fetch('/v1/admin/tests/internal', {
+ headers: { 'X-Admin-Token': apiKey }
+ });
+ const data = await response.json();
+ if (data.code === 0) {
+ internalTestCases = data.data.categories || [];
+ renderInternalTestList();
+ }
+ } catch (e) {
+ document.getElementById('internal-test-list').innerHTML = '加载失败: ' + e.message;
+ }
+ }
+
+ function renderInternalTestList() {
+ let html = '';
+ internalTestCases.forEach(cat => {
+ html += `
+
${cat.name}
`;
+ cat.items.forEach(item => {
+ html += `
+
+
+ SDK
+ ${item.name}
+
+
${item.description}
+
${item.method}
+
+
+
+
+
+
`;
+ });
+ html += '
';
+ });
+ document.getElementById('internal-test-list').innerHTML = html;
+ }
+
+ async function runInternalTest(testId) {
+ const btn = document.getElementById(`btn-internal-${testId}`);
+ const resultDiv = document.getElementById(`internal-result-${testId}`);
+
+ btn.disabled = true;
+ btn.innerHTML = '
';
+ resultDiv.className = 'test-result';
+ resultDiv.classList.add('show');
+ resultDiv.innerHTML = '运行中...';
+
+ try {
+ const response = await fetch('/v1/admin/tests/internal/run', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Admin-Token': apiKey
+ },
+ body: JSON.stringify({ id: testId })
+ });
+ const data = await response.json();
+
+ if (data.code === 0 && data.data) {
+ const result = data.data;
+ const isSuccess = result.success;
+
+ resultDiv.className = 'test-result show ' + (isSuccess ? 'success' : 'error');
+ resultDiv.innerHTML = `
+
+
+ 接口: ${result.interface}
+
+
+
参数: ${JSON.stringify(result.params, null, 2)}
+
+
+
响应:
+
${typeof result.response === 'object' ? JSON.stringify(result.response, null, 2) : result.response}
+
+ ${result.error ? `
错误: ${result.error}
` : ''}
+ `;
+
+ btn.innerHTML = isSuccess ? '✓ 通过' : '✗ 失败';
+ btn.className = isSuccess ? 'btn btn-success btn-sm' : 'btn btn-danger btn-sm';
+ } else {
+ throw new Error(data.message || '测试执行失败');
+ }
+ } catch (e) {
+ resultDiv.className = 'test-result show error';
+ resultDiv.innerHTML = `
${e.message}
`;
+ btn.innerHTML = '✗ 失败';
+ btn.className = 'btn btn-danger btn-sm';
+ }
+
+ btn.disabled = false;
+ }
+
+ async function runAllInternalTests() {
+ const allIds = [];
+ internalTestCases.forEach(cat => cat.items.forEach(item => allIds.push(item.id)));
+
+ for (const id of allIds) {
+ await runInternalTest(id);
+ await new Promise(r => setTimeout(r, 300));
+ }
+ }
+
+ function clearAllInternalResults() {
+ document.querySelectorAll('[id^="internal-result-"]').forEach(el => {
+ el.className = 'test-result';
+ el.innerHTML = '';
+ });
+ document.querySelectorAll('[id^="btn-internal-"]').forEach(el => {
+ el.innerHTML = '测试';
+ el.className = 'btn btn-primary btn-sm';
+ el.disabled = false;
+ });
+ }
+
// ============ WebSocket 测试 ============
let wsTestCases = [];
diff --git a/app/services/__pycache__/test_service.cpython-311.pyc b/app/services/__pycache__/test_service.cpython-311.pyc
index b37c513..a4dba86 100644
Binary files a/app/services/__pycache__/test_service.cpython-311.pyc and b/app/services/__pycache__/test_service.cpython-311.pyc differ
diff --git a/app/services/test_service.py b/app/services/test_service.py
index 72656c9..c15a8e3 100644
--- a/app/services/test_service.py
+++ b/app/services/test_service.py
@@ -7,6 +7,7 @@ from threading import RLock
import httpx
import websockets
+import pandas as pd
from app.models import (
APITestListData, APITestCategory, APITestCase,
@@ -23,18 +24,19 @@ class TestService:
def __init__(self):
self.lock = RLock()
self.api_history: List[APITestResult] = []
+ self.internal_history: List[APITestResult] = []
self.ws_history: List[WSTestResult] = []
self.history_size = 100
def get_api_test_list(self) -> APITestListData:
- """获取API测试列表"""
+ """获取API测试列表(对外接口)"""
# 固定交易时间:2026年3月2日到2026年3月6日
test_start = datetime(2026, 3, 2)
test_end = datetime(2026, 3, 6)
categories = [
APITestCategory(
- name="股票接口",
+ name="【对外】股票数据接口",
items=[
APITestCase(
id="stock_klines",
@@ -73,7 +75,7 @@ class TestService:
),
APITestCase(
id="stock_calendar",
- name="查询交易日历",
+ name="查询股票交易日历",
method="GET",
path="/v1/stock/trading-dates",
description="查询股票交易日历",
@@ -85,7 +87,7 @@ class TestService:
]
),
APITestCategory(
- name="期货接口",
+ name="【对外】期货数据接口",
items=[
APITestCase(
id="futures_klines",
@@ -143,7 +145,7 @@ class TestService:
]
),
APITestCategory(
- name="管理接口",
+ name="【对外】管理接口",
items=[
APITestCase(
id="admin_health",
@@ -181,75 +183,10 @@ class TestService:
description="获取系统运行状态和资源使用情况",
params={}
),
- APITestCase(
- id="admin_config_list",
- name="查询配置列表",
- method="GET",
- path="/v1/admin/config",
- description="获取所有配置项列表",
- params={}
- ),
- APITestCase(
- id="admin_config_update",
- name="更新配置",
- method="PUT",
- path="/v1/admin/config",
- description="更新系统配置",
- body={
- "key": "server.mode",
- "value": "debug",
- "description": "服务器运行模式"
- }
- ),
- APITestCase(
- id="admin_reload_config",
- name="热加载配置",
- method="POST",
- path="/v1/admin/system/reload",
- description="重新加载配置文件",
- body={}
- ),
]
),
APITestCategory(
- name="适配器管理",
- items=[
- APITestCase(
- id="admin_adapters_list",
- name="适配器列表",
- method="GET",
- path="/v1/admin/adapters",
- description="获取所有数据源适配器列表",
- params={}
- ),
- APITestCase(
- id="admin_adapter_toggle",
- name="切换适配器状态",
- method="POST",
- path="/v1/admin/adapters/toggle",
- description="启用或禁用适配器",
- body={
- "name": "amazingdata",
- "enable": True
- }
- ),
- APITestCase(
- id="admin_adapter_config",
- name="更新适配器配置",
- method="PUT",
- path="/v1/admin/adapters/config",
- description="更新适配器配置参数",
- body={
- "name": "amazingdata",
- "config": {
- "timeout": "60"
- }
- }
- ),
- ]
- ),
- APITestCategory(
- name="数据同步接口",
+ name="【对外】数据同步接口",
items=[
APITestCase(
id="admin_data_sync_full",
@@ -270,7 +207,7 @@ class TestService:
name="同步基础K线数据",
method="POST",
path="/v1/admin/data/sync",
- description="仅同步基础K线数据(OHLCV)",
+ description="仅同步OHLCV基础数据",
body={
"symbols": ["000001.SZ"],
"sync_type": "base",
@@ -285,7 +222,7 @@ class TestService:
name="同步行情指标数据",
method="POST",
path="/v1/admin/data/sync",
- description="同步行情指标数据(均线/MACD/涨跌幅)",
+ description="同步均线/MACD/涨跌幅",
body={
"symbols": ["000001.SZ"],
"sync_type": "quote",
@@ -299,7 +236,7 @@ class TestService:
name="同步财务数据",
method="POST",
path="/v1/admin/data/sync",
- description="同步财务数据(市值/股本/利润)",
+ description="同步市值/股本/利润",
body={
"symbols": ["000001.SZ"],
"sync_type": "finance",
@@ -316,41 +253,241 @@ class TestService:
description="触发增量同步(最近30天)",
body=["000001.SZ", "600519.SH"]
),
+ ]
+ ),
+ ]
+
+ return APITestListData(categories=categories, base_url="")
+
+ def get_internal_test_list(self) -> APITestListData:
+ """获取内部接口测试列表(对内接口 - SDK封装层)"""
+ categories = [
+ APITestCategory(
+ name="【对内】市场数据接口 (_market_data)",
+ items=[
APITestCase(
- id="admin_data_sync_futures",
- name="期货数据同步",
- method="POST",
- path="/v1/admin/data/sync",
- description="同步期货数据",
- body={
- "symbols": ["CU2504.SHFE"],
- "sync_type": "full",
- "start_date": "20240301",
- "end_date": "20240310",
- "asset_class": "futures"
- }
+ id="internal_market_query_kline",
+ name="SDK: query_kline",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.market.query_kline",
+ description="查询K线数据(内部SDK调用)",
+ params={"symbol": "000001.SZ", "period": "1d"}
+ ),
+ APITestCase(
+ id="internal_market_query_snapshot",
+ name="SDK: query_snapshot",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.market.query_snapshot",
+ description="查询快照数据(内部SDK调用)",
+ params={"symbol": "000001.SZ"}
),
]
),
APITestCategory(
- name="测试管理",
+ name="【对内】基础数据接口 (_base_data)",
items=[
APITestCase(
- id="admin_test_history",
- name="测试历史",
- method="GET",
- path="/v1/admin/tests/history",
- description="获取测试执行历史记录",
- params={"type": "api", "limit": "20"}
+ id="internal_base_get_code_list",
+ name="SDK: get_code_list",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.base.get_code_list",
+ description="获取股票代码列表(内部SDK调用)",
+ params={}
+ ),
+ APITestCase(
+ id="internal_base_get_future_code_list",
+ name="SDK: get_future_code_list",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.base.get_future_code_list",
+ description="获取期货代码列表(内部SDK调用)",
+ params={}
+ ),
+ APITestCase(
+ id="internal_base_get_code_info",
+ name="SDK: get_code_info",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.base.get_code_info",
+ description="获取代码信息(内部SDK调用)",
+ params={}
+ ),
+ APITestCase(
+ id="internal_base_get_calendar",
+ name="SDK: get_calendar",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.base.get_calendar",
+ description="获取交易日历(内部SDK调用)",
+ params={}
+ ),
+ APITestCase(
+ id="internal_base_get_adj_factor",
+ name="SDK: get_adj_factor",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.base.get_adj_factor",
+ description="获取复权因子(内部SDK调用)",
+ params={}
+ ),
+ APITestCase(
+ id="internal_base_get_etf_pcf",
+ name="SDK: get_etf_pcf",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.base.get_etf_pcf",
+ description="获取ETF申赎数据(内部SDK调用)",
+ params={}
+ ),
+ ]
+ ),
+ APITestCategory(
+ name="【对内】股本股东接口 (_info_data)",
+ items=[
+ APITestCase(
+ id="internal_info_get_equity_structure",
+ name="SDK: get_equity_structure",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_equity_structure",
+ description="获取股本结构(内部SDK调用)",
+ params={"symbol": "000001.SZ"}
+ ),
+ APITestCase(
+ id="internal_info_get_share_holder",
+ name="SDK: get_share_holder",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_share_holder",
+ description="获取股东数据(内部SDK调用)",
+ params={"symbol": "000001.SZ"}
+ ),
+ APITestCase(
+ id="internal_info_get_holder_num",
+ name="SDK: get_holder_num",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_holder_num",
+ description="获取股东户数(内部SDK调用)",
+ params={"symbol": "000001.SZ"}
+ ),
+ ]
+ ),
+ APITestCategory(
+ name="【对内】财务报表接口 (_info_data)",
+ items=[
+ APITestCase(
+ id="internal_info_get_income",
+ name="SDK: get_income",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_income",
+ description="获取利润表(内部SDK调用)",
+ params={"symbol": "000001.SZ"}
+ ),
+ APITestCase(
+ id="internal_info_get_balance_sheet",
+ name="SDK: get_balance_sheet",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_balance_sheet",
+ description="获取资产负债表(内部SDK调用)",
+ params={"symbol": "000001.SZ"}
+ ),
+ APITestCase(
+ id="internal_info_get_cash_flow",
+ name="SDK: get_cash_flow",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_cash_flow",
+ description="获取现金流量表(内部SDK调用)",
+ params={"symbol": "000001.SZ"}
+ ),
+ ]
+ ),
+ APITestCategory(
+ name="【对内】市场状态接口 (_info_data)",
+ items=[
+ APITestCase(
+ id="internal_info_get_history_stock_status",
+ name="SDK: get_history_stock_status",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_history_stock_status",
+ description="获取历史股票状态(涨停/跌停/ST/停牌)(内部SDK调用)",
+ params={"symbol": "000001.SZ"}
+ ),
+ APITestCase(
+ id="internal_info_get_margin_summary",
+ name="SDK: get_margin_summary",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_margin_summary",
+ description="获取融资融券汇总(内部SDK调用)",
+ params={}
+ ),
+ APITestCase(
+ id="internal_info_get_margin_detail",
+ name="SDK: get_margin_detail",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_margin_detail",
+ description="获取融资融券明细(内部SDK调用)",
+ params={"symbol": "000001.SZ"}
+ ),
+ ]
+ ),
+ APITestCategory(
+ name="【对内】特色数据接口 (_info_data)",
+ items=[
+ APITestCase(
+ id="internal_info_get_long_hu_bang",
+ name="SDK: get_long_hu_bang",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_long_hu_bang",
+ description="获取龙虎榜数据(内部SDK调用)",
+ params={}
+ ),
+ APITestCase(
+ id="internal_info_get_block_trading",
+ name="SDK: get_block_trading",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_block_trading",
+ description="获取大宗交易数据(内部SDK调用)",
+ params={}
+ ),
+ APITestCase(
+ id="internal_info_get_index_constituent",
+ name="SDK: get_index_constituent",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_index_constituent",
+ description="获取指数成分股(内部SDK调用)",
+ params={"index": "000300.SH"}
+ ),
+ APITestCase(
+ id="internal_info_get_index_weight",
+ name="SDK: get_index_weight",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_index_weight",
+ description="获取指数权重(内部SDK调用)",
+ params={"index": "000300.SH"}
+ ),
+ ]
+ ),
+ APITestCategory(
+ name="【对内】基金可转债接口 (_info_data)",
+ items=[
+ APITestCase(
+ id="internal_info_get_fund_share",
+ name="SDK: get_fund_share",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_fund_share",
+ description="获取基金份额(内部SDK调用)",
+ params={}
+ ),
+ APITestCase(
+ id="internal_info_get_kzz_issuance",
+ name="SDK: get_kzz_issuance",
+ method="INTERNAL",
+ path="AmazingDataAdapter._internal.info.get_kzz_issuance",
+ description="获取可转债发行数据(内部SDK调用)",
+ params={}
),
]
),
]
return APITestListData(categories=categories, base_url="")
+
async def run_api_test(self, base_url: str, req: APITestRequest) -> APITestResult:
- """执行API测试"""
+ """执行对外API测试"""
# 获取测试用例
test_list = self.get_api_test_list()
@@ -444,6 +581,231 @@ class TestService:
self._add_api_history(result)
return result
+ async def run_internal_test(self, adapter, req: APITestRequest) -> APITestResult:
+ """执行内部接口测试(SDK封装层)"""
+ from app.adapters.amazingdata_adapter import AmazingDataAdapter
+
+ start_time = datetime.now()
+
+ try:
+ # 确保已连接
+ if not adapter._is_logged_in:
+ raise RuntimeError("Adapter not connected")
+
+ # 根据测试ID调用对应的内部接口
+ result_data = None
+ error_msg = None
+
+ if req.id == "internal_market_query_kline":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.market.query_kline(
+ code_list=[symbol],
+ begin_date=20240301,
+ end_date=20240310,
+ period=10000 # SDK只支持10000(日线)
+ )
+
+ elif req.id == "internal_market_query_snapshot":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.market.query_snapshot(
+ code_list=[symbol],
+ begin_date=20240301,
+ end_date=20240301
+ )
+
+ elif req.id == "internal_base_get_code_list":
+ from app.adapters.amazingdata_adapter import SecurityType
+ result_data = adapter._internal.base.get_code_list(
+ security_type=SecurityType.STOCK_A.value
+ )
+
+ elif req.id == "internal_base_get_future_code_list":
+ from app.adapters.amazingdata_adapter import SecurityType
+ result_data = adapter._internal.base.get_future_code_list(
+ security_type=SecurityType.FUTURE.value
+ )
+
+ elif req.id == "internal_base_get_code_info":
+ from app.adapters.amazingdata_adapter import SecurityType
+ result_data = adapter._internal.base.get_code_info(
+ security_type=SecurityType.STOCK_A.value
+ )
+
+ elif req.id == "internal_base_get_calendar":
+ result_data = adapter._internal.base.get_calendar(market="SH")
+
+ elif req.id == "internal_base_get_adj_factor":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.base.get_adj_factor(
+ code_list=[symbol],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_base_get_etf_pcf":
+ result_data = adapter._internal.base.get_etf_pcf(
+ code_list=["510050.SH"]
+ )
+
+ elif req.id == "internal_info_get_equity_structure":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.info.get_equity_structure(
+ code_list=[symbol],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_share_holder":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.info.get_share_holder(
+ code_list=[symbol],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_holder_num":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.info.get_holder_num(
+ code_list=[symbol],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_income":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.info.get_income(
+ code_list=[symbol],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_balance_sheet":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.info.get_balance_sheet(
+ code_list=[symbol],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_cash_flow":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.info.get_cash_flow(
+ code_list=[symbol],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_history_stock_status":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.info.get_history_stock_status(
+ code_list=[symbol],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache,
+ begin_date=20240301,
+ end_date=20240310
+ )
+
+ elif req.id == "internal_info_get_margin_summary":
+ result_data = adapter._internal.info.get_margin_summary(
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_margin_detail":
+ symbol = req.params.get("symbol", "000001.SZ")
+ result_data = adapter._internal.info.get_margin_detail(
+ code_list=[symbol],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_long_hu_bang":
+ result_data = adapter._internal.info.get_long_hu_bang(
+ code_list=["000001.SZ"],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_block_trading":
+ result_data = adapter._internal.info.get_block_trading(
+ code_list=["000001.SZ"],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_index_constituent":
+ index_code = req.params.get("index", "000300.SH")
+ result_data = adapter._internal.info.get_index_constituent(
+ code_list=[index_code],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_index_weight":
+ index_code = req.params.get("index", "000300.SH")
+ result_data = adapter._internal.info.get_index_weight(
+ code_list=[index_code],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_fund_share":
+ result_data = adapter._internal.info.get_fund_share(
+ code_list=["510050.SH"],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ elif req.id == "internal_info_get_kzz_issuance":
+ result_data = adapter._internal.info.get_kzz_issuance(
+ code_list=["110043.SH"],
+ local_path=adapter.config.local_path,
+ is_local=adapter.config.use_local_cache
+ )
+
+ else:
+ raise ValueError(f"Unknown internal test case: {req.id}")
+
+ latency = int((datetime.now() - start_time).total_seconds() * 1000)
+
+ # 检查结果
+ has_data = result_data is not None
+ if isinstance(result_data, (dict, list)):
+ has_data = len(result_data) > 0
+ elif isinstance(result_data, pd.DataFrame):
+ has_data = not result_data.empty
+
+ result = APITestResult(
+ id=int(datetime.now().timestamp()),
+ case_id=req.id,
+ name=req.id.replace("internal_", "").replace("_", ":"),
+ success=has_data,
+ status_code=200 if has_data else 204,
+ latency=latency,
+ request={"params": req.params},
+ response={"data_count": len(result_data) if hasattr(result_data, '__len__') else 1} if has_data else None,
+ error=None if has_data else "No data returned",
+ timestamp=datetime.now()
+ )
+
+ self._add_internal_history(result)
+ return result
+
+ except Exception as e:
+ latency = int((datetime.now() - start_time).total_seconds() * 1000)
+ result = APITestResult(
+ id=int(datetime.now().timestamp()),
+ case_id=req.id,
+ name=req.id.replace("internal_", "").replace("_", ":"),
+ success=False,
+ latency=latency,
+ request={"params": req.params},
+ error=str(e),
+ timestamp=datetime.now()
+ )
+ self._add_internal_history(result)
+ return result
+
def get_ws_test_list(self) -> WSTestListData:
"""获取WebSocket测试列表"""
cases = [
@@ -468,66 +830,12 @@ class TestService:
action="subscribe",
symbols=["000001.SZ", "000002.SZ", "CU2504.SHFE"]
),
- WSTestCase(
- id="ws_subscribe_many",
- name="压力测试-大量订阅",
- description="订阅大量标的测试性能",
- action="subscribe",
- symbols=[
- "000001.SZ", "000002.SZ", "000063.SZ", "000333.SZ",
- "000538.SZ", "000568.SZ", "000651.SZ", "000725.SZ",
- "000768.SZ", "000858.SZ"
- ]
- ),
- WSTestCase(
- id="ws_unsubscribe",
- name="取消订阅",
- description="取消订阅标的",
- action="unsubscribe",
- symbols=["000001.SZ"]
- ),
- WSTestCase(
- id="ws_unsubscribe_all",
- name="取消全部订阅",
- description="取消所有已订阅标的",
- action="unsubscribe",
- symbols=["000001.SZ", "000002.SZ", "CU2504.SHFE"]
- ),
- WSTestCase(
- id="ws_heartbeat",
- name="心跳检测",
- description="测试WebSocket连接心跳",
- action="subscribe",
- symbols=["000001.SZ"]
- ),
- WSTestCase(
- id="ws_invalid_symbol",
- name="无效标的测试",
- description="测试订阅无效标的的错误处理",
- action="subscribe",
- symbols=["INVALID.CODE"]
- ),
- WSTestCase(
- id="ws_empty_symbols",
- name="空订阅测试",
- description="测试空标的列表的处理",
- action="subscribe",
- symbols=[]
- ),
- WSTestCase(
- id="ws_resubscribe",
- name="重新订阅",
- description="取消后重新订阅同一标的",
- action="subscribe",
- symbols=["000001.SZ"]
- ),
]
return WSTestListData(cases=cases, ws_url="")
async def run_ws_test(self, ws_url: str, req: WSTestRequest) -> WSTestResult:
"""执行WebSocket测试"""
- # 获取测试用例
test_list = self.get_ws_test_list()
test_case = None
@@ -539,7 +847,6 @@ class TestService:
if not test_case:
raise ValueError(f"Test case not found: {req.id}")
- # 使用自定义标的
symbols = req.symbols if req.symbols else test_case.symbols
result = WSTestResult(
@@ -549,7 +856,6 @@ class TestService:
messages=[]
)
- # 连接WebSocket
start_time = datetime.now()
try:
@@ -560,14 +866,12 @@ class TestService:
result.latency = int((datetime.now() - start_time).total_seconds() * 1000)
result.success = True
- # 发送订阅消息
msg = {
"action": test_case.action,
"symbols": symbols
}
await ws.send(json.dumps(msg))
- # 等待响应(最多3条消息)
for _ in range(3):
try:
msg_data = await asyncio.wait_for(ws.recv(), timeout=5)
@@ -610,6 +914,13 @@ class TestService:
if len(self.api_history) > self.history_size:
self.api_history = self.api_history[-self.history_size:]
+ def _add_internal_history(self, result: APITestResult):
+ """添加内部接口测试历史"""
+ with self.lock:
+ self.internal_history.append(result)
+ if len(self.internal_history) > self.history_size:
+ self.internal_history = self.internal_history[-self.history_size:]
+
def _add_ws_history(self, result: WSTestResult):
"""添加WebSocket测试历史"""
with self.lock:
diff --git a/test_adapters.py b/test_adapters.py
deleted file mode 100644
index 75054ab..0000000
--- a/test_adapters.py
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env python3
-import urllib.request
-import json
-
-req = urllib.request.Request(
- 'http://localhost:8080/v1/admin/adapters',
- headers={'X-Admin-Token': ''}
-)
-
-try:
- response = urllib.request.urlopen(req, timeout=10)
- data = json.loads(response.read().decode())
- print('✓ Success!')
- print(f"Code: {data['code']}")
- print(f"Message: {data['message']}")
- print(f"Adapters count: {len(data['data']['adapters'])}")
- for adapter in data['data']['adapters']:
- print(f" - {adapter['name']}: {adapter['status']} ({adapter['type']})")
-except Exception as e:
- print(f'✗ Error: {e}')
diff --git a/test_adapters2.py b/test_adapters2.py
deleted file mode 100644
index e46de90..0000000
--- a/test_adapters2.py
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env python3
-import urllib.request
-import json
-
-req = urllib.request.Request(
- 'http://localhost:8080/v1/admin/adapters',
- headers={'X-Admin-Token': ''}
-)
-
-try:
- response = urllib.request.urlopen(req, timeout=10)
- data = json.loads(response.read().decode())
- print('Success!')
- print("Code:", data['code'])
- print("Adapters count:", len(data['data']['adapters']))
- for adapter in data['data']['adapters']:
- print(" -", adapter['name'] + ":", adapter['status'])
-except Exception as e:
- print('Error:', e)
diff --git a/test_db.py b/test_db.py
deleted file mode 100644
index 4e13b0c..0000000
--- a/test_db.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""测试数据库连接"""
-import os
-import sys
-
-# 添加项目根目录到路径
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-
-from app.repositories.database import init_db, engine
-from sqlalchemy import text
-
-print("=" * 50)
-print("数据库连接测试")
-print("=" * 50)
-
-try:
- # 测试连接
- with engine.connect() as conn:
- result = conn.execute(text("SELECT version()"))
- version = result.scalar()
- print(f"✅ 数据库连接成功")
- print(f" PostgreSQL 版本: {version}")
-
- # 初始化数据库(创建表)
- print("\n正在初始化数据库表...")
- init_db()
- print("✅ 数据库表创建成功")
-
- # 显示所有表
- from sqlalchemy import inspect
- inspector = inspect(engine)
- tables = inspector.get_table_names()
- print(f"\n已创建的表 ({len(tables)} 个):")
- for table in sorted(tables):
- print(f" - {table}")
-
- print("\n" + "=" * 50)
- print("数据库初始化完成!")
- print("=" * 50)
-
-except Exception as e:
- print(f"❌ 错误: {e}")
- print("\n请检查:")
- print("1. Docker 是否已启动: docker ps")
- print("2. 数据库端口是否正确: netstat -ano | findstr 5432")
- print("3. 数据库密码是否正确")
- sys.exit(1)
diff --git a/test_klines_api.py b/test_klines_api.py
deleted file mode 100644
index 8850997..0000000
--- a/test_klines_api.py
+++ /dev/null
@@ -1,112 +0,0 @@
-"""测试股票K线接口返回的字段"""
-import requests
-import json
-
-# API 配置
-BASE_URL = "http://localhost:8080/v1"
-API_KEY = ""
-
-# 测试获取股票K线
-def test_stock_klines():
- """测试股票K线接口返回的字段"""
- url = f"{BASE_URL}/stock/klines/000001.SZ"
- headers = {"X-API-Key": API_KEY}
- params = {
- "start": "20260301",
- "end": "20260310",
- "freq": "1d"
- }
-
- print(f"\n{'='*60}")
- print(f"测试接口: GET {url}")
- print(f"{'='*60}")
-
- try:
- response = requests.get(url, headers=headers, params=params)
- data = response.json()
-
- if data.get("code") == 0:
- kline_data = data.get("data", {})
- items = kline_data.get("items", [])
-
- print(f"\n标的: {kline_data.get('symbol')}")
- print(f"周期: {kline_data.get('freq')}")
- print(f"数据条数: {len(items)}")
- print(f"\n{'='*60}")
-
- if items:
- # 显示第一条数据的完整字段
- first_item = items[0]
- print("\n第一条数据详情:")
- print(f"{'-'*60}")
-
- # 基础字段
- print(f"时间戳: {first_item.get('time')}")
- print(f"开盘价: {first_item.get('open')}")
- print(f"最高价: {first_item.get('high')}")
- print(f"最低价: {first_item.get('low')}")
- print(f"收盘价: {first_item.get('close')}")
- print(f"成交量: {first_item.get('volume')}")
- print(f"成交额: {first_item.get('amount')}")
-
- # 扩展字段
- print(f"\n扩展字段:")
- print(f" 交易日: {first_item.get('trade_date')}")
- print(f" 是否涨停: {first_item.get('is_limit_up')}")
- print(f" 是否跌停: {first_item.get('is_limit_down')}")
- print(f" 总市值: {first_item.get('total_market_cap')}")
- print(f" 流通市值: {first_item.get('float_market_cap')}")
- print(f" 机构持仓占比: {first_item.get('inst_holding_ratio')}")
- print(f" 可交易日数: {first_item.get('trading_days')}")
- print(f" 创建时间: {first_item.get('created_at')}")
-
- # 验证所有字段是否存在
- expected_fields = [
- 'symbol', 'time', 'open', 'high', 'low', 'close',
- 'volume', 'amount', 'trade_date', 'is_limit_up',
- 'is_limit_down', 'total_market_cap', 'float_market_cap',
- 'inst_holding_ratio', 'trading_days', 'created_at'
- ]
-
- print(f"\n{'='*60}")
- print("字段完整性检查:")
- print(f"{'-'*60}")
-
- missing_fields = []
- for field in expected_fields:
- if field in first_item:
- print(f" ✓ {field}")
- else:
- print(f" ✗ {field} (缺失)")
- missing_fields.append(field)
-
- if missing_fields:
- print(f"\n缺失字段: {', '.join(missing_fields)}")
- else:
- print(f"\n所有字段都存在!")
-
- return True
- else:
- print("没有获取到数据")
- return False
- else:
- print(f"请求失败: {data.get('message')}")
- return False
-
- except Exception as e:
- print(f"请求异常: {e}")
- return False
-
-if __name__ == "__main__":
- print("\n" + "="*60)
- print("股票K线接口字段测试")
- print("="*60)
-
- success = test_stock_klines()
-
- print(f"\n{'='*60}")
- if success:
- print("测试完成!")
- else:
- print("测试失败!")
- print("="*60 + "\n")
diff --git a/test_klines_extended_fields.py b/test_klines_extended_fields.py
deleted file mode 100644
index ecf6926..0000000
--- a/test_klines_extended_fields.py
+++ /dev/null
@@ -1,133 +0,0 @@
-"""测试K线数据扩展字段获取"""
-import asyncio
-import os
-import sys
-
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-
-from app.adapters.amazingdata_adapter import AmazingDataAdapter
-from datetime import datetime
-
-
-async def test_klines_with_extended_fields():
- """测试获取带有扩展字段的K线数据"""
- print("\n" + "="*60)
- print("测试K线数据扩展字段")
- print("="*60)
-
- adapter = AmazingDataAdapter()
-
- # 连接配置(请根据实际情况修改)
- config = {
- "username": os.getenv("AMAZINGDATA_USERNAME", ""),
- "password": os.getenv("AMAZINGDATA_PASSWORD", ""),
- "host": os.getenv("AMAZINGDATA_HOST", "140.206.44.234"),
- "port": int(os.getenv("AMAZINGDATA_PORT", "8600")),
- "local_path": "./amazing_data_cache/",
- "use_local_cache": True
- }
-
- try:
- # 连接适配器
- print("\n[1/3] 正在连接 AmazingData...")
- await adapter.connect(config)
- print("✓ 连接成功")
-
- # 获取K线数据
- symbol = "000001.SZ" # 平安银行
- start_date = "20260301"
- end_date = "20260310"
-
- print(f"\n[2/3] 正在获取 {symbol} 的K线数据 ({start_date} ~ {end_date})...")
- klines = await adapter.fetch_klines(symbol, start_date, end_date, "1d")
- print(f"✓ 获取到 {len(klines)} 条K线数据")
-
- # 显示第一条数据的完整信息
- if klines:
- print(f"\n[3/3] 数据字段验证")
- print("-"*60)
-
- k = klines[0]
- print(f"\n标的代码: {k.symbol}")
- print(f"交易日: {k.trade_date}")
- print(f"时间戳: {datetime.fromtimestamp(k.time)}")
-
- print(f"\n基础行情:")
- print(f" 开盘价: {k.open}")
- print(f" 最高价: {k.high}")
- print(f" 最低价: {k.low}")
- print(f" 收盘价: {k.close}")
- print(f" 成交量: {k.volume}")
- print(f" 成交额: {k.amount}")
-
- print(f"\n扩展字段:")
- print(f" 是否涨停: {k.is_limit_up} {'✓' if k.is_limit_up is not None else '✗'}")
- print(f" 是否跌停: {k.is_limit_down} {'✓' if k.is_limit_down is not None else '✗'}")
- print(f" 总市值: {k.total_market_cap:,.0f} 元" if k.total_market_cap else " 总市值: None ✗")
- print(f" 流通市值: {k.float_market_cap:,.0f} 元" if k.float_market_cap else " 流通市值: None ✗")
- print(f" 机构持仓占比: {k.inst_holding_ratio}%" if k.inst_holding_ratio else " 机构持仓占比: None")
- print(f" 可交易日数: {k.trading_days} {'✓' if k.trading_days else '✗'}")
-
- # 验证字段完整性
- print(f"\n{'='*60}")
- print("字段完整性检查:")
- print("-"*60)
-
- checks = [
- ("symbol", k.symbol is not None),
- ("time", k.time > 0),
- ("open", k.open > 0),
- ("high", k.high > 0),
- ("low", k.low > 0),
- ("close", k.close > 0),
- ("volume", k.volume > 0),
- ("amount", k.amount > 0),
- ("trade_date", k.trade_date is not None),
- ("is_limit_up", k.is_limit_up is not None),
- ("is_limit_down", k.is_limit_down is not None),
- ("total_market_cap", k.total_market_cap is not None and k.total_market_cap > 0),
- ("float_market_cap", k.float_market_cap is not None and k.float_market_cap > 0),
- ("trading_days", k.trading_days is not None and k.trading_days > 0),
- ]
-
- passed = 0
- for field, check in checks:
- status = "✓" if check else "✗"
- print(f" {status} {field}")
- if check:
- passed += 1
-
- print(f"\n通过: {passed}/{len(checks)}")
-
- # 显示涨跌停判断逻辑验证
- print(f"\n{'='*60}")
- print("涨跌停判断示例:")
- print("-"*60)
- for k in klines[:3]: # 显示前3条
- limit_status = ""
- if k.is_limit_up:
- limit_status = "📈 涨停"
- elif k.is_limit_down:
- limit_status = "📉 跌停"
- else:
- limit_status = "—"
- print(f" {k.trade_date}: 收盘{k.close} {limit_status}")
-
- # 断开连接
- await adapter.close()
- print(f"\n{'='*60}")
- print("测试完成!")
- print("="*60 + "\n")
-
- return True
-
- except Exception as e:
- print(f"\n✗ 测试失败: {e}")
- import traceback
- traceback.print_exc()
- return False
-
-
-if __name__ == "__main__":
- success = asyncio.run(test_klines_with_extended_fields())
- sys.exit(0 if success else 1)
diff --git a/test_sdk_output.txt b/test_sdk_output.txt
new file mode 100644
index 0000000..5fe2667
Binary files /dev/null and b/test_sdk_output.txt differ
diff --git a/test_source.py b/test_source.py
deleted file mode 100644
index b504ff3..0000000
--- a/test_source.py
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env python3
-import urllib.request
-import json
-
-# Test source status
-req = urllib.request.Request(
- 'http://localhost:8080/v1/admin/source/status',
- headers={'X-API-Key': ''}
-)
-
-try:
- response = urllib.request.urlopen(req, timeout=10)
- data = json.loads(response.read().decode())
- print('Source Status:')
- print(json.dumps(data, indent=2, ensure_ascii=False))
-except Exception as e:
- print('Error:', e)
diff --git a/venv/Lib/site-packages/uvicorn/__pycache__/__main__.cpython-311.pyc b/venv/Lib/site-packages/uvicorn/__pycache__/__main__.cpython-311.pyc
new file mode 100644
index 0000000..06c0b19
Binary files /dev/null and b/venv/Lib/site-packages/uvicorn/__pycache__/__main__.cpython-311.pyc differ