Spaces:
Runtime error
Runtime error
File size: 10,499 Bytes
976166f c8b5a38 588011f 976166f 48b892d 976166f 48b892d 976166f 48b892d 976166f 48b892d 976166f 2b059b0 48b892d 2b059b0 48b892d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
'''
contain method for api call to jqdatasdk
'''
from dotenv import load_dotenv
from datetime import datetime, timedelta
import jqdatasdk as jq
import pandas as pd
from typing import List, Optional
from sqlalchemy import create_engine
import table_schema as ts
import os
import utils
db_url = 'sqlite:///instance/local.db'
load_dotenv()
user_name = os.environ.get('JQDATA_USER')
password = os.environ.get('JQDATA_PASSWORD')
def auth_api(func):
"""
decorator for function require jqdatasdk api
"""
def wrapper(*args, **kwargs):
if (not jq.is_auth()):
jq.auth(user_name, password)
result = func(*args, **kwargs)
return result
return wrapper
def aggregate_sector(input: str) -> Optional[str]:
'''
mapping from sector to aggregated sector retur None if not found
this handling is for spotting undefined sector in current mapping
later
Return: str -- aggregated sector
None if no mapping
'''
mapping = {
'电气设备I': '工业',
'建筑装饰I': '工业',
'交通运输I': '工业',
'机械设备I': '工业',
'国防军工I': '工业',
'综合I': '工业',
'电子I': '信息与通信',
'计算机I': '信息与通信',
'通信I': '信息与通信',
'传媒I': '信息与通信',
'纺织服装I': '消费',
'家用电器I': '消费',
'汽车I': '消费',
'休闲服务I': '消费',
'商业贸易I': '消费',
'食品饮料I': '消费',
'美容护理I': '消费',
'农林牧渔I': '消费',
'钢铁I': '原料与能源',
'建筑材料I': '原料与能源',
'有色金属I': '原料与能源',
'化工I': '原料与能源',
'轻工制造I': '原料与能源',
'煤炭I': '原料与能源',
'石油石化I': '原料与能源',
'采掘I': '原料与能源',
'医药生物I': '医药卫生',
'公用事业I': '公用事业',
'环保I': '公用事业',
'房地产I': '金融与地产',
'银行I': '金融与地产',
'非银金融I': '金融与地产'
}
# return the first mapping found
sectors = input.split(" ")
maped_name = "其他"
for sector in sectors:
maped_name = mapping.get(sector, None)
if maped_name is not None:
return maped_name
return maped_name
@auth_api
def get_all_stock_info() -> tuple[pd.DataFrame, List[str]]:
'''
return all stock information
Return
------
tuple: tuple(pd.DataFrame, List[str])
DataFrame -- display_name | name | start_date | end_date | type
'''
error = []
try:
df = jq.get_all_securities()
df['ticker'] = df.index
df.reset_index(drop=True, inplace=True)
# df.reset_index(inplace=True)
return df, error
except Exception as e:
error.append(f'get_all_stock_info\n{e}')
return None, error
@auth_api
def add_detail_to_stocks(df: pd.DataFrame) -> List[str]:
"""
add display_name, name, sector, and aggregate sector to each stock if not exist already
return a list of error message
Args: pd.DataFrame
ticker | date | weight | sector | aggregate_sector | display_name | name
Returns: List[str], error messages
"""
error = []
df[['sector', 'aggregate_sector']] = df.groupby(
'ticker')[['sector', 'aggregate_sector']].ffill()
df[['display_name', 'name']] = df.groupby(
'ticker')[['display_name', 'name']].ffill()
not_have_sector = list(
df[df['aggregate_sector'].isnull()]['ticker'].unique())
not_have_name = list(df[df['name'].isnull()]['ticker'].unique())
# sector and aggregate sector
if len(not_have_sector) != 0:
try:
sectors = jq.get_industry(security=not_have_sector)
df['sector'] = df.apply(lambda x: x.sector if not pd.isna(x.sector)
else " ".join(value['industry_name']
for value in sectors[x.ticker].values()), axis=1)
df['aggregate_sector'] = df.apply(
lambda x: x.aggregate_sector if not pd.isna(x.aggregate_sector)
else aggregate_sector(x.sector), axis=1
)
except Exception as e:
error.append(f'Error on creaet_sector_information\n{ticker}\n{e}')
# display_name and name
if len(not_have_name) != 0:
try:
for ticker in not_have_name:
detail = jq.get_security_info(ticker)
df.loc[df.ticker.isin(not_have_name)
]['display_name'] = detail.display_name
df.loc[df.ticker.isin(not_have_name)]['name'] = detail.name
except Exception as e:
error.append(f'Error on get display_name and name\n{ticker}\n{e}')
return error
@auth_api
def update_portfolio_profile(stocks: List[dict], current_p: pd.DataFrame = None) -> tuple[pd.DataFrame, List[str]]:
"""create or update a portfolio profile,
return a time series of profile
Parameters
----------
stocks : List[{ticker: Str, shares: float, date:datetime}]
update profile with a list of stock information
current_p : pd.DataFrame, optional
current portfolio profile, default is None
Returns
-------
updated_profile : pd.DataFrame
ticker | date | weight | sector | aggregate_sector | display_name | name
error : List[str]
a list of error message
"""
error = []
profile_df = pd.DataFrame(stocks)
profile_df['sector'] = None
profile_df['aggregate_sector'] = None
# add display_name
try:
with create_engine(db_url).connect() as conn:
info_df = pd.read_sql_table(ts.STOCKS_DETAILS_TABLE, conn)
profile_df = pd.merge(
profile_df, info_df[['display_name', 'ticker', 'name', 'aggregate_sector', ]], on='ticker', how='left')
except Exception as e:
error.append(f'create_portfolio \n{e}')
# get sector information
incoming_error = add_detail_to_stocks(profile_df)
error.extend(incoming_error)
# concate to existing profile if exist
if current_p is not None:
profile_df = pd.concat([profile_df, current_p], ignore_index=True)
profile_df.drop_duplicates(
subset=['ticker', 'date'], keep='last', inplace=True)
profile_df.reset_index(drop=True, inplace=True)
return profile_df, error
@auth_api
def get_all_stocks_detail():
'''get df contain all stock display_name, name, sector, aggregate_sector'''
detail_df = jq.get_all_securities()
detail_df['ticker'] = detail_df.index
detail_df.reset_index(drop=True, inplace=True)
industry_info = jq.get_industry(detail_df.ticker.to_list())
detail_df['sector'] = detail_df.apply(lambda x: " ".join(
value['industry_name']for value in industry_info[x.ticker].values()), axis=1)
detail_df['aggregate_sector'] = detail_df.apply(
lambda x: aggregate_sector(x.sector), axis=1)
return detail_df
@auth_api
def get_api_usage():
return jq.get_query_count()
@auth_api
def get_stocks_price(profile: pd.DataFrame, start_date: datetime, end_date: datetime, frequency='daily') -> tuple[pd.DataFrame, List[str]]:
"""
Return a dataframe contain stock price between period of time for price in a portfolio profile
Arguments:
profile {pd.DataFrame} -- ticker | date | weight | sector | aggregate_sector | display_name | name
start_date {datetime} -- start date of the period include start date
end_date {datetime} -- end date of the period include end date
frequency {str} -- resolution of the price, default is daily
Returns: Tuple(pd.DataFrame, List[str])
pd.DataFrame -- ticker date open close high low volumn money
error_message {list} -- a list of error message
"""
error_message = []
start_str = start_date.strftime('%Y-%m-%d')
end_str = end_date.strftime('%Y-%m-%d')
if profile.date.min() < start_date:
# hanlde benchmark doesn't have weight on the exact date
start_str = profile.date.min().strftime('%Y-%m-%d')
ticker = profile['ticker'].to_list()
try:
data = jq.get_price(ticker, start_date=start_str,
end_date=end_str, frequency=frequency)
data.rename(columns={'time': 'date', 'code': "ticker"}, inplace=True)
return data, error_message
except Exception as e:
error_message.append(f'Error when fetching {ticker} \n {e}')
return None, error_message
@auth_api
def fetch_stocks_price(**params):
'''request list of stock price from start_date to end_date with frequency or count'''
stocks_df = jq.get_price(**params)
stocks_df.rename(columns={'code': 'ticker'}, inplace=True)
return stocks_df
@auth_api
def fetch_benchmark_profile(start_date: datetime, end_date: datetime, delta_time=timedelta(days=30), benchmark="000905.XSHG"):
'''
fetch benchmark profile from start_date to end_date with delta_time
Parameters
----------
start_date : datetime
start date of the period include start date
end_date : datetime
end date of the period include end date
delta_time : timedelta, optional
the default is 30 days since the jq api only update index weight once every month
'''
if end_date < start_date:
raise Exception('end_date must be greater than start_date')
results = []
# handle end_date == start_date
while start_date < end_date:
try:
date_str = start_date.strftime('%Y-%m-%d')
result = jq.get_index_weights(benchmark, date=date_str)
results.append(result)
except Exception as e:
print(f'Error when fetching {benchmark}\n\
update on {date_str} is missing\n\
{e}')
start_date += delta_time
update_df = pd.concat(results)
update_df['ticker'] = update_df.index
update_df['date'] = pd.to_datetime(update_df['date'])
# update_df.rename({'date': 'time'}, inplace=True, axis=1)
# remove duplicate row
update_df = update_df.drop_duplicates(
subset=['ticker', 'date'], keep='last')
update_df.reset_index(drop=True, inplace=True)
return update_df
|