更新了数据库格式
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
238
Backend/app.py
238
Backend/app.py
@@ -1,7 +1,7 @@
|
|||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from database import init_db, get_db
|
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_api import FundAPI
|
||||||
from fund_list_cache import get_fund_list_cache
|
from fund_list_cache import get_fund_list_cache
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -15,6 +15,71 @@ init_db()
|
|||||||
fund_api = FundAPI()
|
fund_api = FundAPI()
|
||||||
fund_list_cache = get_fund_list_cache()
|
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('/')
|
@app.route('/')
|
||||||
def hello():
|
def hello():
|
||||||
"""测试接口是否可用"""
|
"""测试接口是否可用"""
|
||||||
@@ -57,37 +122,142 @@ def get_fund_detail(fund_code):
|
|||||||
fund_data = fund_api.get_fund_data(fund_code)
|
fund_data = fund_api.get_fund_data(fund_code)
|
||||||
|
|
||||||
if fund_data:
|
if fund_data:
|
||||||
# 检查数据库是否存在记录
|
basic_info = fund_data.get('basic_info', {})
|
||||||
cached_fund = db.query(FundDetail).filter(FundDetail.fund_code == fund_code).first()
|
performance = fund_data.get('performance', {})
|
||||||
# 更新或新增记录
|
trend = {
|
||||||
if cached_fund:
|
'net_worth_trend': fund_data.get('net_worth_trend', []),
|
||||||
cached_fund.data_json = json.dumps(fund_data, ensure_ascii=False)
|
'accumulated_net_worth': fund_data.get('accumulated_net_worth', []),
|
||||||
cached_fund.net_worth_trend = json.dumps(fund_data.get('net_worth_trend', []), ensure_ascii=False)
|
'position_trend': fund_data.get('position_trend', []),
|
||||||
cached_fund.basic_info = json.dumps(fund_data.get('basic_info', {}), ensure_ascii=False)
|
'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:
|
else:
|
||||||
fund_detail = FundDetail(
|
basic_record = FundBasicInfo(
|
||||||
fund_code=fund_code,
|
fund_code=fund_code,
|
||||||
data_json=json.dumps(fund_data, ensure_ascii=False),
|
fund_name=basic_info.get('fund_name') or fund_code,
|
||||||
net_worth_trend=json.dumps(fund_data.get('net_worth_trend', []), ensure_ascii=False),
|
fund_type=basic_info.get('fund_type'),
|
||||||
basic_info=json.dumps(fund_data.get('basic_info', {}), ensure_ascii=False)
|
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:
|
try:
|
||||||
db.commit() # 提交事务
|
db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving to database: {e}")
|
print(f"Error saving to database: {e}")
|
||||||
db.rollback()
|
db.rollback()
|
||||||
|
|
||||||
return jsonify(fund_data)
|
return jsonify(fund_data)
|
||||||
|
|
||||||
# 如果API获取失败,尝试从数据库获取缓存数据作为兜底
|
# 如果API获取失败,尝试从数据库获取缓存数据作为兜底
|
||||||
cached_fund = db.query(FundDetail).filter(FundDetail.fund_code == fund_code).first()
|
cached_data = _build_cached_response(db, fund_code)
|
||||||
if cached_fund:
|
if cached_data:
|
||||||
try:
|
return jsonify(cached_data)
|
||||||
data = json.loads(cached_fund.data_json)
|
|
||||||
return jsonify(data)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return jsonify({"error": "Fund not found"}), 404
|
return jsonify({"error": "Fund not found"}), 404
|
||||||
|
|
||||||
@@ -98,14 +268,20 @@ def get_fund_basic(fund_code):
|
|||||||
return jsonify({"error": "Fund code is required"}), 400
|
return jsonify({"error": "Fund code is required"}), 400
|
||||||
fund_data = fund_api.get_fund_data(fund_code)
|
fund_data = fund_api.get_fund_data(fund_code)
|
||||||
if fund_data and fund_data.get('basic_info'):
|
if fund_data and fund_data.get('basic_info'):
|
||||||
# 合并 basic_info 和 performance 数据
|
|
||||||
result = {
|
result = {
|
||||||
**fund_data.get('basic_info', {}),
|
**fund_data.get('basic_info', {}),
|
||||||
**fund_data.get('performance', {})
|
**fund_data.get('performance', {})
|
||||||
}
|
}
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
else:
|
|
||||||
return jsonify({"error": "Fund basic info not found"}), 404
|
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'])
|
@app.route('/api/fund/<fund_code>/trend', methods=['GET'])
|
||||||
def get_fund_trend(fund_code):
|
def get_fund_trend(fund_code):
|
||||||
@@ -119,8 +295,16 @@ def get_fund_trend(fund_code):
|
|||||||
"net_worth_trend": fund_data['net_worth_trend'],
|
"net_worth_trend": fund_data['net_worth_trend'],
|
||||||
"accumulated_net_worth": fund_data.get('accumulated_net_worth', [])
|
"accumulated_net_worth": fund_data.get('accumulated_net_worth', [])
|
||||||
})
|
})
|
||||||
else:
|
|
||||||
return jsonify({"error": "Fund trend data not found"}), 404
|
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__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
@@ -6,21 +6,71 @@ Base = declarative_base()
|
|||||||
|
|
||||||
class FundBasicInfo(Base):
|
class FundBasicInfo(Base):
|
||||||
__tablename__ = 'fund_basic_info'
|
__tablename__ = 'fund_basic_info'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
fund_code = Column(String(6), unique=True, nullable=False)
|
fund_code = Column(String(6), unique=True, nullable=False)
|
||||||
fund_name = Column(String(100), nullable=False)
|
fund_name = Column(String(100), nullable=False)
|
||||||
fund_name_en = Column(String(200))
|
|
||||||
fund_type = Column(String(50))
|
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)
|
created_time = Column(DateTime, default=datetime.now)
|
||||||
updated_time = Column(DateTime, default=datetime.now, onupdate=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)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
fund_code = Column(String(6), nullable=False)
|
fund_code = Column(String(6), unique=True, nullable=False)
|
||||||
data_json = Column(Text) # 存储完整的基金数据
|
net_worth_trend_json = Column(Text)
|
||||||
net_worth_trend = Column(Text) # 单位净值走势
|
accumulated_net_worth_json = Column(Text)
|
||||||
basic_info = Column(Text) # 基础信息
|
position_trend_json = Column(Text)
|
||||||
created_time = Column(DateTime, default=datetime.now)
|
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)
|
||||||
@@ -2,6 +2,7 @@ import requests
|
|||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
class StockService:
|
class StockService:
|
||||||
_instance = None
|
_instance = None
|
||||||
@@ -20,13 +21,54 @@ class StockService:
|
|||||||
return
|
return
|
||||||
self.stock_details = {} # Map code -> {name, market}
|
self.stock_details = {} # Map code -> {name, market}
|
||||||
self.last_update = 0
|
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._load_data()
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
def _load_data(self):
|
def _load_data(self):
|
||||||
"""Fetch data from APIs in a separate thread to avoid blocking startup"""
|
"""Load stock data from local cache; refresh if missing or expired."""
|
||||||
threading.Thread(target=self._fetch_all, daemon=True).start()
|
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):
|
def _fetch_all(self):
|
||||||
self._fetch_hk_stocks()
|
self._fetch_hk_stocks()
|
||||||
|
|||||||
BIN
Data/funds.db
BIN
Data/funds.db
Binary file not shown.
1
Data/stock_list_cache.json
Normal file
1
Data/stock_list_cache.json
Normal file
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@
|
|||||||
<th>时间</th>
|
<th>时间</th>
|
||||||
<th v-for="serie in series" :key="serie.name">
|
<th v-for="serie in series" :key="serie.name">
|
||||||
<span class="legend-dot" :style="{ background: getColor(serie.name) }"></span>
|
<span class="legend-dot" :style="{ background: getColor(serie.name) }"></span>
|
||||||
{{ serie.name }}
|
{{ formatLegendName(serie.name) }}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -70,6 +70,11 @@ export default {
|
|||||||
return colors[name] || '#5470c6'
|
return colors[name] || '#5470c6'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatLegendName = (name) => {
|
||||||
|
if (!name) return ''
|
||||||
|
return name.replace(/比例/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
const formatValue = (value) => {
|
const formatValue = (value) => {
|
||||||
if (value === null || value === undefined) return '--'
|
if (value === null || value === undefined) return '--'
|
||||||
return value.toFixed(2) + '%'
|
return value.toFixed(2) + '%'
|
||||||
@@ -86,7 +91,7 @@ export default {
|
|||||||
|
|
||||||
// 准备堆叠柱状图数据
|
// 准备堆叠柱状图数据
|
||||||
const seriesData = series.value.map(serie => ({
|
const seriesData = series.value.map(serie => ({
|
||||||
name: serie.name,
|
name: formatLegendName(serie.name),
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
stack: 'total',
|
stack: 'total',
|
||||||
barWidth: '50%',
|
barWidth: '50%',
|
||||||
@@ -119,7 +124,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
data: series.value.map(s => s.name),
|
data: series.value.map(s => formatLegendName(s.name)),
|
||||||
bottom: 0
|
bottom: 0
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
@@ -168,6 +173,7 @@ export default {
|
|||||||
series,
|
series,
|
||||||
hasData,
|
hasData,
|
||||||
getColor,
|
getColor,
|
||||||
|
formatLegendName,
|
||||||
formatValue
|
formatValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export default {
|
|||||||
.portfolio-content {
|
.portfolio-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portfolio-header {
|
.portfolio-header {
|
||||||
@@ -189,7 +190,7 @@ export default {
|
|||||||
|
|
||||||
.col-market {
|
.col-market {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
text-align: right;
|
text-align: left;
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default {
|
|||||||
emits: ['fund-select'],
|
emits: ['fund-select'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const activePeriod = ref(0)
|
const activePeriod = ref(0)
|
||||||
const periods = ['近1年', '近2年', '近3年', '近5年', '今年以来']
|
const periods = ['主题1', '主题2', '主题3', '主题4', '主题5']
|
||||||
|
|
||||||
const currentFunds = computed(() => {
|
const currentFunds = computed(() => {
|
||||||
if (!props.sameTypeFunds || !Array.isArray(props.sameTypeFunds)) {
|
if (!props.sameTypeFunds || !Array.isArray(props.sameTypeFunds)) {
|
||||||
|
|||||||
36
开发笔记.md
36
开发笔记.md
@@ -187,36 +187,10 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 1.2 核心函数解释
|
## 2. 后续开发计划
|
||||||
|
|
||||||
- **get_fund_basic_info(fund_code)**
|
1. 添加基金自选功能,便于浏览持有基金的情况(写json或者数据库)
|
||||||
- 功能:获取基金综合信息,包括实时估值、基本资料等
|
2. 添加基金对比功能,支持多只基金放在同一版图进行数据对比
|
||||||
- 输入:基金代码
|
3. 添加模型预测功能,通过LSTM或者CNN实现对基金走势的预测
|
||||||
- 输出:基金最近一个交易日的单位净值(dwjz)、基金名称(name)、净值日期(jzrq)、估计净值(gsz)、估计增长率%(gszzl)、估计日期(gztime)等
|
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方法
|
|
||||||
|
|
||||||
- **回溯** :滑动窗口的长度,表示我们回溯过去的周期数。
|
|
||||||
- **前瞻** :我们想要预测未来的周期数。
|
|
||||||
Reference in New Issue
Block a user