|
|
|
@ -41,6 +41,16 @@ INDEX_TRADE_COLUMN_MAP = {
|
|
|
|
'市盈率PE(TTM)中位值 [交易日期]最新 [剔除规则]不调整': 'pe_median'
|
|
|
|
'市盈率PE(TTM)中位值 [交易日期]最新 [剔除规则]不调整': 'pe_median'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
STOCK_BASIC_COLUMN_MAP = {
|
|
|
|
|
|
|
|
'证券代码': 'code',
|
|
|
|
|
|
|
|
'证券名称': 'name',
|
|
|
|
|
|
|
|
'首发上市日': 'list_date',
|
|
|
|
|
|
|
|
'所属东财行业指数名称\n[行业类别]2级': 'industry_index_name',
|
|
|
|
|
|
|
|
'所属东财行业指数代码\n[行业类别]2级': 'industry_index_code',
|
|
|
|
|
|
|
|
'机构持股比例合计\n[报告期]最新一期\n[单位]%\n[比例类型]占总股本比例': 'institution_hold_ratio',
|
|
|
|
|
|
|
|
'所属东财行业名称\n[行业类别]3级': 'industry_level3'
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/index-data", response_model=ResponseModel)
|
|
|
|
@router.post("/index-data", response_model=ResponseModel)
|
|
|
|
async def import_index_data(
|
|
|
|
async def import_index_data(
|
|
|
|
@ -231,70 +241,152 @@ async def import_stock_basic(
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
current_user: User = Depends(get_current_user)
|
|
|
|
current_user: User = Depends(get_current_user)
|
|
|
|
):
|
|
|
|
):
|
|
|
|
"""导入股票基础数据"""
|
|
|
|
"""导入股票基础数据(支持模板格式)"""
|
|
|
|
if not file.filename.endswith(('.xls', '.xlsx')):
|
|
|
|
if not file.filename.endswith(('.xls', '.xlsx')):
|
|
|
|
raise HTTPException(status_code=400, detail="只支持xls或xlsx格式文件")
|
|
|
|
raise HTTPException(status_code=400, detail="只支持xls或xlsx格式文件")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
df = pd.read_excel(file.file)
|
|
|
|
df = pd.read_excel(file.file)
|
|
|
|
|
|
|
|
|
|
|
|
required_columns = ['code', 'name', 'total_shares', 'float_shares',
|
|
|
|
df.columns = df.columns.str.strip()
|
|
|
|
'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]
|
|
|
|
renamed_df = df.rename(columns=STOCK_BASIC_COLUMN_MAP)
|
|
|
|
if missing_columns:
|
|
|
|
|
|
|
|
raise HTTPException(status_code=400, detail=f"缺少必要列: {missing_columns}")
|
|
|
|
if 'code' not in renamed_df.columns:
|
|
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="缺少必要列:证券代码")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if 'name' not in renamed_df.columns:
|
|
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="缺少必要列:证券名称")
|
|
|
|
|
|
|
|
|
|
|
|
success_count = 0
|
|
|
|
success_count = 0
|
|
|
|
error_count = 0
|
|
|
|
error_count = 0
|
|
|
|
|
|
|
|
added_count = 0
|
|
|
|
|
|
|
|
updated_count = 0
|
|
|
|
|
|
|
|
skipped_count = 0
|
|
|
|
|
|
|
|
skipped_details = []
|
|
|
|
|
|
|
|
error_details = []
|
|
|
|
|
|
|
|
|
|
|
|
for _, row in df.iterrows():
|
|
|
|
for _, row in renamed_df.iterrows():
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
existing = db.query(StockBasic).filter(StockBasic.code == str(row['code'])).first()
|
|
|
|
code_val = row.get('code')
|
|
|
|
|
|
|
|
if pd.isna(code_val):
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
code = str(code_val).strip()
|
|
|
|
|
|
|
|
if not code or code.lower() == 'nan':
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
existing = db.query(StockBasic).filter(StockBasic.code == code).first()
|
|
|
|
|
|
|
|
|
|
|
|
list_date = None
|
|
|
|
list_date = None
|
|
|
|
if pd.notna(row['list_date']):
|
|
|
|
list_date_val = row.get('list_date')
|
|
|
|
if isinstance(row['list_date'], datetime):
|
|
|
|
if pd.notna(list_date_val):
|
|
|
|
list_date = row['list_date'].date()
|
|
|
|
if isinstance(list_date_val, datetime):
|
|
|
|
elif isinstance(row['list_date'], str):
|
|
|
|
list_date = list_date_val.date()
|
|
|
|
list_date = datetime.strptime(row['list_date'], '%Y-%m-%d').date()
|
|
|
|
elif isinstance(list_date_val, str):
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
list_date = datetime.strptime(list_date_val, '%Y-%m-%d').date()
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
elif hasattr(list_date_val, 'date'):
|
|
|
|
|
|
|
|
list_date = list_date_val.date()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
name = str(row.get('name', '')) if pd.notna(row.get('name')) 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 = None
|
|
|
|
|
|
|
|
ratio_val = row.get('institution_hold_ratio')
|
|
|
|
|
|
|
|
if pd.notna(ratio_val):
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
if str(ratio_val).strip() == '--':
|
|
|
|
|
|
|
|
institution_hold_ratio = 0.0
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
institution_hold_ratio = float(ratio_val)
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
|
|
|
institution_hold_ratio = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
industry_level3 = str(row.get('industry_level3', '')) if pd.notna(row.get('industry_level3')) else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if industry_index_code:
|
|
|
|
|
|
|
|
index_basic = db.query(IndexBasic).filter(IndexBasic.code == industry_index_code).first()
|
|
|
|
|
|
|
|
if not index_basic:
|
|
|
|
|
|
|
|
index_basic = IndexBasic(
|
|
|
|
|
|
|
|
code=industry_index_code,
|
|
|
|
|
|
|
|
name=industry_index_name or industry_index_code
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
db.add(index_basic)
|
|
|
|
|
|
|
|
db.flush()
|
|
|
|
|
|
|
|
|
|
|
|
if existing:
|
|
|
|
if existing:
|
|
|
|
existing.name = str(row.get('name', existing.name))
|
|
|
|
def is_same_data():
|
|
|
|
existing.total_shares = int(row.get('total_shares', existing.total_shares)) if pd.notna(row.get('total_shares')) else existing.total_shares
|
|
|
|
def compare_ratio():
|
|
|
|
existing.float_shares = int(row.get('float_shares', existing.float_shares)) if pd.notna(row.get('float_shares')) else existing.float_shares
|
|
|
|
if existing.institution_hold_ratio is None and institution_hold_ratio is None:
|
|
|
|
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
|
|
|
|
return True
|
|
|
|
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
|
|
|
|
if existing.institution_hold_ratio is None or institution_hold_ratio is None:
|
|
|
|
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
|
|
|
|
return False
|
|
|
|
existing.industry_level3 = str(row.get('industry_level3', existing.industry_level3)) if pd.notna(row.get('industry_level3')) else existing.industry_level3
|
|
|
|
return abs(float(existing.institution_hold_ratio) - institution_hold_ratio) < 0.0001
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
(existing.name == name or (existing.name is None and name is None)) and
|
|
|
|
|
|
|
|
(existing.industry_index_name == industry_index_name or (existing.industry_index_name is None and industry_index_name is None)) and
|
|
|
|
|
|
|
|
(existing.industry_index_code == industry_index_code or (existing.industry_index_code is None and industry_index_code is None)) and
|
|
|
|
|
|
|
|
compare_ratio() and
|
|
|
|
|
|
|
|
(existing.industry_level3 == industry_level3 or (existing.industry_level3 is None and industry_level3 is None)) and
|
|
|
|
|
|
|
|
(existing.list_date == list_date or (existing.list_date is None and list_date is None))
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if is_same_data():
|
|
|
|
|
|
|
|
skipped_count += 1
|
|
|
|
|
|
|
|
skipped_details.append({
|
|
|
|
|
|
|
|
"code": code,
|
|
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
|
|
"reason": "数据相同,无需更新"
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
existing.name = name
|
|
|
|
|
|
|
|
existing.industry_index_name = industry_index_name
|
|
|
|
|
|
|
|
existing.industry_index_code = industry_index_code
|
|
|
|
|
|
|
|
existing.institution_hold_ratio = institution_hold_ratio
|
|
|
|
|
|
|
|
existing.industry_level3 = industry_level3
|
|
|
|
existing.list_date = list_date
|
|
|
|
existing.list_date = list_date
|
|
|
|
existing.updated_at = datetime.utcnow()
|
|
|
|
existing.updated_at = datetime.utcnow()
|
|
|
|
|
|
|
|
updated_count += 1
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
stock = StockBasic(
|
|
|
|
stock = StockBasic(
|
|
|
|
code=str(row['code']),
|
|
|
|
code=code,
|
|
|
|
name=str(row.get('name', '')),
|
|
|
|
name=name,
|
|
|
|
total_shares=int(row['total_shares']) if pd.notna(row['total_shares']) else None,
|
|
|
|
total_shares=None,
|
|
|
|
float_shares=int(row['float_shares']) if pd.notna(row['float_shares']) else None,
|
|
|
|
float_shares=None,
|
|
|
|
industry_index_name=str(row.get('industry_index_name', '')) if pd.notna(row.get('industry_index_name')) else None,
|
|
|
|
industry_index_name=industry_index_name,
|
|
|
|
industry_index_code=str(row.get('industry_index_code', '')) if pd.notna(row.get('industry_index_code')) else None,
|
|
|
|
industry_index_code=industry_index_code,
|
|
|
|
institution_hold_ratio=float(row['institution_hold_ratio']) if pd.notna(row['institution_hold_ratio']) else None,
|
|
|
|
institution_hold_ratio=institution_hold_ratio,
|
|
|
|
industry_level3=str(row.get('industry_level3', '')) if pd.notna(row.get('industry_level3')) else None,
|
|
|
|
industry_level3=industry_level3,
|
|
|
|
list_date=list_date
|
|
|
|
list_date=list_date
|
|
|
|
)
|
|
|
|
)
|
|
|
|
db.add(stock)
|
|
|
|
db.add(stock)
|
|
|
|
|
|
|
|
added_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
success_count += 1
|
|
|
|
success_count += 1
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
logger.error(f"导入股票{row.get('code')}失败: {str(e)}")
|
|
|
|
logger.error(f"导入股票{row.get('code')}失败: {str(e)}")
|
|
|
|
error_count += 1
|
|
|
|
error_count += 1
|
|
|
|
|
|
|
|
error_details.append({
|
|
|
|
|
|
|
|
"code": str(row.get('code', '')),
|
|
|
|
|
|
|
|
"name": str(row.get('name', '')) if pd.notna(row.get('name')) else '',
|
|
|
|
|
|
|
|
"reason": str(e)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
db.commit()
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return ResponseModel(data={
|
|
|
|
return ResponseModel(data={
|
|
|
|
"success_count": success_count,
|
|
|
|
"success_count": success_count,
|
|
|
|
"error_count": error_count,
|
|
|
|
"error_count": error_count,
|
|
|
|
"total_count": len(df)
|
|
|
|
"total_count": len(df),
|
|
|
|
|
|
|
|
"added_count": added_count,
|
|
|
|
|
|
|
|
"updated_count": updated_count,
|
|
|
|
|
|
|
|
"skipped_count": skipped_count,
|
|
|
|
|
|
|
|
"skipped_details": skipped_details[:100],
|
|
|
|
|
|
|
|
"error_details": error_details[:100]
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
|