fix: 增加数据库配置

master
Lxy 2 months ago
parent 6bf57ef398
commit 1106b3375f

@ -156,3 +156,140 @@ async def set_default_config(
"""设为默认配置""" """设为默认配置"""
ConfigService.set_default_config(db, config_id) ConfigService.set_default_config(db, config_id)
return ResponseModel(message="设置成功") return ResponseModel(message="设置成功")
@router.get("/system", response_model=ResponseModel[dict])
async def get_system_configs(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取系统配置数据库、Redis等"""
configs = {
"database": ConfigService.get_system_config(db, "DATABASE_URL") or "sqlite:///./amazing_data.db",
"redis": ConfigService.get_system_config(db, "REDIS_URL") or "redis://localhost:6379/0"
}
return ResponseModel(data=configs)
@router.put("/system", response_model=ResponseModel)
async def update_system_configs(
configs: dict,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""更新系统配置"""
if "database" in configs:
ConfigService.set_system_config(
db,
"DATABASE_URL",
configs["database"],
"数据库连接URL"
)
if "redis" in configs:
ConfigService.set_system_config(
db,
"REDIS_URL",
configs["redis"],
"Redis连接URL"
)
return ResponseModel(message="更新成功")
@router.post("/system/test", response_model=ResponseModel[dict])
async def test_system_connection(
configs: dict,
current_user: User = Depends(get_current_user)
):
"""测试系统连接数据库和Redis"""
import sqlalchemy
import redis
result = {
"database": False,
"redis": False
}
# 测试数据库连接
if "database" in configs:
try:
engine = sqlalchemy.create_engine(
configs["database"],
connect_args={"check_same_thread": False} if "sqlite" in configs["database"] else {}
)
with engine.connect() as conn:
result["database"] = True
except Exception as e:
pass
# 测试Redis连接
if "redis" in configs:
try:
redis_client = redis.from_url(configs["redis"])
redis_client.ping()
result["redis"] = True
except Exception as e:
pass
return ResponseModel(data=result)
@router.post("/system/init", response_model=ResponseModel[dict])
async def init_system_database(
current_user: User = Depends(get_current_user)
):
"""初始化数据库结构"""
try:
from app.db.session import init_db
init_db()
return ResponseModel(data={"success": True})
except Exception as e:
return ResponseModel(
code=1001,
message=str(e),
data={"success": False}
)
@router.get("/system/structure", response_model=ResponseModel[dict])
async def check_database_structure(
current_user: User = Depends(get_current_user)
):
"""检测数据库结构是否完整"""
try:
from sqlalchemy import inspect
from app.db.session import engine
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
# 检查必要的表是否存在
required_tables = [
'users',
'sdk_configs',
'system_configs',
'stock_kline_daily',
'stock_kline_minute',
'future_kline_daily',
'future_kline_minute',
'cache_tasks',
'stock_basic',
'index_basic',
'index_trade'
]
missing_tables = [table for table in required_tables if table not in existing_tables]
complete = len(missing_tables) == 0
return ResponseModel(data={
"complete": complete,
"missing_tables": missing_tables,
"existing_tables": existing_tables
})
except Exception as e:
return ResponseModel(
code=1001,
message=str(e),
data={"complete": False, "missing_tables": [], "existing_tables": []}
)

@ -0,0 +1,457 @@
"""
数据导入路由
"""
import pandas as pd
import logging
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
from sqlalchemy.orm import Session
from datetime import datetime, date
from app.db.session import get_db
from app.schemas.base import ResponseModel
from app.models.stock_basic import StockBasic, IndexBasic, IndexTrade
from app.core.security import get_current_user
from app.models.user import User
router = APIRouter()
logger = logging.getLogger(__name__)
INDEX_TRADE_COLUMN_MAP = {
'证券代码': 'index_code',
'证券名称': 'name',
'成分个数 [交易日期]最新': 'component_count',
'开盘价 [交易日期]最新': 'open',
'收盘价 [交易日期]最新': 'close',
'成交量 [交易日期]最新 [单位]股': 'volume',
'成交额 [交易日期]最新 [单位]百万元': 'amount',
'总市值 [截止日期]最新 [单位]百万元': 'total_market_value',
'自由流通市值 [交易日期]最新 [单位]百万元': 'float_market_value',
'涨跌幅 [交易日期]最新 [单位]%': 'change_pct',
'最高价 [交易日期]最新': 'high',
'最低价 [交易日期]最新': 'low',
'上涨家数 [交易日期]最新': 'up_count',
'下跌家数 [交易日期]最新': 'down_count',
'平盘家数 [交易日期]最新': 'flat_count',
'涨停家数 [交易日期]最新': 'limit_up_count',
'跌停家数 [交易日期]最新': 'limit_down_count',
'停牌家数 [交易日期]最新': 'suspend_count',
'近期创历史新高 [交易日期]最新 [近N日内]300 [复权方式]不复权': 'is_new_high',
'近期创历史新低 [交易日期]最新 [近N日内]300 [复权方式]不复权': 'is_new_low',
'市盈率PE(TTM) [交易日期]最新 [剔除规则]不调整': 'pe_ratio',
'市盈率PE(TTM)中位值 [交易日期]最新 [剔除规则]不调整': 'pe_median'
}
@router.post("/index-data", response_model=ResponseModel)
async def import_index_data(
file: UploadFile = File(...),
trade_date: str = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""导入指数数据(同时更新指数基础表和指数交易表)"""
if not file.filename.endswith(('.xls', '.xlsx')):
raise HTTPException(status_code=400, detail="只支持xls或xlsx格式文件")
if not trade_date:
raise HTTPException(status_code=400, detail="请提供交易日期参数(YYYY-MM-DD格式)")
try:
trade_date_obj = datetime.strptime(trade_date, '%Y-%m-%d').date()
except:
raise HTTPException(status_code=400, detail="交易日期格式错误请使用YYYY-MM-DD格式")
try:
df = pd.read_excel(file.file)
df.columns = df.columns.str.strip()
renamed_df = df.rename(columns=INDEX_TRADE_COLUMN_MAP)
if 'index_code' not in renamed_df.columns:
raise HTTPException(status_code=400, detail="缺少必要列:证券代码")
success_count = 0
error_count = 0
index_basic_updated = 0
index_basic_added = 0
for _, row in renamed_df.iterrows():
try:
index_code = str(row['index_code']).strip()
if not index_code:
continue
name = str(row.get('name', '')) if pd.notna(row.get('name')) else None
component_count = int(row.get('component_count')) if pd.notna(row.get('component_count')) else None
index_basic = db.query(IndexBasic).filter(IndexBasic.code == index_code).first()
if index_basic:
if component_count and index_basic.component_count != component_count:
index_basic.component_count = component_count
index_basic.name = name or index_basic.name
index_basic.updated_at = datetime.utcnow()
index_basic_updated += 1
else:
index_basic = IndexBasic(
code=index_code,
name=name,
component_count=component_count
)
db.add(index_basic)
db.flush()
index_basic_added += 1
existing_trade = db.query(IndexTrade).filter(
IndexTrade.index_code == index_code,
IndexTrade.trade_date == trade_date_obj
).first()
def get_float_val(col_name):
val = row.get(col_name)
if pd.notna(val):
try:
return float(val)
except:
return None
return None
def get_int_val(col_name):
val = row.get(col_name)
if pd.notna(val):
try:
return int(float(val))
except:
return None
return None
def get_bool_val(col_name):
val = row.get(col_name)
if pd.notna(val):
if isinstance(val, bool):
return val
if isinstance(val, str):
return val.lower() in ['true', '1', 'yes', '']
return bool(val)
return False
open_price = get_float_val('open')
close_price = get_float_val('close')
high_price = get_float_val('high')
low_price = get_float_val('low')
change_pct = get_float_val('change_pct')
volume = get_int_val('volume')
amount = get_float_val('amount')
total_market_value = get_float_val('total_market_value')
float_market_value = get_float_val('float_market_value')
up_count = get_int_val('up_count')
down_count = get_int_val('down_count')
flat_count = get_int_val('flat_count')
limit_up_count = get_int_val('limit_up_count')
limit_down_count = get_int_val('limit_down_count')
suspend_count = get_int_val('suspend_count')
pe_ratio = get_float_val('pe_ratio')
pe_median = get_float_val('pe_median')
is_new_high = get_bool_val('is_new_high')
is_new_low = get_bool_val('is_new_low')
if existing_trade:
existing_trade.open = open_price
existing_trade.close = close_price
existing_trade.high = high_price
existing_trade.low = low_price
existing_trade.change_pct = change_pct
existing_trade.volume = volume
existing_trade.amount = amount
existing_trade.total_market_value = total_market_value
existing_trade.float_market_value = float_market_value
existing_trade.up_count = up_count
existing_trade.down_count = down_count
existing_trade.flat_count = flat_count
existing_trade.limit_up_count = limit_up_count
existing_trade.limit_down_count = limit_down_count
existing_trade.suspend_count = suspend_count
existing_trade.pe_ratio = pe_ratio
existing_trade.pe_median = pe_median
existing_trade.is_new_high = is_new_high
existing_trade.is_new_low = is_new_low
existing_trade.updated_at = datetime.utcnow()
else:
trade = IndexTrade(
index_code=index_code,
trade_date=trade_date_obj,
open=open_price,
close=close_price,
high=high_price,
low=low_price,
change_pct=change_pct,
volume=volume,
amount=amount,
total_market_value=total_market_value,
float_market_value=float_market_value,
up_count=up_count,
down_count=down_count,
flat_count=flat_count,
limit_up_count=limit_up_count,
limit_down_count=limit_down_count,
suspend_count=suspend_count,
pe_ratio=pe_ratio,
pe_median=pe_median,
is_new_high=is_new_high,
is_new_low=is_new_low
)
db.add(trade)
success_count += 1
except Exception as e:
logger.error(f"导入指数{row.get('index_code')}失败: {str(e)}")
error_count += 1
db.commit()
return ResponseModel(data={
"success_count": success_count,
"error_count": error_count,
"total_count": len(df),
"index_basic_added": index_basic_added,
"index_basic_updated": index_basic_updated,
"trade_date": trade_date
})
except Exception as e:
logger.error(f"导入指数数据失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/stock-basic", response_model=ResponseModel)
async def import_stock_basic(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""导入股票基础数据"""
if not file.filename.endswith(('.xls', '.xlsx')):
raise HTTPException(status_code=400, detail="只支持xls或xlsx格式文件")
try:
df = pd.read_excel(file.file)
required_columns = ['code', 'name', 'total_shares', 'float_shares',
'industry_index_name', 'industry_index_code',
'institution_hold_ratio', 'industry_level3', 'list_date']
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
raise HTTPException(status_code=400, detail=f"缺少必要列: {missing_columns}")
success_count = 0
error_count = 0
for _, row in df.iterrows():
try:
existing = db.query(StockBasic).filter(StockBasic.code == str(row['code'])).first()
list_date = None
if pd.notna(row['list_date']):
if isinstance(row['list_date'], datetime):
list_date = row['list_date'].date()
elif isinstance(row['list_date'], str):
list_date = datetime.strptime(row['list_date'], '%Y-%m-%d').date()
if existing:
existing.name = str(row.get('name', existing.name))
existing.total_shares = int(row.get('total_shares', existing.total_shares)) if pd.notna(row.get('total_shares')) else existing.total_shares
existing.float_shares = int(row.get('float_shares', existing.float_shares)) if pd.notna(row.get('float_shares')) else existing.float_shares
existing.industry_index_name = str(row.get('industry_index_name', existing.industry_index_name)) if pd.notna(row.get('industry_index_name')) else existing.industry_index_name
existing.industry_index_code = str(row.get('industry_index_code', existing.industry_index_code)) if pd.notna(row.get('industry_index_code')) else existing.industry_index_code
existing.institution_hold_ratio = float(row.get('institution_hold_ratio', existing.institution_hold_ratio)) if pd.notna(row.get('institution_hold_ratio')) else existing.institution_hold_ratio
existing.industry_level3 = str(row.get('industry_level3', existing.industry_level3)) if pd.notna(row.get('industry_level3')) else existing.industry_level3
existing.list_date = list_date
existing.updated_at = datetime.utcnow()
else:
stock = StockBasic(
code=str(row['code']),
name=str(row.get('name', '')),
total_shares=int(row['total_shares']) if pd.notna(row['total_shares']) else None,
float_shares=int(row['float_shares']) if pd.notna(row['float_shares']) else None,
industry_index_name=str(row.get('industry_index_name', '')) if pd.notna(row.get('industry_index_name')) else None,
industry_index_code=str(row.get('industry_index_code', '')) if pd.notna(row.get('industry_index_code')) else None,
institution_hold_ratio=float(row['institution_hold_ratio']) if pd.notna(row['institution_hold_ratio']) else None,
industry_level3=str(row.get('industry_level3', '')) if pd.notna(row.get('industry_level3')) else None,
list_date=list_date
)
db.add(stock)
success_count += 1
except Exception as e:
logger.error(f"导入股票{row.get('code')}失败: {str(e)}")
error_count += 1
db.commit()
return ResponseModel(data={
"success_count": success_count,
"error_count": error_count,
"total_count": len(df)
})
except Exception as e:
logger.error(f"导入股票基础数据失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/index-basic", response_model=ResponseModel)
async def import_index_basic(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""导入指数基础数据"""
if not file.filename.endswith(('.xls', '.xlsx')):
raise HTTPException(status_code=400, detail="只支持xls或xlsx格式文件")
try:
df = pd.read_excel(file.file)
required_columns = ['code', 'name', 'component_count']
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
raise HTTPException(status_code=400, detail=f"缺少必要列: {missing_columns}")
success_count = 0
error_count = 0
for _, row in df.iterrows():
try:
existing = db.query(IndexBasic).filter(IndexBasic.code == str(row['code'])).first()
if existing:
existing.name = str(row.get('name', existing.name))
existing.component_count = int(row.get('component_count', existing.component_count)) if pd.notna(row.get('component_count')) else existing.component_count
existing.updated_at = datetime.utcnow()
else:
index = IndexBasic(
code=str(row['code']),
name=str(row.get('name', '')),
component_count=int(row['component_count']) if pd.notna(row['component_count']) else None
)
db.add(index)
success_count += 1
except Exception as e:
logger.error(f"导入指数{row.get('code')}失败: {str(e)}")
error_count += 1
db.commit()
return ResponseModel(data={
"success_count": success_count,
"error_count": error_count,
"total_count": len(df)
})
except Exception as e:
logger.error(f"导入指数基础数据失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/index-trade", response_model=ResponseModel)
async def import_index_trade(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""导入指数交易数据"""
if not file.filename.endswith(('.xls', '.xlsx')):
raise HTTPException(status_code=400, detail="只支持xls或xlsx格式文件")
try:
df = pd.read_excel(file.file)
required_columns = ['index_code', 'trade_date', 'open', 'close', 'high', 'low']
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
raise HTTPException(status_code=400, detail=f"缺少必要列: {missing_columns}")
success_count = 0
error_count = 0
for _, row in df.iterrows():
try:
trade_date = None
if pd.notna(row['trade_date']):
if isinstance(row['trade_date'], datetime):
trade_date = row['trade_date'].date()
elif isinstance(row['trade_date'], str):
trade_date = datetime.strptime(row['trade_date'], '%Y-%m-%d').date()
existing = db.query(IndexTrade).filter(
IndexTrade.index_code == str(row['index_code']),
IndexTrade.trade_date == trade_date
).first()
if existing:
existing.open = float(row.get('open', existing.open)) if pd.notna(row.get('open')) else existing.open
existing.close = float(row.get('close', existing.close)) if pd.notna(row.get('close')) else existing.close
existing.high = float(row.get('high', existing.high)) if pd.notna(row.get('high')) else existing.high
existing.low = float(row.get('low', existing.low)) if pd.notna(row.get('low')) else existing.low
existing.change_pct = float(row.get('change_pct', existing.change_pct)) if pd.notna(row.get('change_pct')) else existing.change_pct
existing.volume = int(row.get('volume', existing.volume)) if pd.notna(row.get('volume')) else existing.volume
existing.amount = float(row.get('amount', existing.amount)) if pd.notna(row.get('amount')) else existing.amount
existing.total_market_value = float(row.get('total_market_value', existing.total_market_value)) if pd.notna(row.get('total_market_value')) else existing.total_market_value
existing.float_market_value = float(row.get('float_market_value', existing.float_market_value)) if pd.notna(row.get('float_market_value')) else existing.float_market_value
existing.up_count = int(row.get('up_count', existing.up_count)) if pd.notna(row.get('up_count')) else existing.up_count
existing.down_count = int(row.get('down_count', existing.down_count)) if pd.notna(row.get('down_count')) else existing.down_count
existing.flat_count = int(row.get('flat_count', existing.flat_count)) if pd.notna(row.get('flat_count')) else existing.flat_count
existing.limit_up_count = int(row.get('limit_up_count', existing.limit_up_count)) if pd.notna(row.get('limit_up_count')) else existing.limit_up_count
existing.limit_down_count = int(row.get('limit_down_count', existing.limit_down_count)) if pd.notna(row.get('limit_down_count')) else existing.limit_down_count
existing.suspend_count = int(row.get('suspend_count', existing.suspend_count)) if pd.notna(row.get('suspend_count')) else existing.suspend_count
existing.pe_ratio = float(row.get('pe_ratio', existing.pe_ratio)) if pd.notna(row.get('pe_ratio')) else existing.pe_ratio
existing.pe_median = float(row.get('pe_median', existing.pe_median)) if pd.notna(row.get('pe_median')) else existing.pe_median
existing.is_new_high = bool(row.get('is_new_high', existing.is_new_high)) if pd.notna(row.get('is_new_high')) else existing.is_new_high
existing.is_new_low = bool(row.get('is_new_low', existing.is_new_low)) if pd.notna(row.get('is_new_low')) else existing.is_new_low
existing.updated_at = datetime.utcnow()
else:
trade = IndexTrade(
index_code=str(row['index_code']),
trade_date=trade_date,
open=float(row['open']) if pd.notna(row['open']) else None,
close=float(row['close']) if pd.notna(row['close']) else None,
high=float(row['high']) if pd.notna(row['high']) else None,
low=float(row['low']) if pd.notna(row['low']) else None,
change_pct=float(row.get('change_pct')) if pd.notna(row.get('change_pct')) else None,
volume=int(row.get('volume')) if pd.notna(row.get('volume')) else None,
amount=float(row.get('amount')) if pd.notna(row.get('amount')) else None,
total_market_value=float(row.get('total_market_value')) if pd.notna(row.get('total_market_value')) else None,
float_market_value=float(row.get('float_market_value')) if pd.notna(row.get('float_market_value')) else None,
up_count=int(row.get('up_count')) if pd.notna(row.get('up_count')) else None,
down_count=int(row.get('down_count')) if pd.notna(row.get('down_count')) else None,
flat_count=int(row.get('flat_count')) if pd.notna(row.get('flat_count')) else None,
limit_up_count=int(row.get('limit_up_count')) if pd.notna(row.get('limit_up_count')) else None,
limit_down_count=int(row.get('limit_down_count')) if pd.notna(row.get('limit_down_count')) else None,
suspend_count=int(row.get('suspend_count')) if pd.notna(row.get('suspend_count')) else None,
pe_ratio=float(row.get('pe_ratio')) if pd.notna(row.get('pe_ratio')) else None,
pe_median=float(row.get('pe_median')) if pd.notna(row.get('pe_median')) else None,
is_new_high=bool(row.get('is_new_high')) if pd.notna(row.get('is_new_high')) else False,
is_new_low=bool(row.get('is_new_low')) if pd.notna(row.get('is_new_low')) else False
)
db.add(trade)
success_count += 1
except Exception as e:
logger.error(f"导入指数交易{row.get('index_code')}-{row.get('trade_date')}失败: {str(e)}")
error_count += 1
db.commit()
return ResponseModel(data={
"success_count": success_count,
"error_count": error_count,
"total_count": len(df)
})
except Exception as e:
logger.error(f"导入指数交易数据失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))

@ -0,0 +1,151 @@
"""
指数数据查询路由
"""
from typing import List
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from sqlalchemy import and_
from datetime import date
from app.db.session import get_db
from app.schemas.base import ResponseModel
from app.models.stock_basic import IndexBasic, IndexTrade
from app.core.security import get_current_user
from app.models.user import User
from app.utils.date_utils import parse_date, format_date
router = APIRouter()
@router.get("/list", response_model=ResponseModel)
async def get_index_list(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取指数列表"""
indexes = db.query(IndexBasic).order_by(IndexBasic.code).all()
result = []
for idx in indexes:
result.append({
"code": idx.code,
"name": idx.name,
"component_count": idx.component_count
})
return ResponseModel(data=result)
@router.get("/trade", response_model=ResponseModel)
async def get_index_trade_data(
codes: str = Query(..., description="指数代码列表,逗号分隔"),
start_date: str = Query(..., description="开始日期 YYYYMMDD"),
end_date: str = Query(..., description="结束日期 YYYYMMDD"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取指数交易数据"""
code_list = codes.split(",")
start = parse_date(start_date)
end = parse_date(end_date)
result = {}
for code in code_list:
code = code.strip()
index_basic = db.query(IndexBasic).filter(IndexBasic.code == code).first()
trades = db.query(IndexTrade).filter(
and_(
IndexTrade.index_code == code,
IndexTrade.trade_date >= start,
IndexTrade.trade_date <= end
)
).order_by(IndexTrade.trade_date).all()
trade_list = []
for trade in trades:
trade_list.append({
"trade_date": format_date(trade.trade_date),
"open": float(trade.open) if trade.open else None,
"close": float(trade.close) if trade.close else None,
"high": float(trade.high) if trade.high else None,
"low": float(trade.low) if trade.low else None,
"change_pct": float(trade.change_pct) if trade.change_pct else None,
"volume": trade.volume,
"amount": float(trade.amount) if trade.amount else None,
"total_market_value": float(trade.total_market_value) if trade.total_market_value else None,
"float_market_value": float(trade.float_market_value) if trade.float_market_value else None,
"up_count": trade.up_count,
"down_count": trade.down_count,
"flat_count": trade.flat_count,
"limit_up_count": trade.limit_up_count,
"limit_down_count": trade.limit_down_count,
"suspend_count": trade.suspend_count,
"pe_ratio": float(trade.pe_ratio) if trade.pe_ratio else None,
"pe_median": float(trade.pe_median) if trade.pe_median else None,
"is_new_high": trade.is_new_high,
"is_new_low": trade.is_new_low
})
result[code] = {
"basic": {
"code": index_basic.code if index_basic else code,
"name": index_basic.name if index_basic else None,
"component_count": index_basic.component_count if index_basic else None
},
"trades": trade_list
}
return ResponseModel(data=result)
@router.get("/{code}/chart", response_model=ResponseModel)
async def get_index_chart_data(
code: str,
start_date: str = Query(..., description="开始日期 YYYYMMDD"),
end_date: str = Query(..., description="结束日期 YYYYMMDD"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取指数K线图表数据(ECharts格式)"""
start = parse_date(start_date)
end = parse_date(end_date)
trades = db.query(IndexTrade).filter(
and_(
IndexTrade.index_code == code,
IndexTrade.trade_date >= start,
IndexTrade.trade_date <= end
)
).order_by(IndexTrade.trade_date).all()
category_data = []
values = []
volumes = []
for trade in trades:
category_data.append(format_date(trade.trade_date))
values.append([
float(trade.open) if trade.open else 0,
float(trade.close) if trade.close else 0,
float(trade.low) if trade.low else 0,
float(trade.high) if trade.high else 0,
trade.volume if trade.volume else 0
])
volumes.append([
trade.volume if trade.volume else 0,
1 if (trade.close and trade.open and trade.close >= trade.open) else -1
])
index_basic = db.query(IndexBasic).filter(IndexBasic.code == code).first()
return ResponseModel(data={
"code": code,
"name": index_basic.name if index_basic else None,
"component_count": index_basic.component_count if index_basic else None,
"categoryData": category_data,
"values": values,
"volumes": volumes
})

@ -5,6 +5,7 @@ import os
from typing import Optional from typing import Optional
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from pydantic import Field from pydantic import Field
from sqlalchemy.orm import Session
class Settings(BaseSettings): class Settings(BaseSettings):
@ -52,3 +53,19 @@ class Settings(BaseSettings):
# 全局配置实例 # 全局配置实例
settings = Settings() settings = Settings()
def load_system_configs(db: Session):
"""
从数据库加载系统配置到全局settings
注意这需要在数据库初始化后调用
"""
from app.services.config_service import ConfigService
system_configs = ConfigService.get_all_system_configs(db)
if "DATABASE_URL" in system_configs:
settings.DATABASE_URL = system_configs["DATABASE_URL"]
if "REDIS_URL" in system_configs:
settings.REDIS_URL = system_configs["REDIS_URL"]

@ -8,21 +8,37 @@ from typing import Generator
from app.config import settings from app.config import settings
from app.db.base import Base from app.db.base import Base
# 确保使用SQLite作为默认数据库
database_url = settings.DATABASE_URL or "sqlite:///./amazing_data.db"
# 创建数据库引擎 # 创建数据库引擎
if settings.DATABASE_URL.startswith("sqlite"): try:
if database_url.startswith("sqlite"):
engine = create_engine( engine = create_engine(
settings.DATABASE_URL, database_url,
connect_args={"check_same_thread": False}, connect_args={"check_same_thread": False},
echo=settings.DEBUG echo=settings.DEBUG
) )
else: else:
engine = create_engine( engine = create_engine(
settings.DATABASE_URL, database_url,
pool_pre_ping=True, pool_pre_ping=True,
pool_size=10, pool_size=10,
max_overflow=20, max_overflow=20,
echo=settings.DEBUG echo=settings.DEBUG
) )
# 测试连接
with engine.connect() as conn:
pass
except Exception as e:
print(f"数据库连接失败: {e}")
print("使用SQLite作为备选数据库...")
# 使用SQLite作为备选
engine = create_engine(
"sqlite:///./amazing_data.db",
connect_args={"check_same_thread": False},
echo=settings.DEBUG
)
# 创建会话工厂 # 创建会话工厂
SessionLocal = sessionmaker( SessionLocal = sessionmaker(

@ -30,6 +30,13 @@ async def lifespan(app: FastAPI):
try: try:
init_db() init_db()
print("Database initialized successfully") print("Database initialized successfully")
# 加载系统配置
from app.db.session import get_db
from app.config import load_system_configs
db = next(get_db())
load_system_configs(db)
print("System configs loaded successfully")
except Exception as e: except Exception as e:
print(f"Database initialization warning: {e}") print(f"Database initialization warning: {e}")

@ -0,0 +1,78 @@
"""
股票基础数据模型
"""
from datetime import datetime, date
from sqlalchemy import Column, Integer, BigInteger, String, Numeric, Text, Date, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from app.db.base import Base
class StockBasic(Base):
"""股票基础数据表"""
__tablename__ = "stock_basic"
id = Column(BigInteger, primary_key=True, index=True)
code = Column(String(20), unique=True, nullable=False, index=True)
name = Column(String(50))
total_shares = Column(BigInteger)
float_shares = Column(BigInteger)
industry_index_name = Column(String(100))
industry_index_code = Column(String(20), ForeignKey("index_basic.code"))
institution_hold_ratio = Column(Numeric(10, 4))
industry_level3 = Column(String(100))
list_date = Column(Date)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
industry_index = relationship("IndexBasic", back_populates="stocks")
class IndexBasic(Base):
"""指数基础表"""
__tablename__ = "index_basic"
id = Column(BigInteger, primary_key=True, index=True)
code = Column(String(20), unique=True, nullable=False, index=True)
name = Column(String(100))
component_count = Column(Integer)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
stocks = relationship("StockBasic", back_populates="industry_index")
trades = relationship("IndexTrade", back_populates="index")
class IndexTrade(Base):
"""指数交易表"""
__tablename__ = "index_trade"
id = Column(BigInteger, primary_key=True, index=True)
index_code = Column(String(20), ForeignKey("index_basic.code"), nullable=False, index=True)
trade_date = Column(Date, nullable=False, index=True)
open = Column(Numeric(10, 3))
close = Column(Numeric(10, 3))
high = Column(Numeric(10, 3))
low = Column(Numeric(10, 3))
change_pct = Column(Numeric(10, 4))
volume = Column(BigInteger)
amount = Column(Numeric(18, 2))
total_market_value = Column(Numeric(18, 2))
float_market_value = Column(Numeric(18, 2))
up_count = Column(Integer)
down_count = Column(Integer)
flat_count = Column(Integer)
limit_up_count = Column(Integer)
limit_down_count = Column(Integer)
suspend_count = Column(Integer)
pe_ratio = Column(Numeric(10, 4))
pe_median = Column(Numeric(10, 4))
is_new_high = Column(Boolean, default=False)
is_new_low = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
index = relationship("IndexBasic", back_populates="trades")
__table_args__ = (
{'unique_constraint': None},
)

@ -147,3 +147,9 @@ class ConfigService:
db.add(config) db.add(config)
db.commit() db.commit()
@staticmethod
def get_all_system_configs(db: Session) -> dict:
"""获取所有系统配置"""
configs = db.query(SystemConfig).all()
return {config.config_key: config.config_value for config in configs}

@ -0,0 +1,96 @@
"""
创建股票基础数据相关表
"""
from sqlalchemy import text
from app.db.session import SessionLocal
db = SessionLocal()
try:
# 创建股票基础数据表
db.execute(text("""
CREATE TABLE IF NOT EXISTS stock_basic (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) UNIQUE NOT NULL,
name VARCHAR(50),
total_shares BIGINT,
float_shares BIGINT,
industry_index_name VARCHAR(100),
industry_index_code VARCHAR(20),
institution_hold_ratio DECIMAL(10, 4),
industry_level3 VARCHAR(100),
list_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""))
# 创建指数基础表
db.execute(text("""
CREATE TABLE IF NOT EXISTS index_basic (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) UNIQUE NOT NULL,
name VARCHAR(100),
component_count INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""))
# 创建指数交易表
db.execute(text("""
CREATE TABLE IF NOT EXISTS index_trade (
id BIGSERIAL PRIMARY KEY,
index_code VARCHAR(20) NOT NULL,
trade_date DATE NOT NULL,
open DECIMAL(10, 3),
close DECIMAL(10, 3),
high DECIMAL(10, 3),
low DECIMAL(10, 3),
change_pct DECIMAL(10, 4),
volume BIGINT,
amount DECIMAL(18, 2),
total_market_value DECIMAL(18, 2),
float_market_value DECIMAL(18, 2),
up_count INTEGER,
down_count INTEGER,
flat_count INTEGER,
limit_up_count INTEGER,
limit_down_count INTEGER,
suspend_count INTEGER,
pe_ratio DECIMAL(10, 4),
pe_median DECIMAL(10, 4),
is_new_high BOOLEAN DEFAULT FALSE,
is_new_low BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(index_code, trade_date)
)
"""))
# 创建索引
db.execute(text("CREATE INDEX IF NOT EXISTS idx_stock_basic_code ON stock_basic(code)"))
db.execute(text("CREATE INDEX IF NOT EXISTS idx_index_basic_code ON index_basic(code)"))
db.execute(text("CREATE INDEX IF NOT EXISTS idx_index_trade_code ON index_trade(index_code)"))
db.execute(text("CREATE INDEX IF NOT EXISTS idx_index_trade_date ON index_trade(trade_date)"))
# 添加外键约束
db.execute(text("""
ALTER TABLE stock_basic
ADD CONSTRAINT fk_stock_basic_index_code
FOREIGN KEY (industry_index_code) REFERENCES index_basic(code)
"""))
db.execute(text("""
ALTER TABLE index_trade
ADD CONSTRAINT fk_index_trade_index_code
FOREIGN KEY (index_code) REFERENCES index_basic(code)
"""))
db.commit()
print("表创建成功")
except Exception as e:
print(f"创建表失败: {str(e)}")
db.rollback()
finally:
db.close()

@ -23,3 +23,23 @@ export const testSDKConfig = (id: number) => {
export const setDefaultConfig = (id: number) => { export const setDefaultConfig = (id: number) => {
return request.post(`/configs/sdk/${id}/set-default`) return request.post(`/configs/sdk/${id}/set-default`)
} }
export const getSystemConfigs = () => {
return request.get('/configs/system')
}
export const updateSystemConfigs = (data: any) => {
return request.put('/configs/system', data)
}
export const testSystemConnection = (data: any) => {
return request.post('/configs/system/test', data)
}
export const initDatabase = () => {
return request.post('/configs/system/init')
}
export const checkDatabaseStructure = () => {
return request.get('/configs/system/structure')
}

@ -0,0 +1,41 @@
import request from '@/utils/request'
export const importStockBasic = (file: File) => {
const formData = new FormData()
formData.append('file', file)
return request.post('/import/stock-basic', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export const importIndexBasic = (file: File) => {
const formData = new FormData()
formData.append('file', file)
return request.post('/import/index-basic', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export const importIndexTrade = (file: File) => {
const formData = new FormData()
formData.append('file', file)
return request.post('/import/index-trade', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export const importIndexData = (file: File, tradeDate: string) => {
const formData = new FormData()
formData.append('file', file)
return request.post(`/import/index-data?trade_date=${tradeDate}`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}

@ -0,0 +1,23 @@
import request from '@/utils/request'
export const getIndexList = () => {
return request.get('/index/list')
}
export const getIndexTradeData = (params: {
codes: string
start_date: string
end_date: string
}) => {
return request.get('/index/trade', { params })
}
export const getIndexChartData = (
code: string,
params: {
start_date: string
end_date: string
}
) => {
return request.get(`/index/${code}/chart`, { params })
}

@ -1,5 +1,7 @@
<template> <template>
<div class="config-manager"> <div class="config-manager">
<el-tabs v-model="activeTab" class="config-tabs">
<el-tab-pane label="SDK配置" name="sdk">
<div class="page-header"> <div class="page-header">
<h3>SDK配置管理</h3> <h3>SDK配置管理</h3>
<el-button type="primary" @click="handleAdd"> <el-button type="primary" @click="handleAdd">
@ -49,6 +51,78 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
</el-tab-pane>
<el-tab-pane label="系统配置" name="system">
<div class="page-header">
<h3>系统配置管理</h3>
</div>
<el-card>
<el-form
ref="systemFormRef"
:model="systemForm"
:rules="systemRules"
label-width="100px"
>
<el-form-item label="数据库类型" prop="dbType">
<el-select v-model="systemForm.dbType" placeholder="选择数据库类型">
<el-option label="SQLite" value="sqlite" />
<el-option label="PostgreSQL" value="postgresql" />
<el-option label="MySQL" value="mysql" />
</el-select>
</el-form-item>
<el-form-item label="数据库地址" prop="dbHost" v-if="systemForm.dbType !== 'sqlite'">
<el-input v-model="systemForm.dbHost" placeholder="例如: localhost" />
</el-form-item>
<el-form-item label="数据库端口" prop="dbPort" v-if="systemForm.dbType !== 'sqlite'">
<el-input-number v-model="systemForm.dbPort" :min="1" :max="65535" />
</el-form-item>
<el-form-item label="用户名" prop="dbUsername" v-if="systemForm.dbType !== 'sqlite'">
<el-input v-model="systemForm.dbUsername" placeholder="数据库用户名" />
</el-form-item>
<el-form-item label="密码" prop="dbPassword" v-if="systemForm.dbType !== 'sqlite'">
<el-input v-model="systemForm.dbPassword" type="password" placeholder="数据库密码" show-password />
</el-form-item>
<el-form-item label="数据库名称" prop="dbName" v-if="systemForm.dbType !== 'sqlite'">
<el-input v-model="systemForm.dbName" placeholder="例如: amazing_data" />
</el-form-item>
<el-form-item label="SQLite路径" prop="dbPath" v-if="systemForm.dbType === 'sqlite'">
<el-input v-model="systemForm.dbPath" placeholder="例如: ./amazing_data.db" />
</el-form-item>
<el-form-item label="Redis连接" prop="redis">
<el-input v-model="systemForm.redis" placeholder="例如: redis://localhost:6379/0" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSystemSubmit" :loading="systemSubmitting">
保存配置
</el-button>
<el-button type="warning" @click="handleTestConnection" :loading="testingConnection">
检测连接
</el-button>
<el-button
type="info"
@click="handleInitDatabase"
:loading="initializingDatabase"
:disabled="!showInitButton"
>
初始化数据库
</el-button>
<el-button @click="handleSystemReset">
重置
</el-button>
</el-form-item>
<el-form-item v-if="dbStructureStatus">
<el-alert
:title="dbStructureStatus === 'complete' ? '数据结构完整' : '数据结构不完整,需要初始化'"
:type="dbStructureStatus === 'complete' ? 'success' : 'warning'"
show-icon
/>
</el-form-item>
</el-form>
</el-card>
</el-tab-pane>
</el-tabs>
<!-- 添加/编辑对话框 --> <!-- 添加/编辑对话框 -->
<el-dialog <el-dialog
@ -106,9 +180,15 @@ import {
updateSDKConfig, updateSDKConfig,
deleteSDKConfig, deleteSDKConfig,
testSDKConfig, testSDKConfig,
setDefaultConfig setDefaultConfig,
getSystemConfigs,
updateSystemConfigs,
testSystemConnection,
initDatabase,
checkDatabaseStructure
} from '@/api/config' } from '@/api/config'
const activeTab = ref('sdk')
const loading = ref(false) const loading = ref(false)
const configs = ref<any[]>([]) const configs = ref<any[]>([])
const dialogVisible = ref(false) const dialogVisible = ref(false)
@ -117,6 +197,35 @@ const submitting = ref(false)
const formRef = ref() const formRef = ref()
const currentId = ref<number | null>(null) const currentId = ref<number | null>(null)
//
const systemForm = reactive({
dbType: 'sqlite',
dbHost: 'localhost',
dbPort: 5432,
dbUsername: 'postgres',
dbPassword: '',
dbName: 'amazing_data',
dbPath: './amazing_data.db',
redis: 'redis://localhost:6379/0'
})
const systemRules = {
dbType: [{ required: true, message: '请选择数据库类型', trigger: 'change' }],
dbHost: [{ required: true, message: '请输入数据库地址', trigger: 'blur' }],
dbPort: [{ required: true, message: '请输入数据库端口', trigger: 'blur' }],
dbUsername: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
dbName: [{ required: true, message: '请输入数据库名称', trigger: 'blur' }],
dbPath: [{ required: true, message: '请输入SQLite路径', trigger: 'blur' }],
redis: [{ required: true, message: '请输入Redis连接URL', trigger: 'blur' }]
}
const systemSubmitting = ref(false)
const testingConnection = ref(false)
const initializingDatabase = ref(false)
const systemFormRef = ref()
const showInitButton = ref(false)
const dbStructureStatus = ref<string | null>(null)
const form = reactive({ const form = reactive({
name: '', name: '',
username: '', username: '',
@ -148,6 +257,52 @@ const fetchConfigs = async () => {
} }
} }
const buildDatabaseUrl = () => {
switch (systemForm.dbType) {
case 'sqlite':
return `sqlite:///${systemForm.dbPath}`
case 'postgresql':
return `postgresql://${systemForm.dbUsername}:${systemForm.dbPassword}@${systemForm.dbHost}:${systemForm.dbPort}/${systemForm.dbName}`
case 'mysql':
return `mysql://${systemForm.dbUsername}:${systemForm.dbPassword}@${systemForm.dbHost}:${systemForm.dbPort}/${systemForm.dbName}`
default:
return 'sqlite:///./amazing_data.db'
}
}
const parseDatabaseUrl = (url: string) => {
if (url.startsWith('sqlite://')) {
systemForm.dbType = 'sqlite'
systemForm.dbPath = url.replace('sqlite:///', '')
} else if (url.startsWith('postgresql://')) {
systemForm.dbType = 'postgresql'
systemForm.dbHost = 'localhost'
systemForm.dbPort = 5432
systemForm.dbUsername = 'postgres'
systemForm.dbPassword = ''
systemForm.dbName = 'amazing_data'
} else if (url.startsWith('mysql://')) {
systemForm.dbType = 'mysql'
systemForm.dbHost = 'localhost'
systemForm.dbPort = 3306
systemForm.dbUsername = 'root'
systemForm.dbPassword = ''
systemForm.dbName = 'amazing_data'
}
}
const fetchSystemConfigs = async () => {
try {
const res: any = await getSystemConfigs()
if (res.data) {
parseDatabaseUrl(res.data.database || 'sqlite:///./amazing_data.db')
systemForm.redis = res.data.redis || 'redis://localhost:6379/0'
}
} catch (error) {
console.error(error)
}
}
const handleAdd = () => { const handleAdd = () => {
isEdit.value = false isEdit.value = false
currentId.value = null currentId.value = null
@ -170,7 +325,7 @@ const handleEdit = (row: any) => {
Object.assign(form, { Object.assign(form, {
name: row.name, name: row.name,
username: row.username, username: row.username,
password: '', // password: '',
host: row.host, host: row.host,
port: row.port, port: row.port,
local_path: row.local_path, local_path: row.local_path,
@ -202,6 +357,100 @@ const handleSubmit = async () => {
} }
} }
const handleSystemSubmit = async () => {
const valid = await systemFormRef.value?.validate().catch(() => false)
if (!valid) return
systemSubmitting.value = true
try {
const databaseUrl = buildDatabaseUrl()
await updateSystemConfigs({
database: databaseUrl,
redis: systemForm.redis
})
ElMessage.success('保存成功')
} catch (error) {
console.error(error)
} finally {
systemSubmitting.value = false
}
}
const handleSystemReset = () => {
systemForm.dbType = 'sqlite'
systemForm.dbHost = 'localhost'
systemForm.dbPort = 5432
systemForm.dbUsername = 'postgres'
systemForm.dbPassword = ''
systemForm.dbName = 'amazing_data'
systemForm.dbPath = './amazing_data.db'
systemForm.redis = 'redis://localhost:6379/0'
dbStructureStatus.value = null
showInitButton.value = false
}
const handleTestConnection = async () => {
const valid = await systemFormRef.value?.validate().catch(() => false)
if (!valid) return
testingConnection.value = true
try {
const databaseUrl = buildDatabaseUrl()
const res: any = await testSystemConnection({
database: databaseUrl,
redis: systemForm.redis
})
if (res.data?.database) {
ElMessage.success('数据库连接成功')
const structureRes: any = await checkDatabaseStructure()
if (structureRes.data?.complete) {
dbStructureStatus.value = 'complete'
showInitButton.value = false
ElMessage.success('数据结构完整')
} else {
dbStructureStatus.value = 'incomplete'
showInitButton.value = true
ElMessage.warning('数据结构不完整,需要初始化')
}
} else {
ElMessage.error('数据库连接失败')
dbStructureStatus.value = null
showInitButton.value = false
}
if (res.data?.redis) {
ElMessage.success('Redis连接成功')
} else {
ElMessage.error('Redis连接失败')
}
} catch (error) {
ElMessage.error('检测连接失败')
dbStructureStatus.value = null
showInitButton.value = false
} finally {
testingConnection.value = false
}
}
const handleInitDatabase = async () => {
try {
await ElMessageBox.confirm('确定要初始化数据库吗?这将创建所有必要的表结构。', '提示', {
type: 'warning'
})
initializingDatabase.value = true
const res: any = await initDatabase()
if (res.data?.success) {
ElMessage.success('数据库初始化成功')
} else {
ElMessage.error('数据库初始化失败')
}
} catch (error) {
//
} finally {
initializingDatabase.value = false
}
}
const handleDelete = async (row: any) => { const handleDelete = async (row: any) => {
try { try {
await ElMessageBox.confirm('确定删除该配置吗?', '提示', { await ElMessageBox.confirm('确定删除该配置吗?', '提示', {
@ -240,6 +489,7 @@ const handleSetDefault = async (row: any) => {
onMounted(() => { onMounted(() => {
fetchConfigs() fetchConfigs()
fetchSystemConfigs()
}) })
</script> </script>
@ -248,6 +498,10 @@ onMounted(() => {
padding: 20px; padding: 20px;
} }
.config-tabs {
margin-bottom: 20px;
}
.page-header { .page-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -258,4 +512,8 @@ onMounted(() => {
.page-header h3 { .page-header h3 {
margin: 0; margin: 0;
} }
.el-form {
max-width: 600px;
}
</style> </style>

@ -0,0 +1,320 @@
<template>
<div class="data-import">
<el-card>
<template #header>
<span>数据导入</span>
</template>
<el-tabs v-model="activeTab">
<el-tab-pane label="指数数据导入" name="indexData">
<el-form label-width="120px">
<el-form-item label="交易日期">
<el-date-picker
v-model="tradeDate"
type="date"
placeholder="选择交易日期"
value-format="YYYY-MM-DD"
style="width: 200px;"
/>
</el-form-item>
<el-form-item label="文件格式说明">
<el-text>
Excel第一行标题格式<br/>
证券代码证券名称成分个数 [交易日期]最新开盘价 [交易日期]最新收盘价 [交易日期]最新
成交量 [交易日期]最新 [单位]成交额 [交易日期]最新 [单位]百万元
总市值 [截止日期]最新 [单位]百万元自由流通市值 [交易日期]最新 [单位]百万元
涨跌幅 [交易日期]最新 [单位]%最高价 [交易日期]最新最低价 [交易日期]最新
上涨家数 [交易日期]最新下跌家数 [交易日期]最新平盘家数 [交易日期]最新
涨停家数 [交易日期]最新跌停家数 [交易日期]最新停牌家数 [交易日期]最新
近期创历史新高 [交易日期]最新 [近N日内]300 [复权方式]不复权
近期创历史新低 [交易日期]最新 [近N日内]300 [复权方式]不复权
市盈率PE(TTM) [交易日期]最新 [剔除规则]不调整
市盈率PE(TTM)中位值 [交易日期]最新 [剔除规则]不调整
</el-text>
</el-form-item>
<el-form-item label="选择文件">
<el-upload
ref="indexDataUpload"
:auto-upload="false"
:limit="1"
accept=".xls,.xlsx"
:on-change="handleIndexDataChange"
>
<el-button type="primary">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">只能上传xls/xlsx文件</div>
</template>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="success" @click="handleImportIndexData" :loading="importingIndexData">
导入数据
</el-button>
</el-form-item>
</el-form>
<el-alert v-if="indexDataResult" :title="indexDataResult.title" :type="indexDataResult.type" show-icon />
</el-tab-pane>
<el-tab-pane label="股票基础数据" name="stockBasic">
<el-form label-width="120px">
<el-form-item label="文件格式说明">
<el-text>
必须包含以下列code, name, total_shares, float_shares,
industry_index_name, industry_index_code, institution_hold_ratio,
industry_level3, list_date
</el-text>
</el-form-item>
<el-form-item label="选择文件">
<el-upload
ref="stockBasicUpload"
:auto-upload="false"
:limit="1"
accept=".xls,.xlsx"
:on-change="handleStockBasicChange"
>
<el-button type="primary">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">只能上传xls/xlsx文件</div>
</template>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="success" @click="handleImportStockBasic" :loading="importingStockBasic">
导入数据
</el-button>
</el-form-item>
</el-form>
<el-alert v-if="stockBasicResult" :title="stockBasicResult.title" :type="stockBasicResult.type" show-icon />
</el-tab-pane>
<el-tab-pane label="指数基础数据" name="indexBasic">
<el-form label-width="120px">
<el-form-item label="文件格式说明">
<el-text>
必须包含以下列code, name, component_count
</el-text>
</el-form-item>
<el-form-item label="选择文件">
<el-upload
ref="indexBasicUpload"
:auto-upload="false"
:limit="1"
accept=".xls,.xlsx"
:on-change="handleIndexBasicChange"
>
<el-button type="primary">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">只能上传xls/xlsx文件</div>
</template>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="success" @click="handleImportIndexBasic" :loading="importingIndexBasic">
导入数据
</el-button>
</el-form-item>
</el-form>
<el-alert v-if="indexBasicResult" :title="indexBasicResult.title" :type="indexBasicResult.type" show-icon />
</el-tab-pane>
<el-tab-pane label="指数交易数据" name="indexTrade">
<el-form label-width="120px">
<el-form-item label="文件格式说明">
<el-text>
必须包含以下列index_code, trade_date, open, close, high, low<br/>
可选列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
</el-text>
</el-form-item>
<el-form-item label="选择文件">
<el-upload
ref="indexTradeUpload"
:auto-upload="false"
:limit="1"
accept=".xls,.xlsx"
:on-change="handleIndexTradeChange"
>
<el-button type="primary">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">只能上传xls/xlsx文件</div>
</template>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="success" @click="handleImportIndexTrade" :loading="importingIndexTrade">
导入数据
</el-button>
</el-form-item>
</el-form>
<el-alert v-if="indexTradeResult" :title="indexTradeResult.title" :type="indexTradeResult.type" show-icon />
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { importStockBasic, importIndexBasic, importIndexTrade, importIndexData } from '@/api/dataImport'
const activeTab = ref('indexData')
const tradeDate = ref(new Date().toISOString().slice(0, 10))
const importingStockBasic = ref(false)
const importingIndexBasic = ref(false)
const importingIndexTrade = ref(false)
const importingIndexData = ref(false)
const stockBasicFile = ref<File | null>(null)
const indexBasicFile = ref<File | null>(null)
const indexTradeFile = ref<File | null>(null)
const indexDataFile = ref<File | null>(null)
const stockBasicResult = ref<any>(null)
const indexBasicResult = ref<any>(null)
const indexTradeResult = ref<any>(null)
const indexDataResult = ref<any>(null)
const handleStockBasicChange = (file: any) => {
stockBasicFile.value = file.raw
}
const handleIndexBasicChange = (file: any) => {
indexBasicFile.value = file.raw
}
const handleIndexTradeChange = (file: any) => {
indexTradeFile.value = file.raw
}
const handleIndexDataChange = (file: any) => {
indexDataFile.value = file.raw
}
const handleImportIndexData = async () => {
if (!indexDataFile.value) {
ElMessage.warning('请先选择文件')
return
}
if (!tradeDate.value) {
ElMessage.warning('请选择交易日期')
return
}
importingIndexData.value = true
indexDataResult.value = null
try {
const res: any = await importIndexData(indexDataFile.value, tradeDate.value)
if (res.data) {
indexDataResult.value = {
title: `导入完成:成功${res.data.success_count}条,失败${res.data.error_count}\n新增指数基础数据${res.data.index_basic_added}条,更新${res.data.index_basic_updated}`,
type: res.data.error_count > 0 ? 'warning' : 'success'
}
ElMessage.success('导入完成')
}
} catch (error: any) {
indexDataResult.value = {
title: `导入失败:${error.response?.data?.detail || error.message}`,
type: 'error'
}
ElMessage.error('导入失败')
} finally {
importingIndexData.value = false
}
}
const handleImportStockBasic = async () => {
if (!stockBasicFile.value) {
ElMessage.warning('请先选择文件')
return
}
importingStockBasic.value = true
stockBasicResult.value = null
try {
const res: any = await importStockBasic(stockBasicFile.value)
if (res.data) {
stockBasicResult.value = {
title: `导入完成:成功${res.data.success_count}条,失败${res.data.error_count}条,共${res.data.total_count}`,
type: res.data.error_count > 0 ? 'warning' : 'success'
}
ElMessage.success('导入完成')
}
} catch (error: any) {
stockBasicResult.value = {
title: `导入失败:${error.response?.data?.detail || error.message}`,
type: 'error'
}
ElMessage.error('导入失败')
} finally {
importingStockBasic.value = false
}
}
const handleImportIndexBasic = async () => {
if (!indexBasicFile.value) {
ElMessage.warning('请先选择文件')
return
}
importingIndexBasic.value = true
indexBasicResult.value = null
try {
const res: any = await importIndexBasic(indexBasicFile.value)
if (res.data) {
indexBasicResult.value = {
title: `导入完成:成功${res.data.success_count}条,失败${res.data.error_count}条,共${res.data.total_count}`,
type: res.data.error_count > 0 ? 'warning' : 'success'
}
ElMessage.success('导入完成')
}
} catch (error: any) {
indexBasicResult.value = {
title: `导入失败:${error.response?.data?.detail || error.message}`,
type: 'error'
}
ElMessage.error('导入失败')
} finally {
importingIndexBasic.value = false
}
}
const handleImportIndexTrade = async () => {
if (!indexTradeFile.value) {
ElMessage.warning('请先选择文件')
return
}
importingIndexTrade.value = true
indexTradeResult.value = null
try {
const res: any = await importIndexTrade(indexTradeFile.value)
if (res.data) {
indexTradeResult.value = {
title: `导入完成:成功${res.data.success_count}条,失败${res.data.error_count}条,共${res.data.total_count}`,
type: res.data.error_count > 0 ? 'warning' : 'success'
}
ElMessage.success('导入完成')
}
} catch (error: any) {
indexTradeResult.value = {
title: `导入失败:${error.response?.data?.detail || error.message}`,
type: 'error'
}
ElMessage.error('导入失败')
} finally {
importingIndexTrade.value = false
}
}
</script>
<style scoped>
.data-import {
padding: 20px;
}
</style>

@ -0,0 +1,303 @@
<template>
<div class="index-query">
<el-card>
<el-form :model="queryForm" inline>
<el-form-item label="指数代码">
<el-input v-model="queryForm.code" placeholder="如: BK0001, BK0002" style="width: 150px;" />
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="queryForm.startDate"
type="date"
placeholder="开始日期"
value-format="YYYYMMDD"
style="width: 140px;"
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="queryForm.endDate"
type="date"
placeholder="结束日期"
value-format="YYYYMMDD"
style="width: 140px;"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" :loading="loading">
<el-icon><Search /></el-icon>
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="basic-card" v-if="indexBasic">
<template #header>
<span>指数基础信息</span>
</template>
<el-descriptions :column="3" border>
<el-descriptions-item label="指数代码">{{ indexBasic.code }}</el-descriptions-item>
<el-descriptions-item label="指数名称">{{ indexBasic.name }}</el-descriptions-item>
<el-descriptions-item label="成分个数">{{ indexBasic.component_count }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card class="chart-card" v-if="chartData.categoryData.length > 0">
<template #header>
<span>指数K线图 - {{ indexBasic?.name || queryForm.code }}</span>
</template>
<div ref="chartRef" class="kline-chart"></div>
</el-card>
<el-card class="data-card" v-if="tableData.length > 0">
<template #header>
<span>交易数据列表</span>
</template>
<el-table :data="tableData" stripe height="300">
<el-table-column prop="trade_date" label="日期" width="120" />
<el-table-column prop="open" label="开盘" :formatter="formatNumber" />
<el-table-column prop="close" label="收盘" :formatter="formatNumber" />
<el-table-column prop="high" label="最高" :formatter="formatNumber" />
<el-table-column prop="low" label="最低" :formatter="formatNumber" />
<el-table-column prop="change_pct" label="涨跌幅%">
<template #default="{ row }">
<el-tag :type="row.change_pct >= 0 ? 'success' : 'danger'" size="small">
{{ row.change_pct?.toFixed(2) || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="volume" label="成交量" :formatter="formatVolume" />
<el-table-column prop="amount" label="成交额(百万)" :formatter="formatNumber" />
<el-table-column prop="up_count" label="上涨家数" width="90" />
<el-table-column prop="down_count" label="下跌家数" width="90" />
<el-table-column prop="limit_up_count" label="涨停" width="70" />
<el-table-column prop="limit_down_count" label="跌停" width="70" />
<el-table-column prop="pe_ratio" label="市盈率" :formatter="formatNumber" />
<el-table-column label="新高/新低" width="90">
<template #default="{ row }">
<el-tag v-if="row.is_new_high" type="success" size="small"></el-tag>
<el-tag v-if="row.is_new_low" type="danger" size="small"></el-tag>
<span v-if="!row.is_new_high && !row.is_new_low">-</span>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
import { getIndexChartData, getIndexTradeData } from '@/api/index'
const loading = ref(false)
const chartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
const queryForm = reactive({
code: 'BK0001',
startDate: getDefaultStartDate(),
endDate: getDefaultEndDate()
})
const indexBasic = ref<any>(null)
const chartData = reactive({
categoryData: [] as string[],
values: [] as number[][],
volumes: [] as number[][]
})
const tableData = ref<any[]>([])
function getDefaultStartDate() {
const date = new Date()
date.setMonth(date.getMonth() - 6)
return formatDate(date)
}
function getDefaultEndDate() {
return formatDate(new Date())
}
function formatDate(date: Date) {
return date.toISOString().slice(0, 10).replace(/-/g, '')
}
function formatNumber(row: any, column: any, value: number) {
return value?.toFixed(2) || '-'
}
function formatVolume(row: any, column: any, 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('请输入指数代码')
return
}
loading.value = true
try {
const res: any = await getIndexChartData(queryForm.code, {
start_date: queryForm.startDate,
end_date: queryForm.endDate
})
if (res.data) {
indexBasic.value = {
code: res.data.code,
name: res.data.name,
component_count: res.data.component_count
}
chartData.categoryData = res.data.categoryData || []
chartData.values = res.data.values || []
chartData.volumes = res.data.volumes || []
nextTick(() => {
renderChart()
})
}
const tradeRes: any = await getIndexTradeData({
codes: queryForm.code,
start_date: queryForm.startDate,
end_date: queryForm.endDate
})
if (tradeRes.data && tradeRes.data[queryForm.code]) {
const tradeData = tradeRes.data[queryForm.code]
indexBasic.value = tradeData.basic || indexBasic.value
tableData.value = tradeData.trades || []
}
} catch (error) {
console.error(error)
ElMessage.error('查询失败')
} finally {
loading.value = false
}
}
const renderChart = () => {
if (!chartRef.value) return
if (chartInstance) {
chartInstance.dispose()
}
chartInstance = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' }
},
grid: [
{ left: '10%', right: '8%', height: '50%' },
{ left: '10%', right: '8%', top: '68%', height: '16%' }
],
xAxis: [
{
type: 'category',
data: chartData.categoryData,
scale: true,
boundaryGap: false,
axisLine: { onZero: false },
splitLine: { show: false },
min: 'dataMin',
max: 'dataMax'
},
{
type: 'category',
gridIndex: 1,
data: chartData.categoryData,
scale: true,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
min: 'dataMin',
max: 'dataMax'
}
],
yAxis: [
{
scale: true,
splitArea: { show: true }
},
{
scale: true,
gridIndex: 1,
splitNumber: 2,
axisLabel: { show: false },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false }
}
],
dataZoom: [
{ type: 'inside', xAxisIndex: [0, 1], start: 50, end: 100 },
{ show: true, xAxisIndex: [0, 1], type: 'slider', top: '85%', start: 50, end: 100 }
],
series: [
{
name: 'K线',
type: 'candlestick',
data: chartData.values,
itemStyle: {
color: '#ef232a',
color0: '#14b143',
borderColor: '#ef232a',
borderColor0: '#14b143'
}
},
{
name: '成交量',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
data: chartData.volumes
}
]
}
chartInstance.setOption(option)
}
window.addEventListener('resize', () => {
chartInstance?.resize()
})
</script>
<style scoped>
.index-query {
padding: 10px;
}
.basic-card {
margin-top: 20px;
}
.chart-card {
margin-top: 20px;
}
.kline-chart {
width: 100%;
height: 400px;
}
.data-card {
margin-top: 20px;
}
</style>

@ -13,7 +13,7 @@ export default defineConfig({
port: 3000, port: 3000,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:8000', target: 'http://localhost:8001',
changeOrigin: true changeOrigin: true
} }
} }

Loading…
Cancel
Save