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 }}
+
+
+
- 股票K线图 - {{ queryForm.code }}
+ 股票K线图 - {{ stockBasic?.name || queryForm.code }}
@@ -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' }
]