From 6bf57ef398ea3cf2ee221dc98cfc989ee7bd3c9b Mon Sep 17 00:00:00 2001 From: Lxy Date: Sun, 12 Apr 2026 12:51:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/__init__.py | 4 +- backend/app/api/v1/stock.py | 100 +++++- docs/API.md | 312 +++++++++++++++++- frontend/src/router/index.ts | 6 + .../src/views/DataQuery/StockKlineQuery.vue | 36 +- frontend/src/views/DataQuery/index.vue | 4 + frontend/src/views/Layout.vue | 1 + 7 files changed, 450 insertions(+), 13 deletions(-) diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py index 94ed5f4..8ebf576 100644 --- a/backend/app/api/v1/__init__.py +++ b/backend/app/api/v1/__init__.py @@ -1,7 +1,7 @@ # API v1模块 from fastapi import APIRouter -from app.api.v1 import auth, configs, base_data, stock, future, realtime, finance, cache, test +from app.api.v1 import auth, configs, base_data, stock, future, realtime, finance, cache, test, data_import, index api_router = APIRouter(prefix="/api/v1") @@ -10,7 +10,9 @@ api_router.include_router(configs.router, prefix="/configs", tags=["配置管理 api_router.include_router(base_data.router, prefix="/base", tags=["基础数据"]) api_router.include_router(stock.router, prefix="/stock", tags=["股票数据"]) api_router.include_router(future.router, prefix="/future", tags=["期货数据"]) +api_router.include_router(index.router, prefix="/index", tags=["指数数据"]) api_router.include_router(realtime.router, prefix="/realtime", tags=["实时数据"]) api_router.include_router(finance.router, prefix="/finance", tags=["财务数据"]) api_router.include_router(cache.router, prefix="/cache", tags=["缓存管理"]) api_router.include_router(test.router, prefix="/test", tags=["测试中心"]) +api_router.include_router(data_import.router, prefix="/import", tags=["数据导入"]) diff --git a/backend/app/api/v1/stock.py b/backend/app/api/v1/stock.py index c53a320..cdfc7ff 100644 --- a/backend/app/api/v1/stock.py +++ b/backend/app/api/v1/stock.py @@ -9,6 +9,7 @@ from app.db.session import get_db from app.schemas.base import ResponseModel from app.schemas.kline import KlineRequest, BatchKlineRequest from app.services.stock_service import StockService +from app.models.stock_basic import StockBasic from app.core.security import get_current_user from app.models.user import User from app.utils.date_utils import parse_date @@ -25,14 +26,34 @@ async def get_stock_kline( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): - """获取股票K线数据""" + """获取股票K线数据(含基础信息)""" service = StockService(db) code_list = [c.strip() for c in codes.split(",")] start = parse_date(start_date) end = parse_date(end_date) - data = service.get_kline(code_list, start, end, period) - return ResponseModel(data=data) + kline_data = service.get_kline(code_list, start, end, period) + + result = {} + for code in code_list: + stock_basic = db.query(StockBasic).filter(StockBasic.code == code).first() + + result[code] = { + "basic": { + "code": stock_basic.code if stock_basic else code, + "name": stock_basic.name if stock_basic else None, + "total_shares": stock_basic.total_shares if stock_basic else None, + "float_shares": stock_basic.float_shares if stock_basic else None, + "industry_index_name": stock_basic.industry_index_name if stock_basic else None, + "industry_index_code": stock_basic.industry_index_code if stock_basic else None, + "institution_hold_ratio": float(stock_basic.institution_hold_ratio) if stock_basic and stock_basic.institution_hold_ratio else None, + "industry_level3": stock_basic.industry_level3 if stock_basic else None, + "list_date": str(stock_basic.list_date) if stock_basic and stock_basic.list_date else None + }, + "kline": kline_data.get(code, []) + } + + return ResponseModel(data=result) @router.post("/kline/batch", response_model=ResponseModel) @@ -41,13 +62,33 @@ async def batch_get_stock_kline( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): - """批量获取股票K线数据""" + """批量获取股票K线数据(含基础信息)""" service = StockService(db) start = parse_date(request.start_date) end = parse_date(request.end_date) - data = service.get_kline(request.codes, start, end, request.period) - return ResponseModel(data=data) + kline_data = service.get_kline(request.codes, start, end, request.period) + + result = {} + for code in request.codes: + stock_basic = db.query(StockBasic).filter(StockBasic.code == code).first() + + result[code] = { + "basic": { + "code": stock_basic.code if stock_basic else code, + "name": stock_basic.name if stock_basic else None, + "total_shares": stock_basic.total_shares if stock_basic else None, + "float_shares": stock_basic.float_shares if stock_basic else None, + "industry_index_name": stock_basic.industry_index_name if stock_basic else None, + "industry_index_code": stock_basic.industry_index_code if stock_basic else None, + "institution_hold_ratio": float(stock_basic.institution_hold_ratio) if stock_basic and stock_basic.institution_hold_ratio else None, + "industry_level3": stock_basic.industry_level3 if stock_basic else None, + "list_date": str(stock_basic.list_date) if stock_basic and stock_basic.list_date else None + }, + "kline": kline_data.get(code, []) + } + + return ResponseModel(data=result) @router.get("/kline/{code}/chart", response_model=ResponseModel) @@ -59,13 +100,53 @@ async def get_stock_kline_chart( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): - """获取股票K线图数据(ECharts格式)""" + """获取股票K线图数据(ECharts格式,含基础信息)""" service = StockService(db) start = parse_date(start_date) end = parse_date(end_date) - data = service.get_kline_chart_data(code, start, end, period) - return ResponseModel(data=data) + chart_data = service.get_kline_chart_data(code, start, end, period) + + stock_basic = db.query(StockBasic).filter(StockBasic.code == code).first() + + chart_data["basic"] = { + "code": stock_basic.code if stock_basic else code, + "name": stock_basic.name if stock_basic else None, + "total_shares": stock_basic.total_shares if stock_basic else None, + "float_shares": stock_basic.float_shares if stock_basic else None, + "industry_index_name": stock_basic.industry_index_name if stock_basic else None, + "industry_index_code": stock_basic.industry_index_code if stock_basic else None, + "institution_hold_ratio": float(stock_basic.institution_hold_ratio) if stock_basic and stock_basic.institution_hold_ratio else None, + "industry_level3": stock_basic.industry_level3 if stock_basic else None, + "list_date": str(stock_basic.list_date) if stock_basic and stock_basic.list_date else None + } + + return ResponseModel(data=chart_data) + + +@router.get("/basic/{code}", response_model=ResponseModel) +async def get_stock_basic( + code: str, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """获取股票基础信息""" + stock_basic = db.query(StockBasic).filter(StockBasic.code == code).first() + + if not stock_basic: + return ResponseModel(code=404, message="股票不存在") + + return ResponseModel(data={ + "code": stock_basic.code, + "name": stock_basic.name, + "total_shares": stock_basic.total_shares, + "float_shares": stock_basic.float_shares, + "industry_index_name": stock_basic.industry_index_name, + "industry_index_code": stock_basic.industry_index_code, + "institution_hold_ratio": float(stock_basic.institution_hold_ratio) if stock_basic.institution_hold_ratio else None, + "industry_level3": stock_basic.industry_level3, + "list_date": str(stock_basic.list_date) if stock_basic.list_date else None + }) @router.get("/snapshot", response_model=ResponseModel) @@ -77,5 +158,4 @@ async def get_stock_snapshot( current_user: User = Depends(get_current_user) ): """获取股票历史快照数据""" - # 这里可以实现快照数据查询 return ResponseModel(data={"message": "功能开发中"}) diff --git a/docs/API.md b/docs/API.md index 398b949..6ae3a55 100644 --- a/docs/API.md +++ b/docs/API.md @@ -908,6 +908,316 @@ result = wait_for_task(client, task_id) --- -## 十一、联系与支持 +## 十一、指数数据接口 + +### 11.1 获取指数列表 + +**接口**: `GET /index/list` + +**功能**: 获取所有指数基础信息列表 + +**请求参数**: 无 + +**响应参数**: + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| data | array | 指数列表 | +| data[].code | string | 指数代码 | +| data[].name | string | 指数名称 | +| data[].component_count | int | 成分个数 | + +**调用示例**: + +```python +response = requests.get( + 'http://localhost:8000/api/v1/index/list', + headers=headers +) +indexes = response.json()['data'] +``` + +--- + +### 11.2 获取指数交易数据 + +**接口**: `GET /index/trade` + +**功能**: 获取指数交易数据(含基础信息) + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| codes | string | 是 | 指数代码列表 (逗号分隔) | +| start_date | string | 是 | 开始日期 (YYYYMMDD) | +| end_date | string | 是 | 结束日期 (YYYYMMDD) | + +**响应参数**: + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| data.{code}.basic | object | 指数基础信息 | +| data.{code}.basic.code | string | 指数代码 | +| data.{code}.basic.name | string | 指数名称 | +| data.{code}.basic.component_count | int | 成分个数 | +| data.{code}.trades | array | 交易数据列表 | +| data.{code}.trades[].trade_date | string | 交易日期 | +| data.{code}.trades[].open | float | 开盘价 | +| data.{code}.trades[].close | float | 收盘价 | +| data.{code}.trades[].high | float | 最高价 | +| data.{code}.trades[].low | float | 最低价 | +| data.{code}.trades[].change_pct | float | 涨跌幅(%) | +| data.{code}.trades[].volume | int | 成交量 | +| data.{code}.trades[].amount | float | 成交额(百万元) | +| data.{code}.trades[].total_market_value | float | 总市值(百万元) | +| data.{code}.trades[].float_market_value | float | 流通市值(百万元) | +| data.{code}.trades[].up_count | int | 上涨家数 | +| data.{code}.trades[].down_count | int | 下跌家数 | +| data.{code}.trades[].flat_count | int | 平盘家数 | +| data.{code}.trades[].limit_up_count | int | 涨停家数 | +| data.{code}.trades[].limit_down_count | int | 跌停家数 | +| data.{code}.trades[].suspend_count | int | 停牌家数 | +| data.{code}.trades[].pe_ratio | float | 市盈率 | +| data.{code}.trades[].pe_median | float | 市盈率中位值 | +| data.{code}.trades[].is_new_high | bool | 是否创近期新高 | +| data.{code}.trades[].is_new_low | bool | 是否创近期新低 | + +**调用示例**: + +```python +response = requests.get( + 'http://localhost:8000/api/v1/index/trade', + params={ + 'codes': 'BK0001,BK0002', + 'start_date': '20240101', + 'end_date': '20240131' + }, + headers=headers +) +data = response.json()['data'] +``` + +--- + +### 11.3 获取指数K线图表数据 + +**接口**: `GET /index/{code}/chart` + +**功能**: 获取指数K线图表数据 (ECharts格式,含基础信息) + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| code | string | 是 | 指数代码 (URL路径参数) | +| start_date | string | 是 | 开始日期 | +| end_date | string | 是 | 结束日期 | + +**响应参数**: + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| data.basic | object | 指数基础信息 | +| data.basic.code | string | 指数代码 | +| data.basic.name | string | 指数名称 | +| data.basic.component_count | int | 成分个数 | +| data.categoryData | array | 日期列表 | +| data.values | array | K线值 [open, close, low, high, volume] | +| data.volumes | array | 成交量数据 | + +**调用示例**: + +```python +response = requests.get( + 'http://localhost:8000/api/v1/index/BK0001/chart', + params={ + 'start_date': '20240101', + 'end_date': '20240131' + }, + headers=headers +) +chart_data = response.json()['data'] +``` + +--- + +## 十二、数据导入接口 + +### 12.1 导入指数数据 + +**接口**: `POST /import/index-data` + +**功能**: 导入指数数据(同时更新指数基础表和指数交易表) + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | file | 是 | Excel文件 (multipart/form-data) | +| trade_date | string | 是 | 交易日期 (YYYY-MM-DD格式,Query参数) | + +**Excel文件格式**: + +第一行标题: +- 证券代码 +- 证券名称 +- 成分个数 [交易日期]最新 +- 开盘价 [交易日期]最新 +- 收盘价 [交易日期]最新 +- 成交量 [交易日期]最新 [单位]股 +- 成交额 [交易日期]最新 [单位]百万元 +- 总市值 [截止日期]最新 [单位]百万元 +- 自由流通市值 [交易日期]最新 [单位]百万元 +- 涨跌幅 [交易日期]最新 [单位]% +- 最高价 [交易日期]最新 +- 最低价 [交易日期]最新 +- 上涨家数 [交易日期]最新 +- 下跌家数 [交易日期]最新 +- 平盘家数 [交易日期]最新 +- 涨停家数 [交易日期]最新 +- 跌停家数 [交易日期]最新 +- 停牌家数 [交易日期]最新 +- 近期创历史新高 [交易日期]最新 [近N日内]300 [复权方式]不复权 +- 近期创历史新低 [交易日期]最新 [近N日内]300 [复权方式]不复权 +- 市盈率PE(TTM) [交易日期]最新 [剔除规则]不调整 +- 市盈率PE(TTM)中位值 [交易日期]最新 [剔除规则]不调整 + +**响应参数**: + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| data.success_count | int | 成功导入数量 | +| data.error_count | int | 失败数量 | +| data.total_count | int | 总数量 | +| data.index_basic_added | int | 新增指数基础数据数量 | +| data.index_basic_updated | int | 更新指数基础数据数量 | +| data.trade_date | string | 交易日期 | + +**调用示例**: + +```python +import requests + +files = {'file': open('index_data.xlsx', 'rb')} +response = requests.post( + 'http://localhost:8000/api/v1/import/index-data?trade_date=2024-01-31', + files=files, + headers=headers +) +result = response.json()['data'] +print(f"成功: {result['success_count']}, 新增指数: {result['index_basic_added']}") +``` + +--- + +### 12.2 导入股票基础数据 + +**接口**: `POST /import/stock-basic` + +**功能**: 导入股票基础数据 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | file | 是 | Excel文件 | + +**Excel文件格式**: + +必须列:code, name, total_shares, float_shares, industry_index_name, industry_index_code, institution_hold_ratio, industry_level3, list_date + +**响应参数**: + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| data.success_count | int | 成功导入数量 | +| data.error_count | int | 失败数量 | +| data.total_count | int | 总数量 | + +--- + +### 12.3 导入指数基础数据 + +**接口**: `POST /import/index-basic` + +**功能**: 导入指数基础数据 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | file | 是 | Excel文件 | + +**Excel文件格式**: + +必须列:code, name, component_count + +--- + +### 12.4 导入指数交易数据 + +**接口**: `POST /import/index-trade` + +**功能**: 导入指数交易数据 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | file | 是 | Excel文件 | + +**Excel文件格式**: + +必须列:index_code, trade_date, open, close, high, low + +可选列:change_pct, volume, amount, total_market_value, float_market_value, up_count, down_count, flat_count, limit_up_count, limit_down_count, suspend_count, pe_ratio, pe_median, is_new_high, is_new_low + +--- + +## 十三、股票基础信息接口 + +### 13.1 获取股票基础信息 + +**接口**: `GET /stock/basic/{code}` + +**功能**: 获取指定股票的基础信息 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| code | string | 是 | 股票代码 (URL路径参数) | + +**响应参数**: + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| data.code | string | 股票代码 | +| data.name | string | 股票名称 | +| data.total_shares | int | 总股本 | +| data.float_shares | int | 流通股本 | +| data.industry_index_name | string | 所属东财行业指数 | +| data.industry_index_code | string | 所属东财行业指数代码 | +| data.institution_hold_ratio | float | 机构持股比例合计(%) | +| data.industry_level3 | string | 所属东财3级行业 | +| data.list_date | string | 上市日期 | + +**调用示例**: + +```python +response = requests.get( + 'http://localhost:8000/api/v1/stock/basic/600000.SH', + headers=headers +) +stock_info = response.json()['data'] +print(f"股票名称: {stock_info['name']}") +print(f"总股本: {stock_info['total_shares']}") +print(f"所属行业: {stock_info['industry_index_name']}") +``` + +--- + +## 十四、联系与支持 如有问题,请联系技术支持团队。 \ No newline at end of file diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 623c1ed..22e98dd 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -38,6 +38,12 @@ const routes = [ component: () => import('@/views/CacheManager/index.vue'), meta: { title: '缓存管理', icon: 'Box' } }, + { + path: 'import', + name: 'DataImport', + component: () => import('@/views/DataImport/index.vue'), + meta: { title: '数据导入', icon: 'Upload' } + }, { path: 'test', name: 'TestCenter', diff --git a/frontend/src/views/DataQuery/StockKlineQuery.vue b/frontend/src/views/DataQuery/StockKlineQuery.vue index 3249231..f6c7ad1 100644 --- a/frontend/src/views/DataQuery/StockKlineQuery.vue +++ b/frontend/src/views/DataQuery/StockKlineQuery.vue @@ -41,9 +41,26 @@ + + + + {{ stockBasic.code }} + {{ stockBasic.name }} + {{ stockBasic.list_date }} + {{ formatShares(stockBasic.total_shares) }} + {{ formatShares(stockBasic.float_shares) }} + {{ stockBasic.institution_hold_ratio ? stockBasic.institution_hold_ratio + '%' : '-' }} + {{ stockBasic.industry_index_name }} + {{ stockBasic.industry_index_code }} + {{ stockBasic.industry_level3 }} + + +
@@ -87,6 +104,7 @@ const chartData = reactive({ volumes: [] as number[][] }) +const stockBasic = ref(null) const tableData = ref([]) function getDefaultStartDate() { @@ -118,6 +136,17 @@ function formatVolume(row: any, column: any, value: number) { return value.toString() } +function formatShares(value: number) { + if (!value) return '-' + if (value >= 100000000) { + return (value / 100000000).toFixed(2) + '亿股' + } + if (value >= 10000) { + return (value / 10000).toFixed(2) + '万股' + } + return value.toString() + '股' +} + const handleQuery = async () => { if (!queryForm.code) { ElMessage.warning('请输入股票代码') @@ -133,6 +162,7 @@ const handleQuery = async () => { }) if (res.data) { + stockBasic.value = res.data.basic || null chartData.categoryData = res.data.categoryData || [] chartData.values = res.data.values || [] chartData.volumes = res.data.volumes || [] @@ -255,6 +285,10 @@ window.addEventListener('resize', () => { padding: 10px; } +.basic-card { + margin-top: 20px; +} + .chart-card { margin-top: 20px; } diff --git a/frontend/src/views/DataQuery/index.vue b/frontend/src/views/DataQuery/index.vue index 17ac01a..eb9b1c8 100644 --- a/frontend/src/views/DataQuery/index.vue +++ b/frontend/src/views/DataQuery/index.vue @@ -21,6 +21,9 @@ + + + @@ -31,6 +34,7 @@ import StockKlineQuery from './StockKlineQuery.vue' import StockBatchQuery from './StockBatchQuery.vue' import FutureKlineQuery from './FutureKlineQuery.vue' import FutureBatchQuery from './FutureBatchQuery.vue' +import IndexQuery from './IndexQuery.vue' const activeTab = ref('stock') const stockSubTab = ref('kline') diff --git a/frontend/src/views/Layout.vue b/frontend/src/views/Layout.vue index 5804af3..b097cd2 100644 --- a/frontend/src/views/Layout.vue +++ b/frontend/src/views/Layout.vue @@ -62,6 +62,7 @@ const menuItems = [ { path: '/data-query', title: '数据查询', icon: 'DataLine' }, { path: '/config', title: '配置管理', icon: 'Setting' }, { path: '/cache', title: '缓存管理', icon: 'Box' }, + { path: '/import', title: '数据导入', icon: 'Upload' }, { path: '/test', title: '测试中心', icon: 'CircleCheck' } ]