FundPremium_DynamicPosition.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. # 克隆自聚宽文章:https://www.joinquant.com/post/33636
  2. # 标题:etf基金溢价-改进版-高收益低回撤-速度已最优
  3. # 作者:发锅
  4. # 核心改动就是调整了成交量检查标准变为过去10天最小值都要满足条件
  5. # 本策略网址:https://www.joinquant.com/algorithm/live/index?backtestId=73d74ffd00f110ba66a454c11f10de93
  6. # 导入函数库
  7. from jqdata import *
  8. from jqlib.technical_analysis import *
  9. import numpy as np
  10. import pandas as pd
  11. import statsmodels.api as sm
  12. import datetime as dt
  13. import time
  14. # 初始化函数,设定基准等等
  15. def initialize(context):
  16. # start_check_time = time.time()
  17. # 设定沪深300作为基准
  18. set_benchmark('000300.XSHG')
  19. # 开启异步报单
  20. set_option('async_order', True)
  21. # 开启动态复权模式(真实价格)
  22. set_option('use_real_price', True)
  23. # 是否未来函数
  24. set_option("avoid_future_data", True)
  25. # 过滤掉order系列API产生的比error级别低的log
  26. # log.set_level('order', 'error')
  27. # 初始化全局变量
  28. g.loss_limit = 0.9 # 单基金止损比例
  29. g.drop_limit_days = 20 # 止损卖出后多少天不重新买入
  30. g.control_days = 0 # 初始化控制全局止损之后暂停的天数
  31. g.total_limit_days = 30 # 检查全局止损比例的天数范围
  32. g.total_limit_rate = 0.15 # 全局止损比例
  33. g.cool_days = 0 # 全局止损后多少天内不持仓,必须小于g.total_limit_days
  34. g.rate_list = []
  35. g.check_loss_list = []
  36. g.just_sell_list = []
  37. g.total_value_list = []
  38. g.hold_list = []
  39. g.holiday = ['2010-02-12','2010-04-30','2010-09-30','2011-02-01','2011-04-29','2011-09-30','2012-01-20','2012-04-27','2012-09-28','2013-02-08',
  40. '2013-04-26','2013-09-30','2014-01-30','2014-04-30','2014-09-30','2015-02-17','2015-04-30','2015-09-30','2016-02-05','2016-04-29','2016-09-30',
  41. '2017-01-26','2017-04-28','2017-09-29','2018-02-14','2018-04-27','2018-09-28','2019-02-01','2019-04-30','2019-09-30','2020-01-23','2020-04-30',
  42. '2020-09-30','2021-02-10','2021-04-30','2021-09-30','2022-01-28','2022-04-29','2022-09-30','2023-01-20','2023-04-28','2023-09-28','2024-02-09',
  43. '2024-04-30','2024-09-30','2025-01-28','2025-04-30','2025-09-30','2026-02-15','2026-04-30','2026-09-30' ]
  44. set_order_cost(OrderCost(close_tax=0.000, open_commission=0.00025, close_commission=0.00025, min_commission=0), type='fund')
  45. set_slippage(PriceRelatedSlippage(0.002),type='fund')
  46. run_daily(before_market_open, '09:20', reference_security='000300.XSHG')
  47. run_daily(market_open, '09:30', reference_security='000300.XSHG')
  48. run_daily(check_loss_up, time='14:10', reference_security='000300.XSHG')
  49. run_daily(print_position_info, time='15:10', reference_security='000300.XSHG')
  50. # end_check_time = time.time()
  51. # elapsed_time = end_check_time - start_check_time
  52. # print(f"initialize time: {elapsed_time}")
  53. def before_market_open(context):
  54. start_check_time = time.time()
  55. # 获取基金
  56. fund_list = get_all_securities(['lof', 'etf'], context.previous_date).index.tolist()
  57. g.length1 = len(fund_list)
  58. # 过滤太新的基金
  59. fund_list = filter_new_fund(context,fund_list)
  60. # 嘉实元和事件,所以在2019年5月之后不再买入
  61. if context.current_dt.date() >= np.datetime64('2019-05-01') and ('505888.XSHG' in fund_list):
  62. fund_list.remove('505888.XSHG')
  63. print('remove 505888.XSHG')
  64. # 成交额过滤
  65. df = history(count=10, unit='1d', field="money", security_list=fund_list).T # 整体花费时间少于0.16秒
  66. df['min_money_10d'] = df.min(axis=1)
  67. # cur_total_value = context.portfolio.total_value
  68. # money_threshold = 2 * cur_total_value
  69. # print(f"cur_total_value: {cur_total_value}")
  70. money_threshold = 500000 # 固定值
  71. df = df[df['min_money_10d'] > money_threshold] # 因为最大持仓是5种,那么资金就是5等分,那么只要是总金额的2倍,实际就是10倍
  72. # 新的获取净值的方式,不是通过"get_extras"而是通过query和get_all_securities的方法
  73. future_list = df.index.tolist() # 不需要去重,没有重复
  74. temp_future_list = [item[:6] for item in future_list] # 和codes = list(set(temp_future_list))一致,也不存在重复
  75. codes = list(set(temp_future_list))
  76. # 获取当前日期的前10个交易日(包含当前日期)
  77. latest_trade_day = context.previous_date
  78. latest_trade_day = latest_trade_day.strftime('%Y-%m-%d')
  79. # 获取基金净值,目前除了159001以外,用for循环和in_的方式获得的数量一致
  80. # 避免一次性获取数据量太大,进行分割查询
  81. batch_size = 400
  82. all_temp_dfs = []
  83. # 按400个一组分割 temp_future_list 并逐组查询
  84. for i in range(0, len(temp_future_list), batch_size):
  85. batch = temp_future_list[i:i + batch_size]
  86. temp_batch_df = finance.run_query(
  87. query(finance.FUND_NET_VALUE)
  88. .filter(finance.FUND_NET_VALUE.code.in_(batch))
  89. .filter(finance.FUND_NET_VALUE.day==latest_trade_day)
  90. .order_by(finance.FUND_NET_VALUE.day.desc())
  91. )
  92. all_temp_dfs.append(temp_batch_df)
  93. # 合并所有批次的结果
  94. temp_df = pd.concat(all_temp_dfs, ignore_index=True)
  95. # 获取所有基金的完整代码列表,并转为 DataFrame 便于匹配
  96. all_funds = get_all_securities(['fund'])
  97. all_funds = all_funds.reset_index() # 重置索引,使 'index' 列成为 DataFrame 列
  98. all_funds['code_prefix'] = all_funds['index'].str[:6] # 提取前6位数字
  99. # 获取 temp_df 并提取 code 的前6位数字,方便匹配
  100. temp_df['code_prefix'] = temp_df['code'].str[:6]
  101. # 将 temp_df 中的 code_prefix 匹配到 all_funds 中的 code_prefix,以获取完整的 code
  102. merged_df = pd.merge(temp_df, all_funds[['index', 'code_prefix']],
  103. left_on='code_prefix', right_on='code_prefix', how='left')
  104. # 检查是否存在未匹配的行,并打印出来
  105. unmatched_df = merged_df[merged_df['index'].isna()]
  106. if not unmatched_df.empty:
  107. print("Unmatched entries:")
  108. print(unmatched_df[['code', 'code_prefix']])
  109. # 使用完整的 code 作为索引,并重命名列为 'unit_net_value'
  110. df = merged_df.dropna(subset=['index']).set_index('index')[['net_value']].rename(columns={'net_value': 'unit_net_value'})
  111. g.fund_list = df # 基金和净值的df
  112. # print(f"check g.fund_list structure: {type(g.fund_list)}, data: {g.fund_list}")
  113. log.info('开盘前记录净值...')
  114. end_check_time = time.time()
  115. elapsed_time = end_check_time - start_check_time
  116. print(f"before_market_open time: {elapsed_time}")
  117. def market_open(context):
  118. start_check_time = time.time()
  119. df = g.fund_list
  120. length2 = len(df)
  121. current = get_current_data()
  122. fund_list = df.index.tolist()
  123. ## 获得基金最新价
  124. try:
  125. df['last_price'] = [current[c].last_price for c in fund_list]
  126. except Exception as e:
  127. print(f"error: {e}")
  128. # print("df: ")
  129. # print(df)
  130. ## 计算溢价
  131. df['premium'] = (df.last_price / df.unit_net_value - 1) * 100 #最新价格小于净值的小于0
  132. ## 根据溢价大小排序
  133. if hasattr(df, 'sort'): # 如果有sort方法就用sort,没有用sort_values
  134. df = df.sort(['premium'], ascending = True)
  135. else:
  136. df = df.sort_values(['premium'], ascending = True)
  137. df = df[(df.premium < 0)]
  138. special_rate = len(df)/g.length1 # 最新价格低于净值的占比10%以内
  139. g.rate_list.append(special_rate)
  140. g.rate_list = g.rate_list[-10:] # 最近10天的special_rate
  141. while len(g.rate_list) < 10:
  142. g.rate_list.append(g.rate_list[0])
  143. print(f"g.rate_list - length: {len(g.rate_list)}, mean: {mean(g.rate_list)}")
  144. if g.cool_days == 0:
  145. if (len(g.rate_list) == 10) and (mean(g.rate_list) > 0.1): # 比例过低就不执行买入卖出的操作
  146. target_fund_list = df[:30].index.tolist()
  147. target_fund_list = [stock for stock in target_fund_list if stock not in g.just_sell_list]
  148. target_fund_list = target_fund_list[:30]
  149. g.max_position = len(target_fund_list)
  150. # 卖出
  151. for fund in context.portfolio.positions.keys():
  152. # 卖出不在股票池或节假日前清仓
  153. if fund not in target_fund_list or str(context.current_dt.date()) in g.holiday:
  154. order_target_value(fund, 0)
  155. # 买入, 节假日前不开仓
  156. if str(context.current_dt.date()) not in g.holiday:
  157. # for fund in target_fund_list:
  158. # now_position = g.max_position - len(context.portfolio.positions)
  159. # if now_position == 0:
  160. # continue
  161. # if fund not in context.portfolio.positions.keys():
  162. # position = context.portfolio.available_cash / now_position
  163. # order_target_value(fund, position)
  164. # 计算每个标的的10日平均成交额
  165. money_df = history(count=10, unit='1d', field="money", security_list=target_fund_list).T
  166. money_df['avg_money'] = money_df.min(axis=1) # 过去10天成交额的最小值
  167. # 计算单个持仓的标准金额
  168. standard_position = context.portfolio.available_cash / g.max_position
  169. max_position_limit = standard_position * 3 # 单个标的最大持仓限制
  170. print(f"max_position_limit: {max_position_limit}")
  171. for fund in target_fund_list:
  172. if fund not in context.portfolio.positions.keys():
  173. # 计算该标的的目标买入金额
  174. min_buy_amount = money_df.loc[fund, 'avg_money'] / 5 # 最小买入金额
  175. target_amount = min(max_position_limit,
  176. max(min_buy_amount, standard_position))
  177. print(f"{fund} min_buy_amount: {min_buy_amount} and target_amount: {target_amount}")
  178. # 确保不超过可用资金
  179. if target_amount <= context.portfolio.available_cash:
  180. order_target_value(fund, target_amount)
  181. elif (len(g.rate_list) == 10) and (mean(g.rate_list) <= 0.1):
  182. if g.hold_list:
  183. clear_position(context)
  184. else:
  185. g.cool_days -= 1
  186. # 更新持有的基金池
  187. g.hold_list= []
  188. for position in list(context.portfolio.positions.values()):
  189. fund = position.security
  190. g.hold_list.append(fund)
  191. end_check_time = time.time()
  192. elapsed_time = end_check_time - start_check_time
  193. print(f"market_open time: {elapsed_time}")
  194. ## 收盘后运行函数
  195. def after_market_close(context):
  196. pass
  197. # 1-6 调整亏损比例过大的股票
  198. def check_loss_up(context):
  199. start_check_time = time.time()
  200. if g.hold_list:
  201. check_loss_list = []
  202. for stock in g.hold_list:
  203. position = context.portfolio.positions[stock]
  204. price = position.price
  205. avg_cost = position.avg_cost
  206. # print('check %s, price: %2f, avg_cost: %2f' % (stock, price, avg_cost))
  207. if price < g.loss_limit * avg_cost:
  208. log.info("[%s]损失比例过高,卖出" % stock)
  209. close_position(position)
  210. check_loss_list.append(stock)
  211. if check_loss_list:
  212. g.check_loss_list.append(check_loss_list)
  213. else:
  214. g.check_loss_list.append(['nothing'])
  215. if len(g.check_loss_list) > g.drop_limit_days:
  216. g.check_loss_list = g.check_loss_list[-g.drop_limit_days:]
  217. temp_set = set()
  218. for check_loss_list in g.check_loss_list:
  219. temp_set = temp_set.union(set(check_loss_list))
  220. # 不要购买的股票列表,过往20天因为止损而卖出的股票
  221. g.just_sell_list = list(temp_set)
  222. check_total_value(context)
  223. end_check_time = time.time()
  224. elapsed_time = end_check_time - start_check_time
  225. print(f"check_loss_up time: {elapsed_time}")
  226. # 1-7 检查整体资金比例
  227. def check_total_value(context):
  228. start_check_time = time.time()
  229. total_money_today = context.portfolio.total_value
  230. g.total_value_list.append(total_money_today)
  231. print('检查整体资金比例g.total_value_list: ', len(g.total_value_list))
  232. print(g.total_value_list)
  233. if len(g.total_value_list) >= g.total_limit_days:
  234. g.total_value_list = g.total_value_list[-g.total_limit_days:] # 只考虑最近20天的跌幅来判断是否清仓
  235. biggest_pullback = (total_money_today - max(g.total_value_list))/max(g.total_value_list)
  236. print('检查近 %d 的最大损失为 %2f' % (g.total_limit_days, biggest_pullback * 100))
  237. if biggest_pullback < - g.total_limit_rate: # 当跌幅超过最大限制,则清空仓位
  238. clear_position(context)
  239. if g.control_days == 0: # 设定空仓天数
  240. print('清仓后,未修正的g.control_days为: ', g.control_days)
  241. g.control_days = g.cool_days
  242. print('清仓后,修正g.control_days为: ', g.control_days)
  243. print('持仓情况为: ', g.hold_list)
  244. print('判断标准为: ', (not g.hold_list))
  245. if not g.hold_list: # 如果卖光了,那么调整检查全盘资金的数据量,保留10天的数据,因为检查是最近20天,暂停10天
  246. g.total_value_list = g.total_value_list[-(g.total_limit_days-g.cool_days):]
  247. end_check_time = time.time()
  248. elapsed_time = end_check_time - start_check_time
  249. print(f"check_total_value time: {elapsed_time}")
  250. #3-1 交易模块-自定义下单
  251. def order_target_value_(security, value):
  252. if value == 0:
  253. log.debug("Selling out %s" % (security))
  254. else:
  255. log.debug("Order %s to value %f" % (security, value))
  256. return order_target_value(security, value)
  257. #3-3 交易模块-平仓
  258. def close_position(position):
  259. security = position.security
  260. order = order_target_value_(security, 0) # 可能会因停牌失败
  261. if order != None:
  262. if order.status == OrderStatus.held and order.filled == order.amount:
  263. return True
  264. return False
  265. #3-5 交易模块 - 清仓
  266. def clear_position(context):
  267. if context.portfolio.positions:
  268. g.cool_days = 5 # 清仓后5天不进行买入操作
  269. log.info("==> 清仓,卖出所有股票")
  270. for stock in context.portfolio.positions.keys():
  271. position = context.portfolio.positions[stock]
  272. close_position(position)
  273. #2-7 过滤次新股
  274. def filter_new_fund(context,stock_list):
  275. start_check_time = time.time()
  276. yesterday = context.previous_date
  277. end_check_time = time.time()
  278. elapsed_time = end_check_time - start_check_time
  279. print(f"filter_new_fund time: {elapsed_time}")
  280. return [stock for stock in stock_list if not yesterday - get_security_info(stock).start_date < datetime.timedelta(days=5)]
  281. # 清理list里nan的模块
  282. def clean_List_nan(List):
  283. Myarray=np.array(List)
  284. x = float('nan')
  285. for elem in Myarray:
  286. if math.isnan(x):
  287. x = 0.0
  288. return Myarray
  289. #4-1 打印每日持仓信息
  290. def print_position_info(context):
  291. #打印当天成交记录
  292. trades = get_trades()
  293. for _trade in trades.values():
  294. print('成交记录:'+str(_trade))
  295. #打印账户信息
  296. for position in list(context.portfolio.positions.values()):
  297. securities=position.security
  298. cost=position.avg_cost
  299. price=position.price
  300. ret=100*(price/cost-1)
  301. value=position.value
  302. amount=position.total_amount
  303. print('代码:{}'.format(securities))
  304. print('成本价:{}'.format(format(cost,'.2f')))
  305. print('现价:{}'.format(price))
  306. print('收益率:{}%'.format(format(ret,'.2f')))
  307. print('持仓(股):{}'.format(amount))
  308. print('市值:{}'.format(format(value,'.2f')))
  309. print('———————————————————————————————————')
  310. # 5-1 临时检查的一些函数,查获取基金净值的方法有没有漏洞
  311. def check_fund_net_value(result_df, result_temp_df):
  312. # 1. 比较每个 code 的记录数差异
  313. # 按 code 统计每个查询结果的记录数
  314. result_counts_1 = result_df.groupby('code').size()
  315. result_counts_2 = result_temp_df.groupby('code').size()
  316. print(f"result_counts_1: {result_counts_1.shape}, result_counts_2: {result_counts_2.shape}")
  317. # 将 result_counts_1 和 result_counts_2 转换为 DataFrame 并进行 join 对齐
  318. result_counts_df = pd.DataFrame({
  319. 'count_1': result_counts_1,
  320. 'count_2': result_counts_2
  321. }).fillna(0) # 使用 0 填充缺失值
  322. # 找到记录数不一致的 code
  323. mismatched_codes = result_counts_df[result_counts_df['count_1'] != result_counts_df['count_2']].index.tolist()
  324. # print(f"Records mismatch for codes: {mismatched_codes}")
  325. # 找出在两种方法中缺失的 code
  326. missing_in_result_temp_df = list(set(result_counts_1.index) - set(result_counts_2.index))
  327. missing_in_result_df = list(set(result_counts_2.index) - set(result_counts_1.index))
  328. print(f"Codes missing in result_temp_df: {missing_in_result_temp_df}")
  329. print(f"Codes missing in result_df: {missing_in_result_df}")
  330. # 创建一个包含 'code' 和 'day' 的 DataFrame,用于标记存在情况
  331. all_codes_days = pd.DataFrame({
  332. 'code': pd.concat([result_df['code'], result_temp_df['code']]),
  333. 'day': pd.concat([result_df['day'], result_temp_df['day']])
  334. }).drop_duplicates()
  335. # 使用 merge 操作标记 result_df 和 result_temp_df 中的数据存在情况
  336. all_codes_days = all_codes_days.merge(
  337. result_temp_df[['code', 'day']], on=['code', 'day'], how='left', indicator='in_result_temp_df'
  338. ).merge(
  339. result_df[['code', 'day']], on=['code', 'day'], how='left', indicator='in_result_df'
  340. )
  341. # 添加标志列
  342. all_codes_days['in_result_temp_df'] = (all_codes_days['in_result_temp_df'] == 'both').astype(int)
  343. all_codes_days['in_result_df'] = (all_codes_days['in_result_df'] == 'both').astype(int)
  344. # 去掉第3和第4列都为1的数据
  345. filtered_df = all_codes_days[~((all_codes_days['in_result_temp_df'] == 1) & (all_codes_days['in_result_df'] == 1))]
  346. # 打印结果
  347. # print(filtered_df)
  348. for fund in missing_in_result_temp_df:
  349. print(f"check {fund}")
  350. test_df1 = finance.run_query(
  351. query(finance.FUND_NET_VALUE)
  352. .filter(finance.FUND_NET_VALUE.code == fund)
  353. .order_by(finance.FUND_NET_VALUE.day.desc())
  354. )
  355. temp_list2 = [fund]
  356. test_df2 = finance.run_query(query(finance.FUND_NET_VALUE).filter(finance.FUND_NET_VALUE.code.in_(temp_list2)).order_by(finance.FUND_NET_VALUE.day.desc()))
  357. test_df3 = test_df2.groupby('code').head(10).reset_index(drop=True)
  358. print(f"test_df1: {test_df1.shape}")
  359. print(f"test_df3: {test_df3.shape}")
  360. print(f"test_df2: {test_df2.shape}")