更新了数据库格式

This commit is contained in:
Sebastian
2026-01-17 15:49:02 +08:00
parent 87ee11256b
commit 0abc79d164
13 changed files with 334 additions and 76 deletions

View File

@@ -1,7 +1,7 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
from database import init_db, get_db
from models import FundBasicInfo, FundDetail
from models import FundBasicInfo, FundTrend, FundEstimate, FundPortfolio, FundExtraData
from fund_api import FundAPI
from fund_list_cache import get_fund_list_cache
from sqlalchemy.orm import Session
@@ -15,6 +15,71 @@ init_db()
fund_api = FundAPI()
fund_list_cache = get_fund_list_cache()
def _json_dumps(data):
return json.dumps(data, ensure_ascii=False) if data is not None else None
def _json_loads(data, default):
if not data:
return default
try:
return json.loads(data)
except Exception:
return default
def _build_cached_response(db: Session, fund_code: str):
basic = db.query(FundBasicInfo).filter(FundBasicInfo.fund_code == fund_code).first()
trend = db.query(FundTrend).filter(FundTrend.fund_code == fund_code).first()
estimate = db.query(FundEstimate).filter(FundEstimate.fund_code == fund_code).first()
portfolio = db.query(FundPortfolio).filter(FundPortfolio.fund_code == fund_code).first()
extra = db.query(FundExtraData).filter(FundExtraData.fund_code == fund_code).first()
if not any([basic, trend, estimate, portfolio, extra]):
return None
data = {}
if basic:
data['basic_info'] = _json_loads(basic.basic_json, {})
data['performance'] = _json_loads(basic.performance_json, {})
if trend:
data['net_worth_trend'] = _json_loads(trend.net_worth_trend_json, [])
data['accumulated_net_worth'] = _json_loads(trend.accumulated_net_worth_json, [])
data['position_trend'] = _json_loads(trend.position_trend_json, [])
data['total_return_trend'] = _json_loads(trend.total_return_trend_json, [])
data['ranking_trend'] = _json_loads(trend.ranking_trend_json, [])
data['ranking_percentage'] = _json_loads(trend.ranking_percentage_json, [])
data['scale_fluctuation'] = _json_loads(trend.scale_fluctuation_json, {})
if estimate:
data['realtime_estimate'] = {
'name': estimate.name,
'fund_code': fund_code,
'net_worth': estimate.net_worth,
'net_worth_date': estimate.net_worth_date,
'estimate_value': estimate.estimate_value,
'estimate_change': estimate.estimate_change,
'estimate_time': estimate.estimate_time
}
if portfolio:
data['portfolio'] = {
'stock_codes': _json_loads(portfolio.stock_codes_json, []),
'bond_codes': _json_loads(portfolio.bond_codes_json, []),
'stock_codes_new': _json_loads(portfolio.stock_codes_new_json, []),
'bond_codes_new': _json_loads(portfolio.bond_codes_new_json, [])
}
if extra:
data['holder_structure'] = _json_loads(extra.holder_structure_json, {})
data['asset_allocation'] = _json_loads(extra.asset_allocation_json, {})
data['performance_evaluation'] = _json_loads(extra.performance_evaluation_json, {})
data['fund_managers'] = _json_loads(extra.fund_managers_json, [])
data['subscription_redemption'] = _json_loads(extra.subscription_redemption_json, {})
data['same_type_funds'] = _json_loads(extra.same_type_funds_json, [])
return data
@app.route('/')
def hello():
"""测试接口是否可用"""
@@ -57,23 +122,132 @@ def get_fund_detail(fund_code):
fund_data = fund_api.get_fund_data(fund_code)
if fund_data:
# 检查数据库是否存在记录
cached_fund = db.query(FundDetail).filter(FundDetail.fund_code == fund_code).first()
# 更新或新增记录
if cached_fund:
cached_fund.data_json = json.dumps(fund_data, ensure_ascii=False)
cached_fund.net_worth_trend = json.dumps(fund_data.get('net_worth_trend', []), ensure_ascii=False)
cached_fund.basic_info = json.dumps(fund_data.get('basic_info', {}), ensure_ascii=False)
basic_info = fund_data.get('basic_info', {})
performance = fund_data.get('performance', {})
trend = {
'net_worth_trend': fund_data.get('net_worth_trend', []),
'accumulated_net_worth': fund_data.get('accumulated_net_worth', []),
'position_trend': fund_data.get('position_trend', []),
'total_return_trend': fund_data.get('total_return_trend', []),
'ranking_trend': fund_data.get('ranking_trend', []),
'ranking_percentage': fund_data.get('ranking_percentage', []),
'scale_fluctuation': fund_data.get('scale_fluctuation', {})
}
estimate = fund_data.get('realtime_estimate', {})
portfolio = fund_data.get('portfolio', {})
extra = {
'holder_structure': fund_data.get('holder_structure', {}),
'asset_allocation': fund_data.get('asset_allocation', {}),
'performance_evaluation': fund_data.get('performance_evaluation', {}),
'fund_managers': fund_data.get('fund_managers', []),
'subscription_redemption': fund_data.get('subscription_redemption', {}),
'same_type_funds': fund_data.get('same_type_funds', [])
}
basic_record = db.query(FundBasicInfo).filter(FundBasicInfo.fund_code == fund_code).first()
if basic_record:
basic_record.fund_name = basic_info.get('fund_name')
basic_record.fund_type = basic_info.get('fund_type')
basic_record.original_rate = basic_info.get('original_rate')
basic_record.current_rate = basic_info.get('current_rate')
basic_record.min_subscription_amount = basic_info.get('min_subscription_amount')
basic_record.is_hb = basic_info.get('is_hb')
basic_record.basic_json = _json_dumps(basic_info)
basic_record.performance_json = _json_dumps(performance)
else:
fund_detail = FundDetail(
basic_record = FundBasicInfo(
fund_code=fund_code,
data_json=json.dumps(fund_data, ensure_ascii=False),
net_worth_trend=json.dumps(fund_data.get('net_worth_trend', []), ensure_ascii=False),
basic_info=json.dumps(fund_data.get('basic_info', {}), ensure_ascii=False)
fund_name=basic_info.get('fund_name') or fund_code,
fund_type=basic_info.get('fund_type'),
original_rate=basic_info.get('original_rate'),
current_rate=basic_info.get('current_rate'),
min_subscription_amount=basic_info.get('min_subscription_amount'),
is_hb=basic_info.get('is_hb'),
basic_json=_json_dumps(basic_info),
performance_json=_json_dumps(performance)
)
db.add(fund_detail)
db.add(basic_record)
trend_record = db.query(FundTrend).filter(FundTrend.fund_code == fund_code).first()
if trend_record:
trend_record.net_worth_trend_json = _json_dumps(trend['net_worth_trend'])
trend_record.accumulated_net_worth_json = _json_dumps(trend['accumulated_net_worth'])
trend_record.position_trend_json = _json_dumps(trend['position_trend'])
trend_record.total_return_trend_json = _json_dumps(trend['total_return_trend'])
trend_record.ranking_trend_json = _json_dumps(trend['ranking_trend'])
trend_record.ranking_percentage_json = _json_dumps(trend['ranking_percentage'])
trend_record.scale_fluctuation_json = _json_dumps(trend['scale_fluctuation'])
else:
trend_record = FundTrend(
fund_code=fund_code,
net_worth_trend_json=_json_dumps(trend['net_worth_trend']),
accumulated_net_worth_json=_json_dumps(trend['accumulated_net_worth']),
position_trend_json=_json_dumps(trend['position_trend']),
total_return_trend_json=_json_dumps(trend['total_return_trend']),
ranking_trend_json=_json_dumps(trend['ranking_trend']),
ranking_percentage_json=_json_dumps(trend['ranking_percentage']),
scale_fluctuation_json=_json_dumps(trend['scale_fluctuation'])
)
db.add(trend_record)
estimate_record = db.query(FundEstimate).filter(FundEstimate.fund_code == fund_code).first()
if estimate_record:
estimate_record.name = estimate.get('name')
estimate_record.net_worth = estimate.get('net_worth')
estimate_record.net_worth_date = estimate.get('net_worth_date')
estimate_record.estimate_value = estimate.get('estimate_value')
estimate_record.estimate_change = estimate.get('estimate_change')
estimate_record.estimate_time = estimate.get('estimate_time')
else:
estimate_record = FundEstimate(
fund_code=fund_code,
name=estimate.get('name'),
net_worth=estimate.get('net_worth'),
net_worth_date=estimate.get('net_worth_date'),
estimate_value=estimate.get('estimate_value'),
estimate_change=estimate.get('estimate_change'),
estimate_time=estimate.get('estimate_time')
)
db.add(estimate_record)
portfolio_record = db.query(FundPortfolio).filter(FundPortfolio.fund_code == fund_code).first()
if portfolio_record:
portfolio_record.stock_codes_json = _json_dumps(portfolio.get('stock_codes', []))
portfolio_record.bond_codes_json = _json_dumps(portfolio.get('bond_codes', []))
portfolio_record.stock_codes_new_json = _json_dumps(portfolio.get('stock_codes_new', []))
portfolio_record.bond_codes_new_json = _json_dumps(portfolio.get('bond_codes_new', []))
else:
portfolio_record = FundPortfolio(
fund_code=fund_code,
stock_codes_json=_json_dumps(portfolio.get('stock_codes', [])),
bond_codes_json=_json_dumps(portfolio.get('bond_codes', [])),
stock_codes_new_json=_json_dumps(portfolio.get('stock_codes_new', [])),
bond_codes_new_json=_json_dumps(portfolio.get('bond_codes_new', []))
)
db.add(portfolio_record)
extra_record = db.query(FundExtraData).filter(FundExtraData.fund_code == fund_code).first()
if extra_record:
extra_record.holder_structure_json = _json_dumps(extra['holder_structure'])
extra_record.asset_allocation_json = _json_dumps(extra['asset_allocation'])
extra_record.performance_evaluation_json = _json_dumps(extra['performance_evaluation'])
extra_record.fund_managers_json = _json_dumps(extra['fund_managers'])
extra_record.subscription_redemption_json = _json_dumps(extra['subscription_redemption'])
extra_record.same_type_funds_json = _json_dumps(extra['same_type_funds'])
else:
extra_record = FundExtraData(
fund_code=fund_code,
holder_structure_json=_json_dumps(extra['holder_structure']),
asset_allocation_json=_json_dumps(extra['asset_allocation']),
performance_evaluation_json=_json_dumps(extra['performance_evaluation']),
fund_managers_json=_json_dumps(extra['fund_managers']),
subscription_redemption_json=_json_dumps(extra['subscription_redemption']),
same_type_funds_json=_json_dumps(extra['same_type_funds'])
)
db.add(extra_record)
try:
db.commit() # 提交事务
db.commit()
except Exception as e:
print(f"Error saving to database: {e}")
db.rollback()
@@ -81,13 +255,9 @@ def get_fund_detail(fund_code):
return jsonify(fund_data)
# 如果API获取失败尝试从数据库获取缓存数据作为兜底
cached_fund = db.query(FundDetail).filter(FundDetail.fund_code == fund_code).first()
if cached_fund:
try:
data = json.loads(cached_fund.data_json)
return jsonify(data)
except:
pass
cached_data = _build_cached_response(db, fund_code)
if cached_data:
return jsonify(cached_data)
return jsonify({"error": "Fund not found"}), 404
@@ -98,13 +268,19 @@ def get_fund_basic(fund_code):
return jsonify({"error": "Fund code is required"}), 400
fund_data = fund_api.get_fund_data(fund_code)
if fund_data and fund_data.get('basic_info'):
# 合并 basic_info 和 performance 数据
result = {
**fund_data.get('basic_info', {}),
**fund_data.get('performance', {})
}
return jsonify(result)
else:
db = next(get_db())
basic = db.query(FundBasicInfo).filter(FundBasicInfo.fund_code == fund_code).first()
if basic:
basic_info = _json_loads(basic.basic_json, {})
performance = _json_loads(basic.performance_json, {})
return jsonify({**basic_info, **performance})
return jsonify({"error": "Fund basic info not found"}), 404
@app.route('/api/fund/<fund_code>/trend', methods=['GET'])
@@ -119,7 +295,15 @@ def get_fund_trend(fund_code):
"net_worth_trend": fund_data['net_worth_trend'],
"accumulated_net_worth": fund_data.get('accumulated_net_worth', [])
})
else:
db = next(get_db())
trend = db.query(FundTrend).filter(FundTrend.fund_code == fund_code).first()
if trend:
return jsonify({
"net_worth_trend": _json_loads(trend.net_worth_trend_json, []),
"accumulated_net_worth": _json_loads(trend.accumulated_net_worth_json, [])
})
return jsonify({"error": "Fund trend data not found"}), 404
if __name__ == '__main__':

View File

@@ -10,17 +10,67 @@ class FundBasicInfo(Base):
id = Column(Integer, primary_key=True, autoincrement=True)
fund_code = Column(String(6), unique=True, nullable=False)
fund_name = Column(String(100), nullable=False)
fund_name_en = Column(String(200))
fund_type = Column(String(50))
original_rate = Column(Float)
current_rate = Column(Float)
min_subscription_amount = Column(String(50))
is_hb = Column(String(10))
basic_json = Column(Text)
performance_json = Column(Text)
created_time = Column(DateTime, default=datetime.now)
updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class FundDetail(Base):
__tablename__ = 'fund_detail'
class FundTrend(Base):
__tablename__ = 'fund_trend'
id = Column(Integer, primary_key=True, autoincrement=True)
fund_code = Column(String(6), nullable=False)
data_json = Column(Text) # 存储完整的基金数据
net_worth_trend = Column(Text) # 单位净值走势
basic_info = Column(Text) # 基础信息
created_time = Column(DateTime, default=datetime.now)
fund_code = Column(String(6), unique=True, nullable=False)
net_worth_trend_json = Column(Text)
accumulated_net_worth_json = Column(Text)
position_trend_json = Column(Text)
total_return_trend_json = Column(Text)
ranking_trend_json = Column(Text)
ranking_percentage_json = Column(Text)
scale_fluctuation_json = Column(Text)
updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class FundEstimate(Base):
__tablename__ = 'fund_estimate'
id = Column(Integer, primary_key=True, autoincrement=True)
fund_code = Column(String(6), unique=True, nullable=False)
name = Column(String(100))
net_worth = Column(String(50))
net_worth_date = Column(String(50))
estimate_value = Column(String(50))
estimate_change = Column(String(50))
estimate_time = Column(String(50))
updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class FundPortfolio(Base):
__tablename__ = 'fund_portfolio'
id = Column(Integer, primary_key=True, autoincrement=True)
fund_code = Column(String(6), unique=True, nullable=False)
stock_codes_json = Column(Text)
bond_codes_json = Column(Text)
stock_codes_new_json = Column(Text)
bond_codes_new_json = Column(Text)
updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class FundExtraData(Base):
__tablename__ = 'fund_extra_data'
id = Column(Integer, primary_key=True, autoincrement=True)
fund_code = Column(String(6), unique=True, nullable=False)
holder_structure_json = Column(Text)
asset_allocation_json = Column(Text)
performance_evaluation_json = Column(Text)
fund_managers_json = Column(Text)
subscription_redemption_json = Column(Text)
same_type_funds_json = Column(Text)
updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now)

View File

@@ -2,6 +2,7 @@ import requests
import json
import threading
import time
import os
class StockService:
_instance = None
@@ -20,13 +21,54 @@ class StockService:
return
self.stock_details = {} # Map code -> {name, market}
self.last_update = 0
self.cache_ttl = 24 * 3600 # 24 hours
self.cache_ttl = 24 * 3600 * 10 # 10 days
self.cache_file = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "Data", "stock_list_cache.json")
)
self._load_data()
self._initialized = True
def _load_data(self):
"""Fetch data from APIs in a separate thread to avoid blocking startup"""
threading.Thread(target=self._fetch_all, daemon=True).start()
"""Load stock data from local cache; refresh if missing or expired."""
loaded = self._load_from_cache()
if not loaded or self._is_cache_expired():
threading.Thread(target=self._refresh_cache, daemon=True).start()
def _load_from_cache(self):
if not os.path.exists(self.cache_file):
return False
try:
with open(self.cache_file, "r", encoding="utf-8") as f:
data = json.load(f)
self.last_update = data.get("last_update", 0)
self.stock_details = data.get("stock_details", {})
return bool(self.stock_details)
except Exception as e:
print(f"Error loading stock cache: {e}")
return False
def _is_cache_expired(self):
if not self.last_update:
return True
return (time.time() - self.last_update) > self.cache_ttl
def _save_to_cache(self):
try:
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
with open(self.cache_file, "w", encoding="utf-8") as f:
json.dump({
"last_update": self.last_update,
"stock_details": self.stock_details
}, f, ensure_ascii=False)
except Exception as e:
print(f"Error saving stock cache: {e}")
def _refresh_cache(self):
"""Download stock list and save to local cache."""
self._fetch_all()
self._save_to_cache()
def _fetch_all(self):
self._fetch_hk_stocks()

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -15,7 +15,7 @@
<th>时间</th>
<th v-for="serie in series" :key="serie.name">
<span class="legend-dot" :style="{ background: getColor(serie.name) }"></span>
{{ serie.name }}
{{ formatLegendName(serie.name) }}
</th>
</tr>
</thead>
@@ -70,6 +70,11 @@ export default {
return colors[name] || '#5470c6'
}
const formatLegendName = (name) => {
if (!name) return ''
return name.replace(/比例/g, '')
}
const formatValue = (value) => {
if (value === null || value === undefined) return '--'
return value.toFixed(2) + '%'
@@ -86,7 +91,7 @@ export default {
// 准备堆叠柱状图数据
const seriesData = series.value.map(serie => ({
name: serie.name,
name: formatLegendName(serie.name),
type: 'bar',
stack: 'total',
barWidth: '50%',
@@ -119,7 +124,7 @@ export default {
}
},
legend: {
data: series.value.map(s => s.name),
data: series.value.map(s => formatLegendName(s.name)),
bottom: 0
},
grid: {
@@ -168,6 +173,7 @@ export default {
series,
hasData,
getColor,
formatLegendName,
formatValue
}
}

View File

@@ -120,6 +120,7 @@ export default {
.portfolio-content {
flex: 1;
overflow-y: auto;
padding-right: 12px;
}
.portfolio-header {
@@ -189,7 +190,7 @@ export default {
.col-market {
width: 60px;
text-align: right;
text-align: left;
color: #888;
font-size: 11px;
}

View File

@@ -56,7 +56,7 @@ export default {
emits: ['fund-select'],
setup(props, { emit }) {
const activePeriod = ref(0)
const periods = ['近1年', '近2年', '近3年', '近5年', '今年以来']
const periods = ['主题1', '主题2', '主题3', '主题4', '主题5']
const currentFunds = computed(() => {
if (!props.sameTypeFunds || !Array.isArray(props.sameTypeFunds)) {

View File

@@ -187,36 +187,10 @@
### 1.2 核心函数解释
## 2. 后续开发计划
- **get_fund_basic_info(fund_code)**
- 功能:获取基金综合信息,包括实时估值、基本资料等
- 输入:基金代码
- 输出:基金最近一个交易日的单位净值(dwjz)、基金名称(name)、净值日期(jzrq)、估计净值(gsz)、估计增长率%(gszzl)、估计日期(gztime)等
1. 添加基金自选功能便于浏览持有基金的情况写json或者数据库
2. 添加基金对比功能,支持多只基金放在同一版图进行数据对比
3. 添加模型预测功能通过LSTM或者CNN实现对基金走势的预测
4. 添加基金回测功能,找到合适的量化策略
- **get_fund_net_worth_history(fund_code, years=None)**
- 功能:获取基金历史净值数据
- 输入基金代码显示最近多少年的数据None表示显示全部
- 输出包含日期和净值的DataFrame
- **calculate_max_drawdown(net_worth_series)**
- 功能:计算最大回撤
- 输入:净值序列
- 输出:包含最大回撤信息的字典
- **calculate_technical_indicators(df, window=20)**
- 功能计算技术指标包含20日均值累计收益率等
- **plot_fund_vector_graph(fund_code, years=None, save_path=None)**
- 功能:绘制基金历史表现矢量图
- fund_code (str): 基金代码
- years (int, optional): 显示最近多少年的数据None表示显示全部数据1表示最近1年的数据依此类推
- save_path (str, optional): 保存路径,例如 'fund_plot.svg';默认为 None (直接显示图表)
## 预测策略
LSTM方法
- **回溯** :滑动窗口的长度,表示我们回溯过去的周期数。
- **前瞻** :我们想要预测未来的周期数。