在 构建被动收入组合 一文中描述了一个构建被动投资组合的想法,用来追踪A股指数。目前,这个组合已经开始运行了。
项目地址:GitHub
💡注意:该项目不构成任何投资建议,仅用于技术交流。
1号组合 1号组合是一个被动组合,主要关注沪深300、中证500、科创50、北证50、MSCI中国A50指数。
因为还没有找到合适的券商交易API,为便于操作和计算,选取了对应的场外ETF联接基金,这样可以通过在支付宝中设置定投实现买入操作,分别是:
沪深300:易方达沪深300ETF联接C(007339)
中证500:易方达中证500ETF联接C(007029)
科创50:易方达上证科创50联接C(011609)
北证50:易方达北证50成份指数C(017516)
MSCI中国A50:易方达MSCI中国A50互联互通ETF联接C(014533)
交易策略 为方便计算,假设本金10万元,5个指数均等分配,每个指数2万元。再将每个指数的2万元分成100份进行定投。
买入
沪深300和MSCI中国A50每周二、四定投2次,采用涨跌幅策略。
中证500、科创50和北证50每周二、三、四定投3次,采用涨跌幅策略。
卖出 每个交易日检查基金净值:
当投入份额小于25%时,持有收益大于8%,则全部卖出。
当投入份额小于50%时,持有收益大于10%,则全部卖出。
当投入份额小于75%时,持有收益大于12%,则全部卖出。
当投入份额大于75%时,持有收益大于15%,则全部卖出。
数据源
中证指数
蛋卷基金
雪球基金
东方财富
同花顺
中证指数最近又推出了中证A50指数,中证A50指数从各行业龙头上市公司证券中,选取市值最大的50只证券作为指数样本,以反映各行业最具代表性的龙头上市公司证券的整体表现。组合中未来可能会用中证A50指数替代MSCI中国A50。
指数数据 中证指数官网本是最理想的数据源,通过浏览器访问时可以发现数据是动态加载的,并且返回数据是标准json格式:
但实际使用python获取数据时,发现返回的是一个验证页面,无法直接获取数据。数据要素越来越重要,对数据的保护也越来越严格,这是一个趋势。毕竟数据接口是一项收费服务,如果被轻易绕过就不太对了。
在这个过程中想起来之前关注的一个项目:tushare ,它是一个免费的金融数据接口,提供了丰富的数据接口,包括股票、基金、指数等。这个项目是一个很好的数据源,可以用来获取指数数据。但是随着数据的增加,tushare的数据接口也越来越受到限制,需要付费才能使用更多的数据。
基金数据 实际上,被动组合是一个长期策略,不需要获取高频数据,只需要获取每日的净值数据即可。这样就可以使用免费的数据接口了,比如雪球基金、东方财富、同花顺等,这些网站都提供了免费的基金净值数据。数据更新慢一点也没什么影响。最后用东财的静态页面获取基金净值数据。
关键代码:
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 def update_fund_data (fund_code: str , date: str , max_retries: int = 5 ): url = f"https://fund.eastmoney.com/{fund_code} .html" headers = { "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0" } session = requests.Session() for _ in range (max_retries): try : response = session.get(url, headers=headers) response.raise_for_status() break except requests.exceptions.RequestException as e: print (f"Request failed: {e} . Retrying..." ) else : print ("Max retries exceeded" ) return None soup = BeautifulSoup(response.text, "html.parser" ) data_item_02 = soup.find("dl" , {"class" : "dataItem02" }) if data_item_02 is None : print ("Failed to find data item." ) return None data_date = data_item_02.find("dt" ).text.split(" " )[-1 ].strip("()" ) unit_value = data_item_02.find( "span" , {"class" : re.compile (r"ui-font-large ui-color-(red|green) ui-num" )} ).text growth_rate = data_item_02.find_all( "span" , {"class" : re.compile (r"ui-font-middle ui-color-(red|green) ui-num" )} )[0 ].text if data_date != date: print ( f"The date of the fund {fund_code} data is {data_date} , current is {date} ." ) print (f"Failed to update data for {fund_code} {url} ." ) return None new_data = {"date" : date, "unit_value" : unit_value, "growth_rate" : growth_rate} file_path = f"../data/{fund_code} .json" with open (file_path, "r+" , encoding="utf-8" ) as f: try : fund_data = json.load(f) except json.JSONDecodeError: print ("Failed to decode JSON data." ) return None for item in fund_data["market_data" ]: if item["date" ] == date: item.update(new_data) break else : fund_data["market_data" ].append(new_data) f.seek(0 ) json.dump(fund_data, f, indent=4 ) f.truncate() print (f"Data for {fund_code} on {date} has been updated." ) return new_data
最近几天更新出来的数据:
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 { "name" : "\u6613\u65b9\u8fbe\u4e2d\u8bc1500ETF\u8054\u63a5C" , "code" : "007029" , "market_data" : [ { "date" : "2024-02-20" , "unit_value" : "1.1865" , "growth_rate" : "-0.10%" } , { "date" : "2024-02-21" , "unit_value" : "1.1865" , "growth_rate" : "0.00%" } , { "date" : "2024-02-22" , "unit_value" : "1.1942" , "growth_rate" : "0.65%" } , { "date" : "2024-02-23" , "unit_value" : "1.1991" , "growth_rate" : "0.41%" } , { "date" : "2024-02-26" , "unit_value" : "1.1985" , "growth_rate" : "-0.05%" } , { "date" : "2024-02-27" , "unit_value" : "1.2231" , "growth_rate" : "2.05%" } ] }
我在仓库中设置了一个定时任务,每天更新一次数据。后面有时间再给博客增加一个页面,把这个组合数据可视化出来。