Parcourir la source

Add Lib directory with trading strategies

maxfeng il y a 4 mois
Parent
commit
2c0751cbcf

Fichier diff supprimé car celui-ci est trop grand
+ 547 - 0
Lib/Options/analysis_chart.ipynb


+ 410 - 0
Lib/fund/FundPremium_DynamicPosition.py

@@ -0,0 +1,410 @@
+# 克隆自聚宽文章:https://www.joinquant.com/post/33636
+# 标题:etf基金溢价-改进版-高收益低回撤-速度已最优
+# 作者:发锅
+# 核心改动就是调整了成交量检查标准变为过去10天最小值都要满足条件
+
+# 本策略网址:https://www.joinquant.com/algorithm/live/index?backtestId=73d74ffd00f110ba66a454c11f10de93
+
+# 导入函数库
+from jqdata import *
+from jqlib.technical_analysis import *
+import numpy as np
+import pandas as pd
+import statsmodels.api as sm 
+import datetime as dt
+import time
+
+# 初始化函数,设定基准等等
+def initialize(context):
+    # start_check_time = time.time()
+    # 设定沪深300作为基准
+    set_benchmark('000300.XSHG')
+    # 开启异步报单
+    set_option('async_order', True)
+    # 开启动态复权模式(真实价格)
+    set_option('use_real_price', True)
+    # 是否未来函数
+    set_option("avoid_future_data", True)
+    # 过滤掉order系列API产生的比error级别低的log
+    # log.set_level('order', 'error')
+    # 初始化全局变量
+    g.loss_limit = 0.9 # 单基金止损比例
+    g.drop_limit_days = 20 # 止损卖出后多少天不重新买入
+    g.control_days = 0 # 初始化控制全局止损之后暂停的天数
+    g.total_limit_days = 30 # 检查全局止损比例的天数范围
+    g.total_limit_rate = 0.15 # 全局止损比例
+    g.cool_days = 0 # 全局止损后多少天内不持仓,必须小于g.total_limit_days
+    g.rate_list = []
+    g.check_loss_list = []
+    g.just_sell_list = []
+    g.total_value_list = []
+    g.hold_list = []
+    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',
+    '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',
+    '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',
+    '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',
+    '2024-04-30','2024-09-30','2025-01-28','2025-04-30','2025-09-30','2026-02-15','2026-04-30','2026-09-30' ]
+    set_order_cost(OrderCost(close_tax=0.000, open_commission=0.00025, close_commission=0.00025, min_commission=0), type='fund')
+    set_slippage(PriceRelatedSlippage(0.002),type='fund')
+    run_daily(before_market_open, '09:20', reference_security='000300.XSHG')
+    run_daily(market_open, '09:30', reference_security='000300.XSHG')
+    run_daily(check_loss_up, time='14:10', reference_security='000300.XSHG')
+    # run_daily(print_position_info, time='15:10', reference_security='000300.XSHG')
+    # end_check_time = time.time()
+    # elapsed_time = end_check_time - start_check_time
+    # print(f"initialize time: {elapsed_time}")
+
+def before_market_open(context):
+    start_check_time = time.time()
+    # 获取基金
+    fund_list = get_all_securities(['lof', 'etf'], context.previous_date).index.tolist()
+    g.length1 = len(fund_list)
+    # 过滤太新的基金
+    fund_list = filter_new_fund(context,fund_list)
+    # 嘉实元和事件,所以在2019年5月之后不再买入
+    if context.current_dt.date() >= np.datetime64('2019-05-01') and ('505888.XSHG' in fund_list):
+        fund_list.remove('505888.XSHG')
+        print('remove 505888.XSHG')
+    # 成交额过滤
+    df = history(count=10, unit='1d', field="money", security_list=fund_list).T # 整体花费时间少于0.16秒
+
+    df['min_money_10d'] = df.min(axis=1)
+    # cur_total_value = context.portfolio.total_value
+    # money_threshold = 2 * cur_total_value
+    # print(f"cur_total_value: {cur_total_value}")
+    money_threshold = 500000 # 固定值
+    df = df[df['min_money_10d'] > money_threshold] # 因为最大持仓是5种,那么资金就是5等分,那么只要是总金额的2倍,实际就是10倍 
+    
+    # 新的获取净值的方式,不是通过"get_extras"而是通过query和get_all_securities的方法
+    future_list = df.index.tolist() # 不需要去重,没有重复
+    temp_future_list = [item[:6] for item in future_list] # 和codes = list(set(temp_future_list))一致,也不存在重复
+    codes = list(set(temp_future_list))
+    
+    # 获取当前日期的前10个交易日(包含当前日期)
+    latest_trade_day = context.previous_date
+    latest_trade_day = latest_trade_day.strftime('%Y-%m-%d')
+    # 获取基金净值,目前除了159001以外,用for循环和in_的方式获得的数量一致
+    
+    # 避免一次性获取数据量太大,进行分割查询
+    batch_size = 400
+    all_temp_dfs = []
+    
+    # 按400个一组分割 temp_future_list 并逐组查询
+    for i in range(0, len(temp_future_list), batch_size):
+        batch = temp_future_list[i:i + batch_size]
+        temp_batch_df = finance.run_query(
+            query(finance.FUND_NET_VALUE)
+            .filter(finance.FUND_NET_VALUE.code.in_(batch))
+            .filter(finance.FUND_NET_VALUE.day==latest_trade_day)
+            .order_by(finance.FUND_NET_VALUE.day.desc())
+        )
+        all_temp_dfs.append(temp_batch_df)
+    
+    # 合并所有批次的结果
+    temp_df = pd.concat(all_temp_dfs, ignore_index=True)
+    
+    # 获取所有基金的完整代码列表,并转为 DataFrame 便于匹配
+    all_funds = get_all_securities(['fund'])
+    all_funds = all_funds.reset_index()  # 重置索引,使 'index' 列成为 DataFrame 列
+    all_funds['code_prefix'] = all_funds['index'].str[:6]  # 提取前6位数字
+    
+    # 获取 temp_df 并提取 code 的前6位数字,方便匹配
+    temp_df['code_prefix'] = temp_df['code'].str[:6]
+    
+    # 将 temp_df 中的 code_prefix 匹配到 all_funds 中的 code_prefix,以获取完整的 code
+    merged_df = pd.merge(temp_df, all_funds[['index', 'code_prefix']], 
+                         left_on='code_prefix', right_on='code_prefix', how='left')
+                         
+    # 检查是否存在未匹配的行,并打印出来
+    unmatched_df = merged_df[merged_df['index'].isna()]
+    if not unmatched_df.empty:
+        print("Unmatched entries:")
+        print(unmatched_df[['code', 'code_prefix']])
+    
+    # 使用完整的 code 作为索引,并重命名列为 'unit_net_value'
+    df = merged_df.dropna(subset=['index']).set_index('index')[['net_value']].rename(columns={'net_value': 'unit_net_value'})
+    
+    g.fund_list = df # 基金和净值的df
+    # print(f"check g.fund_list structure: {type(g.fund_list)}, data: {g.fund_list}")
+    log.info('开盘前记录净值...')
+    end_check_time = time.time()
+    elapsed_time = end_check_time - start_check_time
+    print(f"before_market_open time: {elapsed_time}")
+
+def market_open(context):
+    start_check_time = time.time()
+    df = g.fund_list
+    length2 = len(df)
+    current = get_current_data()
+    fund_list = df.index.tolist()
+    ## 获得基金最新价
+    try:
+        df['last_price'] = [current[c].last_price for c in fund_list]
+    except Exception as e:
+        print(f"error: {e}")
+        # print("df: ")
+        # print(df)
+    ## 计算溢价
+    df['premium'] = (df.last_price / df.unit_net_value - 1) * 100 #最新价格小于净值的小于0
+    ## 根据溢价大小排序
+    if hasattr(df, 'sort'): # 如果有sort方法就用sort,没有用sort_values
+        df = df.sort(['premium'], ascending = True)
+    else:
+        df = df.sort_values(['premium'], ascending = True)
+    
+    df = df[(df.premium < 0)]
+    special_rate = len(df)/g.length1 # 最新价格低于净值的占比10%以内
+    g.rate_list.append(special_rate)
+    g.rate_list = g.rate_list[-10:] # 最近10天的special_rate
+    while len(g.rate_list) < 10:
+        g.rate_list.append(g.rate_list[0])
+    
+    print(f"g.rate_list - length: {len(g.rate_list)}, mean: {mean(g.rate_list)}")
+    if g.cool_days == 0:
+        if (len(g.rate_list) == 10) and (mean(g.rate_list) > 0.1): # 比例过低就不执行买入卖出的操作
+            target_fund_list = df[:30].index.tolist()
+            target_fund_list = [stock for stock in target_fund_list if stock not in g.just_sell_list]
+            
+            target_fund_list = target_fund_list[:30]
+            g.max_position = len(target_fund_list)
+        
+            # 卖出
+            for fund in context.portfolio.positions.keys():
+                # 卖出不在股票池或节假日前清仓
+                if fund not in target_fund_list or str(context.current_dt.date()) in g.holiday:
+                    order_target_value(fund, 0)
+        
+            # 买入, 节假日前不开仓
+            if str(context.current_dt.date()) not in g.holiday:
+                # for fund in target_fund_list:
+                #     now_position = g.max_position - len(context.portfolio.positions)
+                #     if now_position == 0:
+                #         continue
+                #     if fund not in context.portfolio.positions.keys():
+                #         position = context.portfolio.available_cash / now_position
+                #         order_target_value(fund, position)
+                # 计算每个标的的10日平均成交额
+                money_df = history(count=10, unit='1d', field="money", security_list=target_fund_list).T
+                money_df['avg_money'] = money_df.min(axis=1) # 过去10天成交额的最小值
+                
+                # 计算单个持仓的标准金额
+                standard_position = context.portfolio.available_cash / g.max_position
+                max_position_limit = standard_position * 3  # 单个标的最大持仓限制
+                print(f"max_position_limit: {max_position_limit}")
+                
+                for fund in target_fund_list:
+                    if fund not in context.portfolio.positions.keys():
+                        # 计算该标的的目标买入金额
+                        min_buy_amount = money_df.loc[fund, 'avg_money'] / 5  # 最小买入金额
+                        target_amount = min(max_position_limit, 
+                                          max(min_buy_amount, standard_position))
+                        print(f"{fund} min_buy_amount: {min_buy_amount} and target_amount: {target_amount}")
+                        # 确保不超过可用资金
+                        if target_amount <= context.portfolio.available_cash:
+                            order_target_value(fund, target_amount)
+                
+        elif (len(g.rate_list) == 10) and (mean(g.rate_list) <= 0.1):
+            if g.hold_list:
+                clear_position(context)
+    else:
+        g.cool_days -= 1
+        
+        # 更新持有的基金池
+    g.hold_list= []
+    for position in list(context.portfolio.positions.values()):
+        fund = position.security
+        g.hold_list.append(fund)
+    
+    end_check_time = time.time()
+    elapsed_time = end_check_time - start_check_time
+    print(f"market_open time: {elapsed_time}")
+
+## 收盘后运行函数
+def after_market_close(context):
+    pass
+
+# 1-6 调整亏损比例过大的股票
+def check_loss_up(context):
+    start_check_time = time.time()
+    if g.hold_list:
+        check_loss_list = []
+        for stock in g.hold_list:
+            position = context.portfolio.positions[stock]
+            price = position.price
+            avg_cost = position.avg_cost
+            # print('check %s, price: %2f, avg_cost: %2f' % (stock, price, avg_cost))
+            if price < g.loss_limit * avg_cost:
+                log.info("[%s]损失比例过高,卖出" % stock)
+                close_position(position)
+                check_loss_list.append(stock)
+                
+        if check_loss_list:        
+            g.check_loss_list.append(check_loss_list)
+        else:
+            g.check_loss_list.append(['nothing'])
+        if len(g.check_loss_list) > g.drop_limit_days:
+            g.check_loss_list = g.check_loss_list[-g.drop_limit_days:]
+        
+        temp_set = set()
+        for check_loss_list in g.check_loss_list:
+            temp_set = temp_set.union(set(check_loss_list))
+        # 不要购买的股票列表,过往20天因为止损而卖出的股票
+        g.just_sell_list = list(temp_set)
+        check_total_value(context)
+        
+    end_check_time = time.time()
+    elapsed_time = end_check_time - start_check_time
+    print(f"check_loss_up time: {elapsed_time}")
+
+# 1-7 检查整体资金比例
+def check_total_value(context):
+    start_check_time = time.time()
+    total_money_today = context.portfolio.total_value
+    g.total_value_list.append(total_money_today)
+    print('检查整体资金比例g.total_value_list: ', len(g.total_value_list))
+    print(g.total_value_list)
+    if len(g.total_value_list) >= g.total_limit_days:
+        g.total_value_list = g.total_value_list[-g.total_limit_days:] # 只考虑最近20天的跌幅来判断是否清仓
+        biggest_pullback = (total_money_today - max(g.total_value_list))/max(g.total_value_list)
+        print('检查近 %d 的最大损失为 %2f' % (g.total_limit_days, biggest_pullback * 100))
+        if biggest_pullback < - g.total_limit_rate: # 当跌幅超过最大限制,则清空仓位
+            clear_position(context)
+            if g.control_days == 0: # 设定空仓天数
+                print('清仓后,未修正的g.control_days为: ', g.control_days)
+                g.control_days = g.cool_days
+                print('清仓后,修正g.control_days为: ', g.control_days)
+            print('持仓情况为: ', g.hold_list)
+            print('判断标准为: ', (not g.hold_list))
+            if not g.hold_list: # 如果卖光了,那么调整检查全盘资金的数据量,保留10天的数据,因为检查是最近20天,暂停10天
+                g.total_value_list = g.total_value_list[-(g.total_limit_days-g.cool_days):]
+    
+    end_check_time = time.time()
+    elapsed_time = end_check_time - start_check_time
+    print(f"check_total_value time: {elapsed_time}")
+
+#3-1 交易模块-自定义下单
+def order_target_value_(security, value):
+	if value == 0:
+		log.debug("Selling out %s" % (security))
+	else:
+		log.debug("Order %s to value %f" % (security, value))
+	return order_target_value(security, value)
+
+#3-3 交易模块-平仓
+def close_position(position):
+	security = position.security
+	order = order_target_value_(security, 0)  # 可能会因停牌失败
+	if order != None:
+		if order.status == OrderStatus.held and order.filled == order.amount:
+			return True
+	return False
+
+#3-5 交易模块 - 清仓
+def clear_position(context):
+    if context.portfolio.positions:
+        g.cool_days = 5 # 清仓后5天不进行买入操作
+        log.info("==> 清仓,卖出所有股票")
+        for stock in context.portfolio.positions.keys():
+            position = context.portfolio.positions[stock]
+            close_position(position)
+
+#2-7 过滤次新股
+def filter_new_fund(context,stock_list):
+    start_check_time = time.time()
+    yesterday = context.previous_date
+    end_check_time = time.time()
+    elapsed_time = end_check_time - start_check_time
+    print(f"filter_new_fund time: {elapsed_time}")
+    return [stock for stock in stock_list if not yesterday - get_security_info(stock).start_date < datetime.timedelta(days=5)]
+
+# 清理list里nan的模块	
+def clean_List_nan(List):
+    Myarray=np.array(List)
+    x = float('nan')
+    for elem in Myarray:
+        if math.isnan(x):
+            x = 0.0
+    return Myarray
+    
+#4-1 打印每日持仓信息
+def print_position_info(context):
+    #打印当天成交记录
+    trades = get_trades()
+    for _trade in trades.values():
+        print('成交记录:'+str(_trade))
+    #打印账户信息
+    for position in list(context.portfolio.positions.values()):
+        securities=position.security
+        cost=position.avg_cost
+        price=position.price
+        ret=100*(price/cost-1)
+        value=position.value
+        amount=position.total_amount    
+        print('代码:{}'.format(securities))
+        print('成本价:{}'.format(format(cost,'.2f')))
+        print('现价:{}'.format(price))
+        print('收益率:{}%'.format(format(ret,'.2f')))
+        print('持仓(股):{}'.format(amount))
+        print('市值:{}'.format(format(value,'.2f')))
+        print('———————————————————————————————————')
+        
+# 5-1 临时检查的一些函数,查获取基金净值的方法有没有漏洞
+def check_fund_net_value(result_df, result_temp_df):
+    # 1. 比较每个 code 的记录数差异
+    # 按 code 统计每个查询结果的记录数
+    result_counts_1 = result_df.groupby('code').size()
+    result_counts_2 = result_temp_df.groupby('code').size()
+    print(f"result_counts_1: {result_counts_1.shape}, result_counts_2: {result_counts_2.shape}")
+    
+    # 将 result_counts_1 和 result_counts_2 转换为 DataFrame 并进行 join 对齐
+    result_counts_df = pd.DataFrame({
+        'count_1': result_counts_1,
+        'count_2': result_counts_2
+    }).fillna(0)  # 使用 0 填充缺失值
+    
+    # 找到记录数不一致的 code
+    mismatched_codes = result_counts_df[result_counts_df['count_1'] != result_counts_df['count_2']].index.tolist()
+    # print(f"Records mismatch for codes: {mismatched_codes}")
+    
+    # 找出在两种方法中缺失的 code
+    missing_in_result_temp_df = list(set(result_counts_1.index) - set(result_counts_2.index))
+    missing_in_result_df = list(set(result_counts_2.index) - set(result_counts_1.index))
+    print(f"Codes missing in result_temp_df: {missing_in_result_temp_df}")
+    print(f"Codes missing in result_df: {missing_in_result_df}")
+    
+    # 创建一个包含 'code' 和 'day' 的 DataFrame,用于标记存在情况
+    all_codes_days = pd.DataFrame({
+        'code': pd.concat([result_df['code'], result_temp_df['code']]),
+        'day': pd.concat([result_df['day'], result_temp_df['day']])
+    }).drop_duplicates()
+    
+    # 使用 merge 操作标记 result_df 和 result_temp_df 中的数据存在情况
+    all_codes_days = all_codes_days.merge(
+        result_temp_df[['code', 'day']], on=['code', 'day'], how='left', indicator='in_result_temp_df'
+    ).merge(
+        result_df[['code', 'day']], on=['code', 'day'], how='left', indicator='in_result_df'
+    )
+    
+    # 添加标志列
+    all_codes_days['in_result_temp_df'] = (all_codes_days['in_result_temp_df'] == 'both').astype(int)
+    all_codes_days['in_result_df'] = (all_codes_days['in_result_df'] == 'both').astype(int)
+    
+    # 去掉第3和第4列都为1的数据
+    filtered_df = all_codes_days[~((all_codes_days['in_result_temp_df'] == 1) & (all_codes_days['in_result_df'] == 1))]
+    
+    # 打印结果
+    # print(filtered_df)
+    for fund in missing_in_result_temp_df:
+        print(f"check {fund}")
+        test_df1 = finance.run_query(
+                query(finance.FUND_NET_VALUE)
+                .filter(finance.FUND_NET_VALUE.code == fund)
+                .order_by(finance.FUND_NET_VALUE.day.desc())
+            )
+        temp_list2 = [fund]
+        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()))
+        test_df3 = test_df2.groupby('code').head(10).reset_index(drop=True)
+        print(f"test_df1: {test_df1.shape}")
+        print(f"test_df3: {test_df3.shape}")
+        print(f"test_df2: {test_df2.shape}")
+    

+ 242 - 0
Lib/fund/Fund_premium.py

@@ -0,0 +1,242 @@
+# 克隆自聚宽文章:https://www.joinquant.com/post/33636
+# 标题:etf基金溢价-改进版-高收益低回撤-速度已最优
+# 作者:发锅
+
+# 本策略网址:https://www.joinquant.com/algorithm/live/index?backtestId=18245a9cbba72599cf2ba03ecd17217c
+
+# 导入函数库
+from jqdata import *
+from jqlib.technical_analysis import *
+import numpy as np
+import pandas as pd
+import statsmodels.api as sm 
+import datetime as dt
+
+# 初始化函数,设定基准等等
+def initialize(context):
+    # 设定沪深300作为基准
+    set_benchmark('000300.XSHG')
+    # 开启异步报单
+    set_option('async_order', True)
+    # 开启动态复权模式(真实价格)
+    set_option('use_real_price', True)
+    # 是否未来函数
+    set_option("avoid_future_data", True)
+    # 过滤掉order系列API产生的比error级别低的log
+    # log.set_level('order', 'error')
+    # 初始化全局变量
+    g.loss_limit = 0.9 # 单基金止损比例
+    g.drop_limit_days = 20 # 止损卖出后多少天不重新买入
+    g.control_days = 0 # 初始化控制全局止损之后暂停的天数
+    g.total_limit_days = 30 # 检查全局止损比例的天数范围
+    g.total_limit_rate = 0.15 # 全局止损比例
+    g.cool_days = 0 # 全局止损后多少天内不持仓,必须小于g.total_limit_days
+    g.rate_list = []
+    g.check_loss_list = []
+    g.just_sell_list = []
+    g.total_value_list = []
+    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',
+    '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',
+    '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',
+    '2020-09-30','2021-02-10','2021-04-30','2021-09-30','2022-01-28','2022-04-29','2022-09-30']
+    set_order_cost(OrderCost(close_tax=0.000, open_commission=0.00025, close_commission=0.00025, min_commission=0), type='fund')
+    run_daily(before_market_open, '09:20', reference_security='000300.XSHG')
+    run_daily(market_open, '09:31', reference_security='000300.XSHG')
+    run_daily(check_loss_up, time='14:10', reference_security='000300.XSHG')
+    # run_daily(print_position_info, time='15:10', reference_security='000300.XSHG')
+
+def before_market_open(context):
+    # 获取基金
+    fund_list = get_all_securities(['lof', 'etf'], context.previous_date).index.tolist()
+    g.length1 = len(fund_list)
+    # 过滤太新的基金
+    fund_list = filter_new_fund(context,fund_list)
+    # 嘉实元和事件,所以在2019年5月之后不再买入
+    if context.current_dt.date() >= np.datetime64('2019-05-01') and ('505888.XSHG' in fund_list):
+        fund_list.remove('505888.XSHG')
+        print('remove 505888.XSHG')
+    # 成交额过滤
+    df = history(count=1, unit='1d', field="volume", security_list=fund_list).T
+    df.columns=['volume']
+    cur_total_value = context.portfolio.total_value
+    df = df[df.volume > 2e6] # 
+    # print('after volume check: ', len(df))
+    # 获取净值
+    df = get_extras('unit_net_value', df.index.tolist(), end_date=context.previous_date, df=True, count=1).T
+    df.columns=['unit_net_value']
+    g.fund_list = df # 基金和净值的df
+    log.info('开盘前记录净值...')
+
+def market_open(context):
+    df = g.fund_list
+    length2 = len(df)
+    current = get_current_data()
+    fund_list = df.index.tolist()
+    ## 获得基金最新价
+    df['last_price'] = [current[c].last_price for c in fund_list]
+    ## 计算溢价
+    df['premium'] = (df.last_price / df.unit_net_value - 1) * 100 #最新价格小于净值的小于0
+    ## 根据溢价大小排序
+    if hasattr(df, 'sort'): # 如果有sort方法就用sort,没有用sort_values
+        df = df.sort(['premium'], ascending = True)
+    else:
+        df = df.sort_values(['premium'], ascending = True)
+    
+    df = df[(df.premium < 0)]
+    special_rate = len(df)/g.length1
+    g.rate_list.append(special_rate)
+    g.rate_list = g.rate_list[-10:]
+    while len(g.rate_list) < 10:
+        g.rate_list.append(g.rate_list[0])
+
+    if g.cool_days == 0:
+        if (len(g.rate_list) == 10) and (mean(g.rate_list) > 0.1): # 比例过低就不执行买入卖出的操作
+            target_fund_list = df[:20].index.tolist()
+            target_fund_list = [stock for stock in target_fund_list if stock not in g.just_sell_list]
+            
+            target_fund_list = target_fund_list[:5]
+            g.max_position = len(target_fund_list)
+        
+            # 卖出
+            for fund in context.portfolio.positions.keys():
+                # 卖出不在股票池或节假日前清仓
+                if fund not in target_fund_list or str(context.current_dt.date()) in g.holiday:
+                    order_target_value(fund, 0)
+        
+            # 买入, 节假日前不开仓
+            if str(context.current_dt.date()) not in g.holiday:
+                for fund in target_fund_list:
+                    now_position = g.max_position - len(context.portfolio.positions)
+                    if now_position == 0:
+                        continue
+                    if fund not in context.portfolio.positions.keys():
+                        position = context.portfolio.available_cash / now_position
+                        order_target_value(fund, position)
+                
+        elif (len(g.rate_list) == 10) and (mean(g.rate_list) <= 0.1):
+            if g.hold_list:
+                clear_position(context)
+    else:
+        g.cool_days -= 1
+        
+        # 更新持有的基金池
+    g.hold_list= []
+    for position in list(context.portfolio.positions.values()):
+        fund = position.security
+        g.hold_list.append(fund)
+
+## 收盘后运行函数
+def after_market_close(context):
+    pass
+
+# 1-6 调整亏损比例过大的股票
+def check_loss_up(context):
+    if g.hold_list:
+        check_loss_list = []
+        for stock in g.hold_list:
+            position = context.portfolio.positions[stock]
+            price = position.price
+            avg_cost = position.avg_cost
+            # print('check %s, price: %2f, avg_cost: %2f' % (stock, price, avg_cost))
+            if price < g.loss_limit * avg_cost:
+                log.info("[%s]损失比例过高,卖出" % stock)
+                close_position(position)
+                check_loss_list.append(stock)
+                
+        if check_loss_list:        
+            g.check_loss_list.append(check_loss_list)
+        else:
+            g.check_loss_list.append(['nothing'])
+        if len(g.check_loss_list) > g.drop_limit_days:
+            g.check_loss_list = g.check_loss_list[-g.drop_limit_days:]
+        
+        temp_set = set()
+        for check_loss_list in g.check_loss_list:
+            temp_set = temp_set.union(set(check_loss_list))
+        # 不要购买的股票列表,过往20天因为止损而卖出的股票
+        g.just_sell_list = list(temp_set)
+        check_total_value(context)
+
+# 1-7 检查整体资金比例
+def check_total_value(context):
+    total_money_today = context.portfolio.total_value
+    g.total_value_list.append(total_money_today)
+    print('检查整体资金比例g.total_value_list: ', len(g.total_value_list))
+    print(g.total_value_list)
+    if len(g.total_value_list) >= g.total_limit_days:
+        g.total_value_list = g.total_value_list[-g.total_limit_days:] # 只考虑最近20天的跌幅来判断是否清仓
+        biggest_pullback = (total_money_today - max(g.total_value_list))/max(g.total_value_list)
+        print('检查近 %d 的最大损失为 %2f' % (g.total_limit_days, biggest_pullback * 100))
+        if biggest_pullback < - g.total_limit_rate: # 当跌幅超过最大限制,则清空仓位
+            clear_position(context)
+            if g.control_days == 0: # 设定空仓天数
+                print('清仓后,未修正的g.control_days为: ', g.control_days)
+                g.control_days = g.cool_days
+                print('清仓后,修正g.control_days为: ', g.control_days)
+            print('持仓情况为: ', g.hold_list)
+            print('判断标准为: ', (not g.hold_list))
+            if not g.hold_list: # 如果卖光了,那么调整检查全盘资金的数据量,保留10天的数据,因为检查是最近20天,暂停10天
+                g.total_value_list = g.total_value_list[-(g.total_limit_days-g.cool_days):]
+                
+
+#3-1 交易模块-自定义下单
+def order_target_value_(security, value):
+	if value == 0:
+		log.debug("Selling out %s" % (security))
+	else:
+		log.debug("Order %s to value %f" % (security, value))
+	return order_target_value(security, value)
+
+#3-3 交易模块-平仓
+def close_position(position):
+	security = position.security
+	order = order_target_value_(security, 0)  # 可能会因停牌失败
+	if order != None:
+		if order.status == OrderStatus.held and order.filled == order.amount:
+			return True
+	return False
+
+#3-5 交易模块 - 清仓
+def clear_position(context):
+    if context.portfolio.positions:
+        g.cool_days = 5 # 清仓后5天不进行买入操作
+        log.info("==> 清仓,卖出所有股票")
+        for stock in context.portfolio.positions.keys():
+            position = context.portfolio.positions[stock]
+            close_position(position)
+
+#2-7 过滤次新股
+def filter_new_fund(context,stock_list):
+    yesterday = context.previous_date
+    return [stock for stock in stock_list if not yesterday - get_security_info(stock).start_date < datetime.timedelta(days=5)]
+
+# 清理list里nan的模块	
+def clean_List_nan(List):
+    Myarray=np.array(List)
+    x = float('nan')
+    for elem in Myarray:
+        if math.isnan(x):
+            x = 0.0
+    return Myarray
+    
+#4-1 打印每日持仓信息
+def print_position_info(context):
+    #打印当天成交记录
+    trades = get_trades()
+    for _trade in trades.values():
+        print('成交记录:'+str(_trade))
+    #打印账户信息
+    for position in list(context.portfolio.positions.values()):
+        securities=position.security
+        cost=position.avg_cost
+        price=position.price
+        ret=100*(price/cost-1)
+        value=position.value
+        amount=position.total_amount    
+        print('代码:{}'.format(securities))
+        print('成本价:{}'.format(format(cost,'.2f')))
+        print('现价:{}'.format(price))
+        print('收益率:{}%'.format(format(ret,'.2f')))
+        print('持仓(股):{}'.format(amount))
+        print('市值:{}'.format(format(value,'.2f')))
+        print('———————————————————————————————————')

+ 245 - 0
Lib/fund/Fund_premium_improvement_fast_001.py

@@ -0,0 +1,245 @@
+# 克隆自聚宽文章:https://www.joinquant.com/post/33636
+# 标题:etf基金溢价-改进版-高收益低回撤-速度已最优
+# 作者:发锅
+# 本策略网址:https://www.joinquant.com/algorithm/index/edit?algorithmId=2eb4f4246fdf81a56bf43c681de7278b&backtest=37
+
+# 导入函数库
+from jqdata import *
+from jqlib.technical_analysis import *
+import numpy as np
+import pandas as pd
+import statsmodels.api as sm 
+import datetime as dt
+
+# 初始化函数,设定基准等等
+def initialize(context):
+    # 设定沪深300作为基准
+    set_benchmark('000300.XSHG')
+    # 开启异步报单
+    set_option('async_order', True)
+    # 开启动态复权模式(真实价格)
+    set_option('use_real_price', True)
+    # 是否未来函数
+    set_option("avoid_future_data", True)
+    # 过滤掉order系列API产生的比error级别低的log
+    # log.set_level('order', 'error')
+    # 初始化全局变量
+    g.loss_limit = 0.9 # 单基金止损比例
+    g.drop_limit_days = 20 # 止损卖出后多少天不重新买入
+    g.control_days = 0 # 初始化控制全局止损之后暂停的天数
+    g.total_limit_days = 30 # 检查全局止损比例的天数范围
+    g.total_limit_rate = 0.15 # 全局止损比例
+    g.cool_days = 0 # 全局止损后多少天内不持仓,必须小于g.total_limit_days
+    g.rate_list = []
+    g.check_loss_list = []
+    g.just_sell_list = []
+    g.total_value_list = []
+    g.hold_list = []
+    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',
+    '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',
+    '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',
+    '2020-09-30','2021-02-10','2021-04-30','2021-09-30','2022-01-28','2022-04-29','2022-09-30']
+    set_order_cost(OrderCost(close_tax=0.000, open_commission=0.00025, close_commission=0.00025, min_commission=0), type='fund')
+    run_daily(before_market_open, '09:20', reference_security='000300.XSHG')
+    run_daily(market_open, '14:40', reference_security='000300.XSHG')
+    run_daily(check_loss_up, time='14:45', reference_security='000300.XSHG')
+    # run_daily(print_position_info, time='15:10', reference_security='000300.XSHG')
+
+def before_market_open(context):
+    # 获取基金
+    fund_list = get_all_securities(['lof', 'etf'], context.previous_date).index.tolist()
+    g.length1 = len(fund_list)
+    # 过滤太新的基金
+    fund_list = filter_new_fund(context,fund_list)
+    # 嘉实元和事件,所以在2019年5月之后不再买入
+    if context.current_dt.date() >= np.datetime64('2019-05-01') and ('505888.XSHG' in fund_list):
+        fund_list.remove('505888.XSHG')
+        print('remove 505888.XSHG')
+    # 成交额过滤
+    df = history(count=5, unit='1d', field="volume", security_list=fund_list).T
+    df.columns=['volume1','volume2','volume3','volume4','volume5']
+    df['volume'] = df[['volume1','volume2','volume3','volume4','volume5']].mean(axis=1)
+    print('after volume check: ', df.iloc[:5,:5])
+    df.drop(['volume1','volume2','volume3','volume4','volume5'],inplace=True,axis=1)
+    cur_total_value = context.portfolio.total_value
+    df = df[df.volume > 1e5] # 
+    # print('after volume check: ', df.head())
+    # 获取净值
+    df = get_extras('unit_net_value', df.index.tolist(), end_date=context.previous_date, df=True, count=1).T
+    df.columns=['unit_net_value']
+    g.fund_list = df # 基金和净值的df
+    log.info('开盘前记录净值...')
+
+def market_open(context):
+    df = g.fund_list
+    length2 = len(df)
+    current = get_current_data()
+    fund_list = df.index.tolist()
+    ## 获得基金最新价
+    df['last_price'] = [current[c].last_price for c in fund_list]
+    ## 计算溢价
+    df['premium'] = (df.last_price / df.unit_net_value - 1) * 100 #最新价格小于净值的小于0
+    ## 根据溢价大小排序
+    if hasattr(df, 'sort'): # 如果有sort方法就用sort,没有用sort_values
+        df = df.sort(['premium'], ascending = True)
+    else:
+        df = df.sort_values(['premium'], ascending = True)
+    
+    df = df[(df.premium < 0)]
+    special_rate = len(df)/g.length1
+    g.rate_list.append(special_rate)
+    g.rate_list = g.rate_list[-10:]
+    while len(g.rate_list) < 10:
+        g.rate_list.append(g.rate_list[0])
+
+    if g.cool_days == 0:
+        if (len(g.rate_list) == 10) and (mean(g.rate_list) > 0.1): # 比例过低就不执行买入卖出的操作
+            target_fund_list = df[:20].index.tolist()
+            target_fund_list = [stock for stock in target_fund_list if stock not in g.just_sell_list]
+            
+            target_fund_list = target_fund_list[:5]
+            g.max_position = len(target_fund_list)
+        
+            # 卖出
+            for fund in context.portfolio.positions.keys():
+                # 卖出不在股票池或节假日前清仓
+                if fund not in target_fund_list or str(context.current_dt.date()) in g.holiday:
+                    order_target_value(fund, 0)
+        
+            # 买入, 节假日前不开仓
+            if str(context.current_dt.date()) not in g.holiday:
+                for fund in target_fund_list:
+                    now_position = g.max_position - len(context.portfolio.positions)
+                    if now_position == 0:
+                        continue
+                    if fund not in context.portfolio.positions.keys():
+                        position = context.portfolio.available_cash / now_position
+                        order_target_value(fund, position)
+                
+        elif (len(g.rate_list) == 10) and (mean(g.rate_list) <= 0.1):
+            if g.hold_list:
+                clear_position(context)
+    else:
+        g.cool_days -= 1
+        
+        # 更新持有的基金池
+    g.hold_list= []
+    for position in list(context.portfolio.positions.values()):
+        fund = position.security
+        g.hold_list.append(fund)
+
+## 收盘后运行函数
+def after_market_close(context):
+    pass
+
+# 1-6 调整亏损比例过大的股票
+def check_loss_up(context):
+    if g.hold_list:
+        check_loss_list = []
+        for stock in g.hold_list:
+            position = context.portfolio.positions[stock]
+            price = position.price
+            avg_cost = position.avg_cost
+            # print('check %s, price: %2f, avg_cost: %2f' % (stock, price, avg_cost))
+            if price < g.loss_limit * avg_cost:
+                log.info("[%s]损失比例过高,卖出" % stock)
+                close_position(position)
+                check_loss_list.append(stock)
+                
+        if check_loss_list:        
+            g.check_loss_list.append(check_loss_list)
+        else:
+            g.check_loss_list.append(['nothing'])
+        if len(g.check_loss_list) > g.drop_limit_days:
+            g.check_loss_list = g.check_loss_list[-g.drop_limit_days:]
+        
+        temp_set = set()
+        for check_loss_list in g.check_loss_list:
+            temp_set = temp_set.union(set(check_loss_list))
+        # 不要购买的股票列表,过往20天因为止损而卖出的股票
+        g.just_sell_list = list(temp_set)
+        check_total_value(context)
+
+# 1-7 检查整体资金比例
+def check_total_value(context):
+    total_money_today = context.portfolio.total_value
+    g.total_value_list.append(total_money_today)
+    print('检查整体资金比例g.total_value_list: ', len(g.total_value_list))
+    print(g.total_value_list)
+    if len(g.total_value_list) >= g.total_limit_days:
+        g.total_value_list = g.total_value_list[-g.total_limit_days:] # 只考虑最近20天的跌幅来判断是否清仓
+        biggest_pullback = (total_money_today - max(g.total_value_list))/max(g.total_value_list)
+        print('检查近 %d 的最大损失为 %2f' % (g.total_limit_days, biggest_pullback * 100))
+        if biggest_pullback < - g.total_limit_rate: # 当跌幅超过最大限制,则清空仓位
+            clear_position(context)
+            if g.control_days == 0: # 设定空仓天数
+                print('清仓后,未修正的g.control_days为: ', g.control_days)
+                g.control_days = g.cool_days
+                print('清仓后,修正g.control_days为: ', g.control_days)
+            print('持仓情况为: ', g.hold_list)
+            print('判断标准为: ', (not g.hold_list))
+            if not g.hold_list: # 如果卖光了,那么调整检查全盘资金的数据量,保留10天的数据,因为检查是最近20天,暂停10天
+                g.total_value_list = g.total_value_list[-(g.total_limit_days-g.cool_days):]
+                
+
+#3-1 交易模块-自定义下单
+def order_target_value_(security, value):
+	if value == 0:
+		log.debug("Selling out %s" % (security))
+	else:
+		log.debug("Order %s to value %f" % (security, value))
+	return order_target_value(security, value)
+
+#3-3 交易模块-平仓
+def close_position(position):
+	security = position.security
+	order = order_target_value_(security, 0)  # 可能会因停牌失败
+	if order != None:
+		if order.status == OrderStatus.held and order.filled == order.amount:
+			return True
+	return False
+
+#3-5 交易模块 - 清仓
+def clear_position(context):
+    if context.portfolio.positions:
+        g.cool_days = 5 # 清仓后5天不进行买入操作
+        log.info("==> 清仓,卖出所有股票")
+        for stock in context.portfolio.positions.keys():
+            position = context.portfolio.positions[stock]
+            close_position(position)
+
+#2-7 过滤次新股
+def filter_new_fund(context,stock_list):
+    yesterday = context.previous_date
+    return [stock for stock in stock_list if not yesterday - get_security_info(stock).start_date < datetime.timedelta(days=5)]
+
+# 清理list里nan的模块	
+def clean_List_nan(List):
+    Myarray=np.array(List)
+    x = float('nan')
+    for elem in Myarray:
+        if math.isnan(x):
+            x = 0.0
+    return Myarray
+    
+#4-1 打印每日持仓信息
+def print_position_info(context):
+    #打印当天成交记录
+    trades = get_trades()
+    for _trade in trades.values():
+        print('成交记录:'+str(_trade))
+    #打印账户信息
+    for position in list(context.portfolio.positions.values()):
+        securities=position.security
+        cost=position.avg_cost
+        price=position.price
+        ret=100*(price/cost-1)
+        value=position.value
+        amount=position.total_amount    
+        print('代码:{}'.format(securities))
+        print('成本价:{}'.format(format(cost,'.2f')))
+        print('现价:{}'.format(price))
+        print('收益率:{}%'.format(format(ret,'.2f')))
+        print('持仓(股):{}'.format(amount))
+        print('市值:{}'.format(format(value,'.2f')))
+        print('———————————————————————————————————')

+ 98 - 0
Lib/fund/QDII

@@ -0,0 +1,98 @@
+# 克隆自聚宽文章:https://www.joinquant.com/post/52938
+# 标题:QDII ETF/LOF 折溢价策略
+# 作者:Gyro^.^
+# 实际地址:https://www.joinquant.com/algorithm/index/edit?algorithmId=9608e6e01e60c8b9c1e0d8521b10882d
+
+import pandas as pd
+import datetime as dt
+from jqdata import *
+
+def initialize(context):
+    # setting system
+    log.set_level('order', 'error')
+    set_option('use_real_price', True)
+    set_option('avoid_future_data', True)
+    # setting strategy
+    run_daily(iUpdate, 'before_open')
+    run_daily(iTrader, 'every_bar')
+    run_daily(iReport, 'after_close')
+
+def iUpdate(context):
+    # parameters
+    keys = ['标普', '美国', '纳指', '纳斯达克', '法国', '德国', \
+            '日经', '亚太', '东南亚', '印度', '沙特',]
+    black_list = ['562060.XSHG']
+    # all funds
+    dt_last = context.previous_date
+    all_fund = get_all_securities('fund', dt_last)
+    # onlist 1-month
+    dt_1m = dt_last - dt.timedelta(days=30)
+    funds = all_fund[all_fund.start_date < dt_1m].index.tolist()
+    # filter, liquity
+    hm = history(20, '1d', 'money', funds).mean()
+    funds = hm[hm > 1e6].index.tolist()
+    # key funds
+    funds = [s for k in keys for s in funds if k in all_fund.display_name[s]]
+    # black_list
+    funds = [s for s in funds if s not in black_list]
+    # net value
+    value = get_extras('unit_net_value', funds, end_date=dt_last, count=2).iloc[0]
+    # price
+    price = history(1, '1d', 'close', funds).iloc[0]
+    # low price
+    r = price/value - 1.0
+    r = r[r < 0.15].sort_values()
+    # choice funds
+    g.ratio = r
+    log.info('funds', len(g.ratio))
+
+def iTrader(context):
+    # load data
+    r = g.ratio
+    position_size = 1.0/max(3, len(g.ratio)) * context.portfolio.total_value
+    lm_value = 0.8*position_size
+    hm_value = 1.3*position_size
+    cash_size = 0.1 * context.portfolio.total_value
+    cdata = get_current_data()
+    # sell
+    for s in context.portfolio.positions:
+        if cdata[s].paused:
+            continue
+        if s not in r.index:
+            log.info('sell', s, cdata[s].name)
+            order_target(s, 0, MarketOrderStyle(0.99*cdata[s].last_price))
+    # buy stocks
+    for s in r.index:
+        if context.portfolio.available_cash < cash_size:
+            break
+        if cdata[s].paused:
+            continue
+        if s not in context.portfolio.positions:
+            if r[s] < 0:
+                log.info('buy', s, cdata[s].name)
+                order_target_value(s, position_size, MarketOrderStyle(1.01*cdata[s].last_price))
+        elif context.portfolio.positions[s].value < lm_value:
+            if r[s] < 0:
+                log.info('balance+', s, cdata[s].name)
+                order_target_value(s, position_size, MarketOrderStyle(1.01*cdata[s].last_price))
+        elif context.portfolio.positions[s].value > hm_value:
+            if r[s] > 0:
+                log.info('balance-', s, cdata[s].name)
+                order_target_value(s, position_size, MarketOrderStyle(0.99*cdata[s].last_price))
+
+def iReport(context):
+    # load data
+    cdata = get_current_data()
+    tvalue = context.portfolio.total_value
+    # table of positions
+    ptable = pd.DataFrame(columns=['amount', 'value', 'weight', 'name'])
+    for s in context.portfolio.positions:
+        ps = context.portfolio.positions[s]
+        ptable.loc[s] = [ps.total_amount, int(ps.value), 100*ps.value/tvalue, cdata[s].name]
+    ptable = ptable.sort_values(by='weight', ascending=False)
+    # daily report
+    pd.set_option('display.max_rows', None)
+    log.info('  positions', len(ptable), '\n', ptable.head(10))
+    log.info('  total value %.2f, cash %.2f', \
+            context.portfolio.total_value/10000, context.portfolio.available_cash/10000)
+# end

+ 213 - 0
Lib/fund/README.md

@@ -0,0 +1,213 @@
+# 基金投资策略说明
+
+## ETF基金溢价策略 (FundPremium_DynamicPosition)
+
+### 核心思路
+该策略基于ETF基金的溢价/折价现象进行交易,主要关注折价(当前价格低于净值)的基金进行投资。通过动态调整持仓和多重风控机制,在把握折价机会的同时控制风险。
+
+1. [策略网址](https://www.joinquant.com/algorithm/live/index?backtestId=73d74ffd00f110ba66a454c11f10de93)
+2. 策略名称:基金溢价-动态仓位
+
+### 主要特点
+1. 动态仓位管理
+2. 多重风控机制
+3. 基于成交量的流动性筛选
+4. 节假日风险规避
+5. 市场状态自适应
+
+### 具体策略逻辑
+
+#### 1. 筛选标准
+- 从所有ETF和LOF基金中进行筛选
+- 排除上市时间少于5天的新基金
+- 要求过去10天的最小日成交额大于 **money_threshold** (当前是50万元)
+- 特殊处理:2019年5月后不再买入嘉实元和事件基金(505888.XSHG)
+
+#### 2. 买入条件
+##### 2.1 折价率计算与筛选
+- 折价率 = (最新价格 / 单位净值 - 1) × 100%
+- 当折价率 < 0 时,表示基金价格低于净值
+- 策略仅买入折价的基金(折价率 < 0)
+- 按折价率从小到大排序(即折价幅度从大到小)
+- 选择折价率最低的前30只基金作为候选池
+
+##### 2.2 持仓规则
+- 从候选池中选择基金,持仓数量由 **g.max_position** 动态决定
+- 另外 **g.max_position** 则是由筛选出来的前30折价基金数量决定的,也就是最多是30
+- 当前设置为从前30只折价基金中选择
+- 排除最近 **g.drop_limit_days** (当前是20天)内因止损卖出的基金
+
+##### 2.3 仓位控制
+- 标准仓位计算:
+  - 基础标准仓位 = 可用资金 / **g.max_position**
+  - 单个标的最大持仓限制 = 基础标准仓位 * 3
+- 实际买入金额计算:
+  - 最小买入金额 = 该标的10日平均成交额 / 10
+  - 实际买入金额 = min(单个标的最大持仓限制, max(最小买入金额, 基础标准仓位))
+  - 确保不超过当前可用资金
+
+#### 3. 卖出条件
+
+##### 3.1 日常调仓卖出
+- 当基金不再满足选股条件时卖出(不在前30名折价基金中)
+- 当基金从折价变为溢价时卖出
+
+##### 3.2 单基金止损
+- 当单只基金亏损超过 **g.loss_limit** (当前是10%)时触发止损
+- 止损后 **g.drop_limit_days** (当前是20天)内不再买入该基金
+
+##### 3.3 整体仓位控制
+- 监控最近 **g.total_limit_days** (当前是30天)的最大回撤
+- 当整体回撤超过 **g.total_limit_rate** (当前是15%)时清仓
+- 清仓后设置 **g.control_days** = **g.cool_days** (当前是0)进行冷却期控制
+
+##### 3.4 市场状态监控
+- 跟踪市场中折价基金的占比
+- 维护最近10天的折价基金占比列表 **g.rate_list**
+- 当 **g.rate_list** 的平均值低于0.1(10%)时,清仓观望
+- 等待市场状态改善后再进行操作
+
+##### 3.5 节假日风险规避
+- 在节假日前自动清仓
+- 节假日前不开新仓
+- 节假日列表通过 **g.holiday** 维护
+
+### 交易规则
+- 采用异步报单模式
+- 使用真实价格(动态复权)
+- 考虑交易成本:
+  - 买入手续费:0.025%
+  - 卖出手续费:0.025%
+  - 滑点设置:0.2%
+
+---
+
+## ETF基金溢价策略 (Fund_premium)
+
+### 核心思路
+该策略基于ETF基金的溢价/折价现象进行交易,主要关注折价(当前价格低于净值)的基金进行投资。通过固定持仓数量和多重风控机制,在把握折价机会的同时控制风险。
+
+1. [策略网址](https://www.joinquant.com/algorithm/live/index?backtestId=18245a9cbba72599cf2ba03ecd17217c)
+2. 策略名称:基金溢价-固定持仓
+
+### 主要特点
+1. 固定持仓数量(5只基金)
+2. 多重风控机制
+3. 基于成交量的流动性筛选
+4. 节假日风险规避
+5. 市场状态自适应
+
+### 具体策略逻辑
+
+#### 1. 筛选标准
+- 从所有ETF和LOF基金中进行筛选
+- 排除上市时间少于5天的新基金
+- 要求日成交量大于200万股
+- 特殊处理:2019年5月后不再买入嘉实元和事件基金(505888.XSHG)
+
+#### 2. 买入条件
+##### 2.1 折价率计算与筛选
+- 折价率 = (最新价格 / 单位净值 - 1) × 100%
+- 当折价率 < 0 时,表示基金价格低于净值
+- 策略仅买入折价的基金(折价率 < 0)
+- 按折价率从小到大排序(即折价幅度从大到小)
+- 选择折价率最低的前20只基金作为候选池
+
+##### 2.2 持仓规则
+- 从候选池中选择前5只基金,持仓数量由 **g.max_position** (固定为5)控制
+- 排除最近 **g.drop_limit_days** (当前是20天)内因止损卖出的基金
+
+##### 2.3 仓位控制
+- 等权重分配资金:
+  - 单个持仓金额 = 可用资金 / **g.max_position**
+  - 确保不超过当前可用资金
+
+#### 3. 卖出条件
+
+##### 3.1 日常调仓卖出
+- 当基金不再满足选股条件时卖出(不在前5名折价基金中)
+- 当基金从折价变为溢价时卖出
+
+##### 3.2 单基金止损
+- 当单只基金亏损超过 **g.loss_limit** (当前是10%)时触发止损
+- 止损后 **g.drop_limit_days** (当前是20天)内不再买入该基金
+
+##### 3.3 整体仓位控制
+- 监控最近 **g.total_limit_days** (当前是30天)的最大回撤
+- 当整体回撤超过 **g.total_limit_rate** (当前是15%)时清仓
+- 清仓后设置 **g.control_days** = **g.cool_days** (当前是0)进行冷却期控制
+
+##### 3.4 市场状态监控
+- 跟踪市场中折价基金的占比
+- 维护最近10天的折价基金占比列表 **g.rate_list**
+- 当 **g.rate_list** 的平均值低于0.1(10%)时,清仓观望
+- 等待市场状态改善后再进行操作
+
+##### 3.5 节假日风险规避
+- 在节假日前自动清仓
+- 节假日前不开新仓
+- 节假日列表通过 **g.holiday** 维护
+
+### 交易规则
+- 采用异步报单模式
+- 使用真实价格(动态复权)
+- 考虑交易成本:
+  - 买入手续费:0.025%
+  - 卖出手续费:0.025%
+  - 滑点设置:未设置
+
+---
+
+# 策略描述撰写标准
+
+## 基本结构
+
+### 1. 策略概述
+- 策略名称(遵循代码名称,简短且能反映核心特点)
+- 策略网址(如有)
+- 核心思路(一段话概括策略的主要思想)
+- 主要特点(列举3-5个关键特点)
+
+### 2. 具体策略逻辑
+应包含以下主要部分:
+
+#### 2.1 筛选标准
+- 列举选择标的的基本条件
+- 说明特殊处理规则(如有)
+- 标注关键参数及其变量名(使用粗体)
+
+#### 2.2 买入条件
+- 分类说明买入的触发条件
+- 说明持仓规则
+- 说明仓位控制方法
+- 标注关键参数及其变量名(使用粗体)
+
+#### 2.3 卖出条件
+- 分类说明所有卖出情况
+- 包括止盈、止损、调仓等规则
+- 说明风控相关的卖出条件
+- 标注关键参数及其变量名(使用粗体)
+
+### 3. 交易规则
+- 说明交易执行相关的设置
+- 包含交易成本、滑点等具体数值
+
+## 撰写要求
+
+1. 核心要求
+- 仔细阅读代码文件,理解代码逻辑,不要臆想
+
+2. 参数说明
+- 使用粗体标注关键变量名:如 **g.example_var**
+- 在变量名后注明当前值:如 **g.loss_limit** (当前是10%)
+
+3. 逻辑说明
+- 使用清晰的层级结构
+- 相关的规则要分组说明
+- 对于复杂的计算要分步骤说明
+
+4. 格式要求
+- 使用Markdown格式
+- 保持统一的缩进和列表样式
+- 使用标题区分不同层级的内容
+

+ 286 - 0
Lib/fund/etf_MomentumRotation.py

@@ -0,0 +1,286 @@
+# 克隆自聚宽文章:https://www.joinquant.com/post/34314
+# 标题:8年10倍,回撤小,有滑点!ETF动量简单轮动策略!
+# 作者:萌新王富贵
+
+'''
+优化说明:
+    1.使用修正标准分
+        rsrs_score的算法有:
+            仅斜率slope,效果一般;
+            仅标准分zscore,效果不错;
+            修正标准分 = zscore * r2,效果最佳;
+            右偏标准分 = 修正标准分 * slope,效果不错。
+    2.将原策略的每次持有两只etf改成只买最优的一个,收益显著提高
+    3.将每周调仓换成每日调仓,收益显著提高
+    4.因为交易etf,所以手续费设为万分之三,印花税设为零,未设置滑点
+    5.修改股票池中候选etf,删除银行,红利等收益较弱品种,增加纳指etf以增加不同国家市场间轮动的可能性
+    6.根据研报,默认参数介已设定为最优
+    7.加入防未来函数
+    8.增加择时与选股模块的打印日志,方便观察每笔操作依据
+'''
+
+#导入函数库
+from jqdata import *
+import numpy as np
+
+#初始化函数 
+def initialize(context):
+    # 设定沪深300作为基准
+    set_benchmark('000300.XSHG')
+    # 用真实价格交易
+    set_option('use_real_price', True)
+    # 打开防未来函数
+    set_option("avoid_future_data", True)
+    # 将滑点设置为0.001
+    set_slippage(FixedSlippage(0.001))
+    # 设置交易成本万分之三
+    set_order_cost(OrderCost(open_tax=0, close_tax=0, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5),
+                   type='fund')
+    # 过滤order中低于error级别的日志
+    log.set_level('order', 'error')
+    # 初始化各类全局变量
+    #股票池
+    g.stock_pool = [
+        # '510050.XSHG', #上证50
+        '159915.XSHE', #创业板
+        # '513100.XSHG', #纳指
+        # '159928.XSHE', #中证消费
+        '510300.XSHG', # 沪深300ETF
+        '510500.XSHG', # 中证500ETF
+        
+    ]
+    #动量轮动参数
+    g.stock_num = 1 #买入评分最高的前stock_num只股票
+    g.momentum_day = 29 #最新动量参考最近momentum_day的
+    #rsrs择时参数
+    g.ref_stock = '000300.XSHG' #用ref_stock做择时计算的基础数据
+    g.N = 18 # 计算最新斜率slope,拟合度r2参考最近N天
+    g.M = 600 # 计算最新标准分zscore,rsrs_score参考最近M天
+    g.score_threshold = 0.7 # rsrs标准分指标阈值
+    #ma择时参数
+    g.mean_day = 20 #计算结束ma收盘价,参考最近mean_day
+    g.mean_diff_day = 3 #计算初始ma收盘价,参考(mean_day + mean_diff_day)天前,窗口为mean_diff_day的一段时间
+    g.slope_series = initial_slope_series()[:-1] # 除去回测第一天的slope,避免运行时重复加入
+    # 设置交易时间,每天运行
+    run_daily(my_trade, time='11:30', reference_security='000300.XSHG')
+    run_daily(check_lose, time='open', reference_security='000300.XSHG')
+    run_daily(print_trade_info, time='15:30', reference_security='000300.XSHG')
+    
+#1-1 选股模块-动量因子轮动 
+#基于股票年化收益和判定系数打分,并按照分数从大到小排名
+def get_rank(stock_pool):
+    score_list = []
+    for stock in g.stock_pool:
+        data = attribute_history(stock, g.momentum_day, '1d', ['close']) #过去29天的收盘价
+        y = data['log'] = np.log(data.close)
+        x = data['num'] = np.arange(data.log.size)
+        slope, intercept = np.polyfit(x, y, 1)
+        annualized_returns = math.pow(math.exp(slope), 250) - 1
+        r_squared = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1)))
+        score = annualized_returns * r_squared
+        score_list.append(score)
+    stock_dict=dict(zip(g.stock_pool, score_list))
+    sort_list=sorted(stock_dict.items(), key=lambda item:item[1], reverse=True) #True为降序
+    code_list=[]
+    for i in range((len(g.stock_pool))):
+        code_list.append(sort_list[i][0])
+    rank_stock = code_list[0:g.stock_num]
+    print(code_list[0:5])
+    return rank_stock
+
+
+
+#2-1 择时模块-计算线性回归统计值
+#对输入的自变量每日最低价x(series)和因变量每日最高价y(series)建立OLS回归模型,返回元组(截距,斜率,拟合度)
+def get_ols(x, y):
+    slope, intercept = np.polyfit(x, y, 1)
+    r2 = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1)))
+    return (intercept, slope, r2)
+
+#2-2 择时模块-设定初始斜率序列
+#通过前M日最高最低价的线性回归计算初始的斜率,返回斜率的列表
+def initial_slope_series():
+    data = attribute_history(g.ref_stock, g.N + g.M, '1d', ['high', 'low'])
+    return [get_ols(data.low[i:i+g.N], data.high[i:i+g.N])[1] for i in range(g.M)]
+
+#2-3 择时模块-计算标准分
+#通过斜率列表计算并返回截至回测结束日的最新标准分
+def get_zscore(slope_series):
+    mean = np.mean(slope_series)
+    std = np.std(slope_series)
+    return (slope_series[-1] - mean) / std
+
+#2-4 择时模块-计算综合信号
+#1.获得rsrs与MA信号,rsrs信号算法参考优化说明,MA信号为一段时间两个端点的MA数值比较大小
+#2.信号同时为True时返回买入信号,同为False时返回卖出信号,其余情况返回持仓不变信号
+def get_timing_signal(stock):
+    #计算MA信号
+    close_data = attribute_history(g.ref_stock, g.mean_day + g.mean_diff_day, '1d', ['close'])
+    today_MA = close_data.close[g.mean_diff_day:].mean() 
+    before_MA = close_data.close[:-g.mean_diff_day].mean()
+    #计算rsrs信号
+    high_low_data = attribute_history(g.ref_stock, g.N, '1d', ['high', 'low'])
+    intercept, slope, r2 = get_ols(high_low_data.low, high_low_data.high)
+    g.slope_series.append(slope)
+    rsrs_score = get_zscore(g.slope_series[-g.M:]) * r2
+    #综合判断所有信号
+    if rsrs_score > g.score_threshold and today_MA > before_MA:
+        print('BUY')
+        return "BUY"
+    elif rsrs_score < -g.score_threshold and today_MA < before_MA:
+        print('SELL')
+        return "SELL"
+    else:
+        print('KEEP')
+        return "KEEP"
+
+
+
+#3-1 过滤模块-过滤停牌股票
+#输入选股列表,返回剔除停牌股票后的列表
+def filter_paused_stock(stock_list):
+	current_data = get_current_data()
+	return [stock for stock in stock_list if not current_data[stock].paused]
+
+#3-2 过滤模块-过滤ST及其他具有退市标签的股票
+#输入选股列表,返回剔除ST及其他具有退市标签股票后的列表
+def filter_st_stock(stock_list):
+	current_data = get_current_data()
+	return [stock for stock in stock_list
+			if not current_data[stock].is_st
+			and 'ST' not in current_data[stock].name
+			and '*' not in current_data[stock].name
+			and '退' not in current_data[stock].name]
+
+#3-3 过滤模块-过滤涨停的股票
+#输入选股列表,返回剔除未持有且已涨停股票后的列表
+def filter_limitup_stock(context, stock_list):
+	last_prices = history(1, unit='1m', field='close', security_list=stock_list)
+	current_data = get_current_data()
+	# 已存在于持仓的股票即使涨停也不过滤,避免此股票再次可买,但因被过滤而导致选择别的股票
+	return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
+			or last_prices[stock][-1] < current_data[stock].high_limit]
+
+#3-4 过滤模块-过滤跌停的股票
+#输入股票列表,返回剔除已跌停股票后的列表
+def filter_limitdown_stock(context, stock_list):
+	last_prices = history(1, unit='1m', field='close', security_list=stock_list)
+	current_data = get_current_data()
+	return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
+			or last_prices[stock][-1] > current_data[stock].low_limit]
+
+
+
+#4-1 交易模块-自定义下单
+#报单成功返回报单(不代表一定会成交),否则返回None,应用于
+def order_target_value_(security, value):
+	if value == 0:
+		log.debug("Selling out %s" % (security))
+	else:
+		log.debug("Order %s to value %f" % (security, value))
+	# 如果股票停牌,创建报单会失败,order_target_value 返回None
+	# 如果股票涨跌停,创建报单会成功,order_target_value 返回Order,但是报单会取消
+	# 部成部撤的报单,聚宽状态是已撤,此时成交量>0,可通过成交量判断是否有成交
+	return order_target_value(security, value)
+
+#4-2 交易模块-开仓
+#买入指定价值的证券,报单成功并成交(包括全部成交或部分成交,此时成交量大于0)返回True,报单失败或者报单成功但被取消(此时成交量等于0),返回False
+def open_position(security, value):
+	order = order_target_value_(security, value)
+	if order != None and order.filled > 0:
+		return True
+	return False
+
+#4-3 交易模块-平仓
+#卖出指定持仓,报单成功并全部成交返回True,报单失败或者报单成功但被取消(此时成交量等于0),或者报单非全部成交,返回False
+def close_position(position):
+	security = position.security
+	order = order_target_value_(security, 0)  # 可能会因停牌失败
+	if order != None:
+		if order.status == OrderStatus.held and order.filled == order.amount:
+			return True
+	return False
+
+#4-4 交易模块-调仓
+#当择时信号为买入时开始调仓,输入过滤模块处理后的股票列表,执行交易模块中的开平仓操作
+def adjust_position(context, buy_stocks):
+	for stock in context.portfolio.positions:
+		if stock not in buy_stocks:
+			log.info("[%s]已不在应买入列表中" % (stock))
+			position = context.portfolio.positions[stock]
+			close_position(position)
+		else:
+			log.info("[%s]已经持有无需重复买入" % (stock))
+	# 根据股票数量分仓
+	# 此处只根据可用金额平均分配购买,不能保证每个仓位平均分配
+	position_count = len(context.portfolio.positions)
+	if g.stock_num > position_count:
+		value = context.portfolio.cash / (g.stock_num - position_count)
+		for stock in buy_stocks:
+			if context.portfolio.positions[stock].total_amount == 0:
+				if open_position(stock, value):
+					if len(context.portfolio.positions) == g.stock_num:
+						break
+
+#4-5 交易模块-择时交易
+#结合择时模块综合信号进行交易
+def my_trade(context):
+    #获取选股列表并过滤掉:st,st*,退市,涨停,跌停,停牌
+    check_out_list = get_rank(g.stock_pool)
+    check_out_list = filter_st_stock(check_out_list)
+    check_out_list = filter_limitup_stock(context, check_out_list)
+    check_out_list = filter_limitdown_stock(context, check_out_list)
+    check_out_list = filter_paused_stock(check_out_list)
+    print('今日自选股:{}'.format(check_out_list))
+    #获取综合择时信号
+    timing_signal = get_timing_signal(g.ref_stock)
+    print('今日择时信号:{}'.format(timing_signal))
+    #开始交易
+    if timing_signal == 'SELL':
+        for stock in context.portfolio.positions:
+            position = context.portfolio.positions[stock]
+            close_position(position)
+    elif timing_signal == 'BUY' or timing_signal == 'KEEP':
+        adjust_position(context, check_out_list)
+    else:
+        pass
+
+#4-6 交易模块-止损
+#检查持仓并进行必要的止损操作
+def check_lose(context):
+    for position in list(context.portfolio.positions.values()):
+        securities=position.security
+        cost=position.avg_cost
+        price=position.price
+        ret=100*(price/cost-1)
+        value=position.value
+        amount=position.total_amount
+        #这里设定80%止损几乎等同不止损,因为止损在指数etf策略中影响不大
+        if ret <=-80:
+            order_target_value(position.security, 0)
+            print("!!!!!!触发止损信号: 标的={},标的价值={},浮动盈亏={}% !!!!!!"
+                .format(securities,format(value,'.2f'),format(ret,'.2f')))
+
+#5-1 复盘模块-打印
+#打印每日持仓信息
+def print_trade_info(context):
+    #打印当天成交记录
+    trades = get_trades()
+    for _trade in trades.values():
+        print('成交记录:'+str(_trade))
+    #打印账户信息
+    for position in list(context.portfolio.positions.values()):
+        securities=position.security
+        cost=position.avg_cost
+        price=position.price
+        ret=100*(price/cost-1)
+        value=position.value
+        amount=position.total_amount    
+        print('代码:{}'.format(securities))
+        print('成本价:{}'.format(format(cost,'.2f')))
+        print('现价:{}'.format(price))
+        print('收益率:{}%'.format(format(ret,'.2f')))
+        print('持仓(股):{}'.format(amount))
+        print('市值:{}'.format(format(value,'.2f')))
+    print('一天结束')
+    print('———————————————————————————————————————分割线————————————————————————————————————————')

+ 307 - 0
Lib/fund/etf_MomentumRotation_test.py

@@ -0,0 +1,307 @@
+# 克隆自聚宽文章:https://www.joinquant.com/post/34314
+# 标题:8年10倍,回撤小,有滑点!ETF动量简单轮动策略!
+# 作者:萌新王富贵
+
+
+'''
+优化说明:
+    1.使用修正标准分
+        rsrs_score的算法有:
+            仅斜率slope,效果一般;
+            仅标准分zscore,效果不错;
+            修正标准分 = zscore * r2,效果最佳;
+            右偏标准分 = 修正标准分 * slope,效果不错。
+    2.将原策略的每次持有两只etf改成只买最优的一个,收益显著提高
+    3.将每周调仓换成每日调仓,收益显著提高
+    4.因为交易etf,所以手续费设为万分之三,印花税设为零,未设置滑点
+    5.修改股票池中候选etf,删除银行,红利等收益较弱品种,增加纳指etf以增加不同国家市场间轮动的可能性
+    6.根据研报,默认参数介已设定为最优
+    7.加入防未来函数
+    8.增加择时与选股模块的打印日志,方便观察每笔操作依据
+'''
+
+#导入函数库
+from jqdata import *
+import numpy as np
+
+#初始化函数 
+def initialize(context):
+    # 设定沪深300作为基准
+    set_benchmark('000300.XSHG')
+    # 用真实价格交易
+    set_option('use_real_price', True)
+    # 打开防未来函数
+    set_option("avoid_future_data", True)
+    # 将滑点设置为0.001
+    set_slippage(FixedSlippage(0.001))
+    # 设置交易成本万分之三
+    set_order_cost(OrderCost(open_tax=0, close_tax=0, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5),
+                   type='fund')
+    # 过滤order中低于error级别的日志
+    log.set_level('order', 'error')
+    # 初始化各类全局变量
+    #股票池
+    g.fund_pool = [
+        # '510050.XSHG', #上证50
+        '159915.XSHE', #创业板
+        '513100.XSHG', #纳指
+        '159928.XSHE', #中证消费
+        # '510300.XSHG', # 沪深300ETF
+        '510500.XSHG', # 中证500ETF
+        '159939.XSHE', # 信息技术
+        '512010.XSHG', # 医药ETF
+        '159940.XSHE', # 全指金融
+        '512980.XSHG', # 传媒ETF
+        '159920.XSHE', # 恒生ETF
+        # '513050.XSHG', # 中概互联
+        # '162411.XSHE', # 华宝油气lof
+        # '501018.XSHG', # 南方原油lof
+        '512880.XSHG', # 证券ETF
+        # '163119.XSHE', # 申万健康lof
+        '512580.XSHG', # 环保etf
+        '513500.XSHG', # 标普500
+        # '513030.XSHG', # 德国30
+        # '513880.XSHG' # 日经225
+        
+    ]
+    #动量轮动参数
+    g.stock_num = 1 #买入评分最高的前stock_num只股票
+    g.momentum_day = 257 #最新动量参考最近momentum_day的
+    #rsrs择时参数
+    g.ref_stock = '000300.XSHG' #用ref_stock做择时计算的基础数据
+    g.N = 17 # 计算最新斜率slope,拟合度r2参考最近N天
+    g.M = 900 # 计算最新标准分zscore,rsrs_score参考最近M天
+    g.score_threshold = 0.7 # rsrs标准分指标阈值
+    #ma择时参数
+    g.mean_day = 20 #计算结束ma收盘价,参考最近mean_day 
+    g.mean_diff_day = 3 #计算初始ma收盘价,参考(mean_day + mean_diff_day)天前,窗口为mean_diff_day的一段时间
+    g.slope_series = initial_slope_series()[:-1] # 除去回测第一天的slope,避免运行时重复加入
+    # 设置交易时间,每天运行
+    run_daily(my_trade, time='11:30', reference_security='000300.XSHG')
+    run_daily(check_lose, time='open', reference_security='000300.XSHG')
+    run_daily(print_trade_info, time='15:30', reference_security='000300.XSHG')
+    
+#1-1 选股模块-动量因子轮动 
+#基于股票年化收益和判定系数打分,并按照分数从大到小排名
+def get_rank(context, stock_pool):
+    score_list = []
+    fund_list = g.fund_pool
+    fund_list = filter_new_fund(context, g.fund_pool)
+    for fund in fund_list:
+        data = attribute_history(fund, g.momentum_day, '1d', ['close'])
+        y = data['log'] = np.log(data.close)
+        x = data['num'] = np.arange(data.log.size)
+ 
+        slope, intercept = np.polyfit(x, y, 1)
+        annualized_returns = math.pow(math.exp(slope), 250) - 1
+        r_squared = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1)))
+        score = annualized_returns * r_squared
+        score_list.append(score)
+    stock_dict=dict(zip(fund_list, score_list))
+    sort_list=sorted(stock_dict.items(), key=lambda item:item[1], reverse=True) #True为降序
+    code_list=[]
+    for i in range((len(fund_list))):
+        code_list.append(sort_list[i][0])
+    rank_stock = code_list[0:g.stock_num]
+    print(code_list[0:5])
+    return rank_stock
+
+#2-1 择时模块-计算线性回归统计值
+#对输入的自变量每日最低价x(series)和因变量每日最高价y(series)建立OLS回归模型,返回元组(截距,斜率,拟合度)
+def get_ols(x, y):
+    slope, intercept = np.polyfit(x, y, 1)
+    r2 = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1)))
+    return (intercept, slope, r2)
+
+#2-2 择时模块-设定初始斜率序列
+#通过前M日最高最低价的线性回归计算初始的斜率,返回斜率的列表
+def initial_slope_series():
+    data = attribute_history(g.ref_stock, g.N + g.M, '1d', ['high', 'low'])
+    return [get_ols(data.low[i:i+g.N], data.high[i:i+g.N])[1] for i in range(g.M)]
+
+#2-3 择时模块-计算标准分
+#通过斜率列表计算并返回截至回测结束日的最新标准分
+def get_zscore(slope_series):
+    mean = np.mean(slope_series)
+    std = np.std(slope_series)
+    return (slope_series[-1] - mean) / std
+
+#2-4 择时模块-计算综合信号
+#1.获得rsrs与MA信号,rsrs信号算法参考优化说明,MA信号为一段时间两个端点的MA数值比较大小
+#2.信号同时为True时返回买入信号,同为False时返回卖出信号,其余情况返回持仓不变信号
+def get_timing_signal(stock):
+    #计算MA信号
+    close_data = attribute_history(g.ref_stock, g.mean_day + g.mean_diff_day, '1d', ['close'])
+    today_MA = close_data.close[g.mean_diff_day:].mean() 
+    before_MA = close_data.close[:-g.mean_diff_day].mean()
+    #计算rsrs信号
+    high_low_data = attribute_history(g.ref_stock, g.N, '1d', ['high', 'low'])
+    intercept, slope, r2 = get_ols(high_low_data.low, high_low_data.high)
+    g.slope_series.append(slope)
+    rsrs_score = get_zscore(g.slope_series[-g.M:]) * r2
+    #综合判断所有信号
+    if rsrs_score > g.score_threshold:# and today_MA > before_MA:
+        print('BUY')
+        return "BUY"
+    elif rsrs_score < -g.score_threshold:# and today_MA < before_MA:
+        print('SELL')
+        return "SELL"
+    else:
+        print('KEEP')
+        return "KEEP"
+
+
+
+#3-1 过滤模块-过滤停牌股票
+#输入选股列表,返回剔除停牌股票后的列表
+def filter_paused_stock(stock_list):
+	current_data = get_current_data()
+	return [stock for stock in stock_list if not current_data[stock].paused]
+
+#3-2 过滤模块-过滤ST及其他具有退市标签的股票
+#输入选股列表,返回剔除ST及其他具有退市标签股票后的列表
+def filter_st_stock(stock_list):
+	current_data = get_current_data()
+	return [stock for stock in stock_list
+			if not current_data[stock].is_st
+			and 'ST' not in current_data[stock].name
+			and '*' not in current_data[stock].name
+			and '退' not in current_data[stock].name]
+
+#3-3 过滤模块-过滤涨停的股票
+#输入选股列表,返回剔除未持有且已涨停股票后的列表
+def filter_limitup_stock(context, stock_list):
+	last_prices = history(1, unit='1m', field='close', security_list=stock_list)
+	current_data = get_current_data()
+	# 已存在于持仓的股票即使涨停也不过滤,避免此股票再次可买,但因被过滤而导致选择别的股票
+	return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
+			or last_prices[stock][-1] < current_data[stock].high_limit]
+
+#3-4 过滤模块-过滤跌停的股票
+#输入股票列表,返回剔除已跌停股票后的列表
+def filter_limitdown_stock(context, stock_list):
+	last_prices = history(1, unit='1m', field='close', security_list=stock_list)
+	current_data = get_current_data()
+	return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
+			or last_prices[stock][-1] > current_data[stock].low_limit]
+
+#4-1 交易模块-自定义下单
+#报单成功返回报单(不代表一定会成交),否则返回None,应用于
+def order_target_value_(security, value):
+	if value == 0:
+		log.debug("Selling out %s" % (security))
+	else:
+		log.debug("Order %s to value %f" % (security, value))
+	# 如果股票停牌,创建报单会失败,order_target_value 返回None
+	# 如果股票涨跌停,创建报单会成功,order_target_value 返回Order,但是报单会取消
+	# 部成部撤的报单,聚宽状态是已撤,此时成交量>0,可通过成交量判断是否有成交
+	return order_target_value(security, value)
+
+#4-2 交易模块-开仓
+#买入指定价值的证券,报单成功并成交(包括全部成交或部分成交,此时成交量大于0)返回True,报单失败或者报单成功但被取消(此时成交量等于0),返回False
+def open_position(security, value):
+	order = order_target_value_(security, value)
+	print(order) # 查看订单的信息
+	if order != None and order.filled > 0:
+		return True
+	return False
+
+#4-3 交易模块-平仓
+#卖出指定持仓,报单成功并全部成交返回True,报单失败或者报单成功但被取消(此时成交量等于0),或者报单非全部成交,返回False
+def close_position(position):
+	security = position.security
+	order = order_target_value_(security, 0)  # 可能会因停牌失败
+	if order != None:
+		if order.status == OrderStatus.held and order.filled == order.amount:
+			return True
+	return False
+
+#4-4 交易模块-调仓
+#当择时信号为买入时开始调仓,输入过滤模块处理后的股票列表,执行交易模块中的开平仓操作
+def adjust_position(context, buy_stocks):
+	for stock in context.portfolio.positions:
+		if stock not in buy_stocks:
+			log.info("[%s]已不在应买入列表中" % (stock))
+			position = context.portfolio.positions[stock]
+			close_position(position)
+		else:
+			log.info("[%s]已经持有无需重复买入" % (stock))
+	# 根据股票数量分仓
+	# 此处只根据可用金额平均分配购买,不能保证每个仓位平均分配
+	position_count = len(context.portfolio.positions)
+	if g.stock_num > position_count:
+		value = context.portfolio.cash / (g.stock_num - position_count)
+		for stock in buy_stocks:
+			if context.portfolio.positions[stock].total_amount == 0:
+				if open_position(stock, value):
+					if len(context.portfolio.positions) == g.stock_num:
+						break
+
+#4-5 交易模块-择时交易
+#结合择时模块综合信号进行交易
+def my_trade(context):
+    #获取选股列表并过滤掉:st,st*,退市,涨停,跌停,停牌
+    check_out_list = get_rank(context,g.fund_pool) # 获得排名
+    # 感觉不需要这些,都只是对应股票的
+    # check_out_list = filter_st_stock(check_out_list) # 去掉st
+    # check_out_list = filter_limitup_stock(context, check_out_list) # 去掉涨停
+    # check_out_list = filter_limitdown_stock(context, check_out_list) # 去掉跌停
+    # check_out_list = filter_paused_stock(check_out_list) # 去掉停牌
+    print('今日自选股:{}'.format(check_out_list))
+    #获取综合择时信号
+    timing_signal = get_timing_signal(g.ref_stock) # 判断买卖的信号
+    print('今日择时信号:{}'.format(timing_signal))
+    #开始交易
+    if timing_signal == 'SELL':
+        for stock in context.portfolio.positions:
+            position = context.portfolio.positions[stock]
+            close_position(position)
+    elif timing_signal == 'BUY' or timing_signal == 'KEEP':
+        adjust_position(context, check_out_list)
+    else:
+        pass
+
+#4-6 交易模块-止损
+#检查持仓并进行必要的止损操作
+def check_lose(context):
+    for position in list(context.portfolio.positions.values()):
+        securities=position.security
+        cost=position.avg_cost
+        price=position.price
+        ret=100*(price/cost-1)
+        value=position.value
+        amount=position.total_amount
+        #这里设定80%止损几乎等同不止损,因为止损在指数etf策略中影响不大
+        if ret <=-80:
+            order_target_value(position.security, 0)
+            print("!!!!!!触发止损信号: 标的={},标的价值={},浮动盈亏={}% !!!!!!"
+                .format(securities,format(value,'.2f'),format(ret,'.2f')))
+
+#5-1 复盘模块-打印
+#打印每日持仓信息
+def print_trade_info(context):
+    #打印当天成交记录
+    trades = get_trades()
+    for _trade in trades.values():
+        print('成交记录:'+str(_trade))
+    #打印账户信息
+    for position in list(context.portfolio.positions.values()):
+        securities=position.security
+        cost=position.avg_cost
+        price=position.price
+        ret=100*(price/cost-1)
+        value=position.value
+        amount=position.total_amount    
+        print('代码:{}'.format(securities))
+        print('成本价:{}'.format(format(cost,'.2f')))
+        print('现价:{}'.format(price))
+        print('收益率:{}%'.format(format(ret,'.2f')))
+        print('持仓(股):{}'.format(amount))
+        print('市值:{}'.format(format(value,'.2f')))
+    print('一天结束')
+    print('———————————————————————————————————————分割线————————————————————————————————————————')
+
+
+# 过滤次新基金
+def filter_new_fund(context, fund_list):
+    return [fund for fund in fund_list if (context.previous_date - datetime.timedelta(days=200)) > get_security_info(fund).start_date] #? 应该是按照300天在计算

+ 401 - 0
Lib/fund/fund_grid_trading.py

@@ -0,0 +1,401 @@
+from jqdata import *
+from jqlib.technical_analysis import *
+import numpy as np
+import pandas as pd
+import datetime as dt
+
+# 初始化函数,设定基准等等
+def initialize(context):
+    # 设定沪深300作为基准
+    set_benchmark('000300.XSHG')
+    # 开启异步报单
+    set_option('async_order', True)
+    # 开启动态复权模式(真实价格)
+    set_option('use_real_price', True)
+    # 是否未来函数
+    set_option("avoid_future_data", True)
+    
+    # 初始化全局变量
+    g.observation_period = 1260  # 默认观察周期为5年(约252个交易日)
+    g.total_portions = 100  # 总资金分成100份
+    g.buy_thresholds = {
+        0.3: 1,  # 相对位置30%买入1份
+        0.2: 2,  # 相对位置20%买入2份
+        0.1: 3,  # 相对位置10%买入3份
+        0.0: 5   # 相对位置最低买入5份
+    }
+    g.profit_target = 0.5  # 50%的目标收益率
+    g.take_profit_portion = 0.05  # 每次卖出获利5%的部分
+    
+    # 记录买入历史和持仓信息
+    g.buy_records = {}  # 记录每个基金在不同阈值的买入状态
+    g.position_records = {}  # 记录每个基金的持仓成本和数量
+    g.profit_records = {}  # 记录已经获利的部分
+
+    # 新增:交易信息存储字典
+    g.trading_records = {
+        'normal_positions': {},  # 正常网格交易持仓
+        'profit_positions': {}   # 获利后保留的持仓
+    }
+    
+    # 设置交易费用
+    set_order_cost(OrderCost(close_tax=0.000, open_commission=0.00025, close_commission=0.00025, min_commission=0), type='fund')
+    
+    # 设定定时运行函数
+    run_weekly(before_market_open, 1, '09:20')
+    run_daily(market_open, '09:31')
+    run_daily(check_profit, '14:30')
+    # run_daily(print_position_info, '15:30')
+
+def before_market_open(context):
+    # 获取目标基金列表
+    g.target_funds = [
+        '512880.XSHG', '512980.XSHG', '513050.XSHG', '159938.XSHE',
+        '512070.XSHG', '515180.XSHG', '513520.XSHG', '159952.XSHE',
+        '162412.XSHE', '510300.XSHG', '510050.XSHG', '510500.XSHG',
+        '588080.XSHG', '588000.XSHG', '159901.XSHE', '159915.XSHE',
+        '159919.XSHE', '159922.XSHE', '159920.XSHE', '159985.XSHE',
+        '501018.XSHG', '162411.XSHE'
+    ]
+    
+    # 初始化当日需要检查的基金列表
+    g.check_funds = []
+
+    # log.info("\n=== 周一基金检查 ===")
+    # log.info(f"观察周期: {g.observation_period}个交易日")
+    # log.info("\n基金历史位置统计:")
+    
+    # 记录所有基金的相对位置,用于计算平均值
+    all_positions = []
+    
+    for fund in g.target_funds:
+        try:
+            df = get_price(fund, count=g.observation_period, end_date=context.previous_date, fields=['close'])
+            current_price = df['close'][-1]
+            hist_prices = df['close'].values
+            
+            # 计算相对于历史最高价和最低价的位置
+            hist_high = np.max(hist_prices)
+            hist_low = np.min(hist_prices)
+            price_position = (current_price - hist_low) / (hist_high - hist_low) if hist_high != hist_low else 0
+            all_positions.append(price_position)
+            
+            # 获取基金名称
+            fund_info = get_security_info(fund)
+            fund_name = fund_info.display_name if fund_info else fund
+            
+            # log.info(f"基金 {fund}({fund_name}): | 当前价格: {format(current_price, '.4f')}|  相对位置: {format(price_position * 100, '.2f')}%| 价格区间: {format(hist_low, '.4f')} - {format(hist_high, '.4f')}")
+            
+            if price_position <= 0.35:  # 如果当前价格在相对位置40%以下
+                g.check_funds.append(fund)
+            #     log.info("  状态: 需要每日检查 ✓")
+            # else:
+            #     log.info("  状态: 暂不检查 ✗")
+            
+        except Exception as e:
+            # log.error(f"获取基金{fund}数据失败: {str(e)}")
+            continue
+    
+    # 输出统计信息
+    if all_positions:
+        avg_position = np.mean(all_positions)
+        median_position = np.median(all_positions)
+        low_position_count = sum(1 for p in all_positions if p <= 0.3)
+        
+        # log.info("\n=== 统计信息 ===")
+        # log.info(f"平均相对位置: {format(avg_position * 100, '.2f')}% | 中位相对位置: {format(median_position * 100, '.2f')}% | 低位基金数量: {low_position_count} (相对位置≤30%)")
+        
+        if g.check_funds:
+            # log.info("\n=== 需要每日检查的基金 ===")
+            for fund in g.check_funds:
+                fund_info = get_security_info(fund)
+                fund_name = fund_info.display_name if fund_info else fund
+        #         log.info(f"- {fund}({fund_name})")
+        # else:
+        #     log.info("\n没有基金需要每日检查")
+
+        # 在统计信息之后,添加交易持仓信息的输出
+    log.info("\n=== 交易持仓信息 ===")
+    
+    # 输出正常网格交易持仓
+    if g.trading_records['normal_positions']:
+        log.info("正常网格交易持仓:")
+        for fund, positions in g.trading_records['normal_positions'].items():
+            fund_info = get_security_info(fund)
+            fund_name = fund_info.display_name if fund_info else fund
+            for pos in positions:
+                log.info(f"标的 {fund}({fund_name}): "
+                        f"买入时间 {pos['buy_time'].strftime('%Y-%m-%d %H:%M')}, "
+                        f"相对位置 {format(pos['price_position'] * 100, '.2f')}%, "
+                        f"买入数量 {format(pos['shares'], '.2f')}, "
+                        f"买入价格 {format(pos['buy_price'], '.4f')}, "
+                        f"买入金额 {format(pos['total_amount'], '.2f')}")
+    else:
+        log.info("当前无正常网格交易持仓")
+    
+    # 输出获利保留持仓
+    if g.trading_records['profit_positions']:
+        log.info("\n获利保留持仓:")
+        for fund, positions in g.trading_records['profit_positions'].items():
+            fund_info = get_security_info(fund)
+            fund_name = fund_info.display_name if fund_info else fund
+            for pos in positions:
+                log.info(f"标的 {fund}({fund_name}): "
+                        f"原始买入时间 {pos['buy_time'].strftime('%Y-%m-%d %H:%M')}, "
+                        f"卖出时间 {pos['sell_time'].strftime('%Y-%m-%d %H:%M')}, "
+                        f"原始相对位置 {format(pos['price_position'] * 100, '.2f')}%, "
+                        f"买入价格 {format(pos['buy_price'], '.4f')}, "
+                        f"卖出价格 {format(pos['sell_price'], '.4f')}, "
+                        f"卖出数量 {format(pos['sell_shares'], '.2f')}, "
+                        f"卖出金额 {format(pos['sell_amount'], '.2f')}, "
+                        f"保留份额 {format(pos['remaining_shares'], '.2f')}")
+    else:
+        log.info("当前无获利保留持仓")
+
+def market_open(context):
+    # 检查需要买入的基金
+    for fund in g.check_funds:
+        try:
+            # 过滤停牌和新成立基金
+            if not filter_paused_fund(context, fund) or not filter_new_fund(context, fund):
+                continue
+                
+            df = get_price(fund, count=g.observation_period, end_date=context.previous_date, fields=['close'])
+            current_price = df['close'][-1]
+            hist_prices = df['close'].values
+            
+            # 计算相对于历史最高价和最低价的位置
+            hist_high = np.max(hist_prices)
+            hist_low = np.min(hist_prices)
+            price_position = (current_price - hist_low) / (hist_high - hist_low) if hist_high != hist_low else 0
+            
+            # 如果该基金没有买入记录,初始化记录
+            if fund not in g.buy_records:
+                g.buy_records[fund] = {threshold: False for threshold in g.buy_thresholds.keys()}
+            
+            # 检查每个阈值是否需要买入
+            for threshold, portions in g.buy_thresholds.items():
+                if price_position <= threshold and not g.buy_records[fund][threshold]:
+                    # 计算买入金额
+                    portion_value = context.portfolio.total_value / g.total_portions
+                    buy_amount = portion_value * portions
+                    
+                    # 执行买入
+                    order = order_target_value_(fund, buy_amount)
+                    if order is not None and order.status == OrderStatus.held and order.filled == order.amount:
+                        g.buy_records[fund][threshold] = True
+                        
+                        # 记录持仓信息
+                        if fund not in g.position_records:
+                            g.position_records[fund] = []
+                        g.position_records[fund].append({
+                            'buy_price': current_price,
+                            'amount': buy_amount,
+                            'threshold': threshold
+                        })
+                        
+                        # 更新交易记录
+                        if fund not in g.trading_records['normal_positions']:
+                            g.trading_records['normal_positions'][fund] = []
+                        
+                        # 获取实际成交信息
+                        filled_shares = order.filled
+                        total_amount = filled_shares * current_price
+                        
+                        g.trading_records['normal_positions'][fund].append({
+                            'buy_time': context.current_dt,
+                            'price_position': price_position,
+                            'shares': filled_shares,
+                            'buy_price': current_price,
+                            'total_amount': total_amount
+                        })
+                        
+                        # log.info(f"买入基金{fund},价格{current_price},金额{buy_amount},相对位置{format(price_position * 100, '.2f')}%,阈值{format(threshold * 100, '.2f')}%")
+        except Exception as e:
+            # log.error(f"处理基金{fund}买入失败: {str(e)}")
+            continue
+
+def check_profit(context):
+    # 检查所有持仓是否达到盈利目标
+    for fund in list(g.position_records.keys()):  # 使用list复制键,避免运行时修改字典
+        try:
+            if not filter_paused_fund(context, fund):
+                continue
+                
+            current_price = get_current_data()[fund].last_price
+            
+            # 检查每个持仓记录
+            positions_to_remove = []  # 记录需要移除的持仓
+            for position in g.position_records[fund]:
+                if current_price >= position['buy_price'] * (1 + g.profit_target):
+                    # 计算需要卖出的份额
+                    # 原始投入金额
+                    original_value = position['amount']
+                    # 目标收益金额(比如10%)
+                    target_profit = original_value * g.take_profit_portion
+                    # 需要卖出的份额 = (原始金额 + 目标收益) / 当前价格
+                    shares_to_sell = (original_value + target_profit) / current_price
+                    # 计算卖出金额
+                    sell_amount = shares_to_sell * current_price
+                    
+                    # 执行卖出
+                    order = order_target_value_(fund, -sell_amount)
+                    if order is not None and order.status == OrderStatus.held and order.filled > 0:
+                        # 计算实际获利
+                        actual_profit = sell_amount - (sell_amount / current_price * position['buy_price'])
+                        
+                        # 更新持仓记录
+                        position['amount'] -= sell_amount
+                        
+                        # 记录获利部分
+                        if fund not in g.profit_records:
+                            g.profit_records[fund] = []
+                        g.profit_records[fund].append({
+                            'sell_price': current_price,
+                            'profit_amount': actual_profit,
+                            'sell_date': context.current_dt,
+                            'sell_shares': shares_to_sell
+                        })
+                        
+                        # 更新交易记录 - 从normal_positions移除卖出部分
+                        if fund in g.trading_records['normal_positions']:
+                            # 找到对应的买入记录
+                            for normal_pos in g.trading_records['normal_positions'][fund]:
+                                if normal_pos['buy_price'] == position['buy_price']:
+                                    # 计算剩余份额
+                                    remaining_shares = normal_pos['shares'] - shares_to_sell
+                                    if remaining_shares > 0:
+                                        normal_pos['shares'] = remaining_shares
+                                        normal_pos['total_amount'] = remaining_shares * normal_pos['buy_price']
+                                    else:
+                                        g.trading_records['normal_positions'][fund].remove(normal_pos)
+                                    
+                                    # 添加到获利持仓记录
+                                    if fund not in g.trading_records['profit_positions']:
+                                        g.trading_records['profit_positions'][fund] = []
+                                    
+                                    g.trading_records['profit_positions'][fund].append({
+                                        'buy_time': normal_pos['buy_time'],
+                                        'sell_time': context.current_dt,
+                                        'price_position': normal_pos['price_position'],
+                                        'buy_price': normal_pos['buy_price'],
+                                        'sell_price': current_price,
+                                        'sell_shares': shares_to_sell,
+                                        'sell_amount': sell_amount,
+                                        'remaining_shares': remaining_shares if remaining_shares > 0 else 0
+                                    })
+                                    break
+                            
+                            # 如果该基金没有正常持仓了,清理记录
+                            if not g.trading_records['normal_positions'][fund]:
+                                del g.trading_records['normal_positions'][fund]
+                        
+                        # log.info(f"卖出基金{fund}获利部分: | 买入价格: {format(position['buy_price'], '.4f')}| 当前价格: {format(current_price, '.4f')}| 卖出份额: {format(shares_to_sell, '.2f')}| 卖出金额: {format(sell_amount, '.2f')}| 实际获利: {format(actual_profit, '.2f')}| 剩余金额: {format(position['amount'], '.2f')}") 
+                        
+                        # 如果剩余金额很小,加入待清理列表
+                        if position['amount'] < 100:
+                            positions_to_remove.append(position)
+            
+            # 清理需要移除的持仓记录
+            for position in positions_to_remove:
+                g.buy_records[fund][position['threshold']] = False
+                g.position_records[fund].remove(position)
+            
+            # 如果该基金没有持仓了,清理记录
+            if not g.position_records[fund]:
+                del g.position_records[fund]
+                
+        except Exception as e:
+            # log.error(f"处理基金{fund}获利了结失败: {str(e)}")
+            continue
+
+def print_position_info(context):
+    # 打印当天成交记录
+    trades = get_trades()
+    for _trade in trades.values():
+        print(f'成交记录:{str(_trade)}')
+    
+    # 打印账户信息
+    print("\n=== 当前持仓信息 ===")
+    for position in list(context.portfolio.positions.values()):
+        security = position.security
+        cost = position.avg_cost
+        price = position.price
+        ret = 100 * (price/cost - 1)
+        value = position.value
+        amount = position.total_amount    
+        print(f'代码: {security} | 成本价: {format(cost, ".2f")} | 现价: {price} | 收益率: {format(ret, ".2f")}% | 持仓(份): {amount} | 市值: {format(value, ".2f")}')
+    
+    # 打印当日盈利记录
+    print("\n=== 当日获利记录 ===")
+    for fund in g.profit_records.keys():
+        if g.profit_records[fund]:
+            today_profits = [record for record in g.profit_records[fund] 
+                           if record['sell_date'].date() == context.current_dt.date()]
+            if today_profits:
+                print(f"\n基金{fund}今日获利记录:")
+                total_profit = sum([record['profit_amount'] for record in today_profits])
+                print(f"今日获利:{format(total_profit, '.2f')}")
+
+# def after_trading_end(context):
+#     # 打印每日持仓和盈利信息
+#     log.info("=== 当日持仓信息 ===")
+#     for fund in g.position_records.keys():
+#         if g.position_records[fund]:
+#             log.info(f"基金{fund}持仓:")
+#             for pos in g.position_records[fund]:
+#                 log.info(f"买入价格:{pos['buy_price']},持仓金额:{pos['amount']},买入阈值:{pos['threshold']}")
+    
+#     log.info("=== 当日获利记录 ===")
+#     for fund in g.profit_records.keys():
+#         if g.profit_records[fund]:
+#             total_profit = sum([record['profit_amount'] for record in g.profit_records[fund]])
+#             log.info(f"基金{fund}获利记录: | 总获利:{total_profit}") 
+
+# 交易模块 - 自定义下单
+def order_target_value_(security, value):
+    if value == 0:
+        log.debug(f"清空持仓 {security}")
+    else:`
+        log.debug(f"目标下单 {security},金额 {value}")
+    return order_target_value(security, value)
+
+# 交易模块 - 开仓
+def open_position(security, value):
+    if value == 0:
+        return True
+    order = order_target_value_(security, value)
+    if order is not None and order.status == OrderStatus.held and order.filled == order.amount:
+        return True
+    return False
+
+# 交易模块 - 平仓
+def close_position(position):
+    security = position.security
+    order = order_target_value_(security, 0)
+    if order is not None and order.status == OrderStatus.held and order.filled == order.amount:
+        return True
+    return False
+
+# 交易模块 - 调整仓位
+def adjust_position(context, security, target_value):
+    if security not in context.portfolio.positions:
+        if target_value > 0:
+            return open_position(security, target_value)
+    else:
+        position = context.portfolio.positions[security]
+        if target_value == 0:
+            return close_position(position)
+        else:
+            return order_target_value_(security, target_value)
+    return False
+
+# 过滤模块 - 过滤停牌基金
+def filter_paused_fund(context, security):
+    current_data = get_current_data()
+    return not current_data[security].paused
+
+# 过滤模块 - 过滤新成立基金
+def filter_new_fund(context, security):
+    yesterday = context.previous_date
+    return not yesterday - get_security_info(security).start_date < datetime.timedelta(days=30)  # 过滤成立不足30天的基金

+ 193 - 0
Lib/future/HS300_SimplifiedSpiderGridTrading.py

@@ -0,0 +1,193 @@
+# 克隆自聚宽文章:https://www.joinquant.com/post/36687
+# 标题:股指期货-简化版蜘蛛网策略
+# 作者:宋兵乙
+
+# 导入函数库
+from six import BytesIO
+import numpy as np
+import pandas as pd
+import talib as ta
+from jqdata import finance
+
+from datetime import datetime, timedelta
+
+## 初始化函数,设定基准等等
+def initialize(context):
+    
+    # 设定沪深300作为基准
+    set_benchmark('000300.XSHG')
+    # 开启动态复权模式(真实价格)
+    set_option('use_real_price', True)
+    # 过滤掉order系列API产生的比error级别低的log
+    log.set_level('order', 'error')
+    # 输出内容到日志 log.info()
+    log.info('初始函数开始运行且全局只运行一次')
+    set_subportfolios([SubPortfolioConfig(cash=context.portfolio.starting_cash, type='index_futures')])
+    set_order_cost(OrderCost(open_commission=0.000023, close_commission=0.000023,close_today_commission=0.0004), type='index_futures')
+    set_option('futures_margin_rate', 0.17)
+    set_slippage(StepRelatedSlippage(4))
+    
+    g.close_long = []
+    g.close_short = []
+    g.open_long = []
+    g.open_short = []
+    g.times = 3
+
+    run_daily(cal_signal, time='20:30')
+
+    run_daily(trade, time='09:32')
+    
+## 开盘前运行函数
+def set_parameter():
+    ## 设置交易品种
+    g.symbol_list = [
+        'IH',
+        ]
+        
+    ## 设置品种乘数
+    g.muliti_dict = {
+        ## 中金所
+        'IF':300, 'TF':10000, 'IC':200, 'IH':300,
+        }
+
+## 开盘前20:30:00运行函数
+def cal_signal(context):
+    # 参数设置
+    set_parameter()
+    # 创建保持计算结果的DataFrame
+    g.signal_df = pd.DataFrame(columns=['期货代码','交易信号','手数'])
+    ## 获取当前持仓
+    g.short_holds = context.portfolio.short_positions.keys()
+    g.long_holds = context.portfolio.long_positions.keys()
+    for symbol in g.symbol_list:
+        ## 获取主力合约
+        symbol_dominant = get_dominant_future(symbol)
+        
+        if len(symbol_dominant) == 0:
+            continue
+        
+        # # 获取品种持仓状况
+        if  symbol_dominant in g.short_holds:
+            hold_signal = -1
+        # 查看多单仓位情况    
+        elif  symbol_dominant in g.long_holds:
+            hold_signal = 1
+        else:
+            hold_signal = 0
+        
+        ## 现在的自然日日期
+        date = context.current_dt.strftime('%Y%m%d')
+    
+        ## 获取主力合约价格
+        current_time = context.current_dt
+        price_data = get_price(symbol_dominant, end_date=current_time, frequency='1d', 
+                                    fields=['close'], count = 10)
+        dominant_price = price_data['close'][-1]
+        # 获取所有合约
+        contract_all = get_future_contracts(symbol, current_time)
+        # print('contract_all: ', contract_all)
+
+        long_hold_delta = 0
+        short_hold_delta = 0
+        
+        for contract in contract_all:
+        
+            q=query(finance.FUT_MEMBER_POSITION_RANK.day,
+            finance.FUT_MEMBER_POSITION_RANK.code,
+            finance.FUT_MEMBER_POSITION_RANK.rank_type,
+            finance.FUT_MEMBER_POSITION_RANK.rank,
+            finance.FUT_MEMBER_POSITION_RANK.member_name,
+            finance.FUT_MEMBER_POSITION_RANK.indicator,
+            finance.FUT_MEMBER_POSITION_RANK.indicator_increase).\
+            filter(finance.FUT_MEMBER_POSITION_RANK.code==contract,
+                   finance.FUT_MEMBER_POSITION_RANK.day==date)
+            
+            df=finance.run_query(q)
+            
+            #数据分离,分别按多单持有量与空单持有量排序
+            df1 = df[df['rank_type']=='持买单量排名']
+            df2 = df[df['rank_type']=='持卖单量排名']
+            
+            #分别计算多、空持仓前二十会员的多仓增量与空仓增量
+            long_hold_delta = long_hold_delta  + df1[:20]['indicator_increase'].sum()
+            short_hold_delta = short_hold_delta + df2[:20]['indicator_increase'].sum()
+            # print(contract)
+            # print('long_hold_delta: ', long_hold_delta)
+            # print('short_hold_delta: ', short_hold_delta)
+            
+        try:
+            if hold_signal == 0:
+                if long_hold_delta>0 and short_hold_delta<0:
+                    signal = 1
+                elif long_hold_delta<0 and short_hold_delta>0:
+                    signal = -1
+                else: signal = 0
+                    
+            if hold_signal > 0:
+                if long_hold_delta <= 0 or short_hold_delta >= 0:
+                    signal = 0
+                else: 
+                    signal = 1
+              
+            if hold_signal < 0:
+                if long_hold_delta >= 0 or short_hold_delta <= 0:
+                    signal = 0
+                else: 
+                    signal = -1   
+        except:
+            log.info('信号计算报错'+str(symbol_dominant))
+            
+        # 按照两倍杠杆计算持仓手数
+        total_value = context.portfolio.total_value
+        multi = g.muliti_dict[symbol]
+        amount = int(total_value / dominant_price / multi /len(g.symbol_list) * g.times)
+        print('total_value: ', total_value)
+        print('dominant_price: ', dominant_price)
+        print('multi: ', multi)
+        print('g.symbol_list: ', len(g.symbol_list))
+        print('amount: ', amount)
+        # 开仓数量过滤
+        if amount < 1:
+            amount = 1
+            
+        g.signal_df = g.signal_df.append({'期货代码': symbol_dominant, 
+                                '交易信号': signal,
+                                '手数': amount,
+                                },
+                              ignore_index=True)
+                          
+    log.info('\n' + str(g.signal_df))
+    signal_df_analysis(context)
+    
+## 夜盘交易函数
+def trade(context):
+
+    # 平仓
+    for future in g.close_long:
+        order_target(future, 0, side='long')
+    for future in g.close_short:
+        order_target(future, 0, side='short')
+
+    # 开仓
+    for future in g.open_long:
+        amount = g.signal_df[g.signal_df['期货代码'] == future]['手数'].values[0]
+        order_target(future, amount, side='long')
+    for future in g.open_short:
+        amount = g.signal_df[g.signal_df['期货代码'] == future]['手数'].values[0]
+        order_target(future, amount, side='short') 
+    
+    
+def signal_df_analysis(context):
+    #交易信号
+    target_long = g.signal_df[g.signal_df['交易信号'] > 0]
+    target_short = g.signal_df[g.signal_df['交易信号'] < 0]
+    
+    target_long_list = list(target_long['期货代码'])  # 想持多仓的目标
+    target_short_list = list(target_short['期货代码'])  # 想持空仓的目标
+    
+    # 下一个交易日需要处理多类的品种
+    g.open_long = [i for i in target_long_list if i not in g.long_holds]
+    g.close_long = [i for i in g.long_holds if i not in target_long_list]
+    # 下一个交易日需要处理空类的品种
+    g.open_short = [i for i in target_short_list if i not in g.short_holds]
+    g.close_short = [i for i in g.short_holds if i not in target_short_list]

+ 189 - 0
Lib/future/README.md

@@ -0,0 +1,189 @@
+# 期货投资策略说明
+
+## 沪深300期货蜘蛛网策略
+
+### 核心思路
+该策略基于期货主力合约的持仓数据进行交易,通过监控主力多空持仓变化来判断市场方向。策略重点关注机构持仓变动,通过多空持仓增量的对比来进行交易决策。
+
+1. [策略来源](https://www.joinquant.com/algorithm/index/edit?algorithmId=7dfbd7e00953112d8557fe677d18a600)
+2. 策略名称:股指期货-简化版蜘蛛网策略
+
+### 主要特点
+1. 基于持仓数据分析
+2. 多空双向交易
+3. 动态仓位管理
+4. 适应性强(可用于多个期货品种)
+5. 基于日频交易
+
+### 具体策略逻辑
+
+#### 1. 交易品种
+- 主要交易品种:**g.symbol_list**, 沪深300股指期货(IH)
+- 交易合约:主力合约
+- 合约乘数:300
+
+#### 2. 信号生成
+
+##### 2.1 数据获取
+- 时间:每个交易日20:30计算信号
+- 数据范围:
+  1. 获取所有可交易合约
+  2. 获取每个合约的持仓排名数据
+  3. 分别统计多空前20名会员的持仓变化
+
+##### 2.2 信号计算
+- 核心逻辑:基于主力持仓变化判断市场方向
+- 计算流程:
+  1. 汇总所有合约的多空持仓变化
+  2. 计算多头持仓增量(**long_hold_delta**):前20名多头持仓的总变化
+  3. 计算空头持仓增量(**short_hold_delta**):前20名空头持仓的总变化
+  4. 信号判断:
+     - 开多条件:多头增仓(>0)且空头减仓(<0)
+     - 开空条件:多头减仓(<0)且空头增仓(>0)
+     - 其他情况:观望或平仓
+
+#### 3. 仓位管理
+
+##### 3.1 开仓规则
+- 基础计算:
+  - 单位资金 = 总资金 / 可以交易品种数量
+  - 杠杆倍数:**g.times**(默认3倍)
+- 具体计算:
+  ```python
+  手数 = int(总资金 / (合约价格 × 合约乘数 × 可以交易品种数量) × 杠杆倍数)
+  ```
+- 最小开仓:至少1手
+
+##### 3.2 持仓调整
+- 多头持仓:
+  - 持仓时遇到多头减仓或空头增仓则平仓
+  - 否则保持原有仓位
+- 空头持仓:
+  - 持仓时遇到多头增仓或空头减仓则平仓
+  - 否则保持原有仓位
+
+#### 4. 交易执行
+
+##### 4.1 交易时间
+- 信号计算:20:30
+- 交易执行:次日09:32
+
+##### 4.2 交易顺序
+1. 先执行平仓指令(平多、平空)
+2. 再执行开仓指令(开多、开空)
+
+### 交易规则
+- 采用异步报单模式
+- 使用真实价格
+- 考虑交易成本:
+  - 开仓手续费:0.0023%
+  - 平仓手续费:0.0023%
+  - 平今手续费:0.04%
+  - 最低保证金:17%
+  - 滑点值:4个价格单位
+
+## K线趋势交易策略
+
+### 核心思路
+该策略基于K线实体与多条均线的关系进行交易,通过监控K线实体与均线的交叉、穿越情况以及均线之间的排列形态来判断趋势方向。策略同时结合止损和移仓换月机制来控制风险。
+
+1. [策略来源](https://www.joinquant.com)
+2. 策略名称:K线趋势交易策略
+
+### 主要特点
+1. 多均线系统(5、10、20、30日均线)
+2. K线形态分析
+3. 完整的止损机制
+4. 自动移仓换月
+5. 支持日内和夜盘交易
+
+### 具体策略逻辑
+
+#### 1. 交易品种
+- 交易品种:商品期货和金融期货
+- 夜盘品种:金属、能源、农产品等
+- 日盘品种:股指、国债等
+- 交易合约:主力合约
+
+#### 2. 信号生成
+
+##### 2.1 趋势判断
+- 均线系统:
+  - MA5(5日均线)
+  - MA10(10日均线)
+  - MA20(20日均线)
+  - MA30(30日均线)
+- 趋势判断条件:
+  1. 计算K线实体与各均线的位置关系
+  2. 统计均线交叉次数(**count_ma_crosses**函数)
+  3. 判断均线排列形态(**check_ma_relations**函数)
+
+##### 2.2 开仓条件
+- K线形态要求:
+  1. K线实体穿越均线(**check_shadow_cross**函数)
+  2. 影线比例不超过阈值(**shadow_body_ratio_threshold**,默认0.5)
+- 均线条件:
+  1. 连续多日保持同向趋势(**continuous_days**记录)
+  2. 均线间距合理,无过度发散
+
+#### 3. 仓位管理
+
+##### 3.1 开仓规则
+- 计算开仓数量:
+  ```python
+  订单金额 = 总资产 × 单次开仓比例
+  开仓手数 = 订单金额 / (合约价格 × 合约乘数)
+  ```
+- 开仓限制:
+  1. 检查涨跌停限制
+  2. 考虑保证金要求
+  3. 检查流动性条件
+
+##### 3.2 持仓调整
+- 移仓换月条件:
+  1. 主力合约更换时自动执行
+  2. 确保新旧合约可交易
+  3. 保持原有持仓方向
+- 移仓流程:
+  1. 平掉旧合约
+  2. 开仓新合约
+  3. 更新交易记录
+
+#### 4. 风险控制
+
+##### 4.1 止损机制
+- 固定止损:
+  - 初始止损额度:**initial_loss_limit**(默认-4000)
+  - 每日调整:**loss_increment_per_day**(默认200)
+- 均线止损:
+  - 监控均线偏离度
+  - 突破重要均线立即止损
+
+##### 4.2 特殊情况处理
+- 涨跌停板:
+  - 无法开仓时取消交易
+  - 无法平仓时持续尝试
+- 移仓失败:
+  - 记录失败历史(**g.change_fail_history**)
+  - 保留原有持仓信息
+
+#### 5. 交易执行
+
+##### 5.1 交易时间
+- 日盘:09:30开始交易
+- 夜盘:根据品种交易时间
+- 收盘前15分钟停止开新仓
+
+##### 5.2 订单管理
+- 采用异步报单
+- 订单完整性检查
+- 成交状态跟踪
+- 失败订单重试机制
+
+### 交易规则
+- 采用异步报单模式
+- 使用真实价格
+- 考虑交易成本:
+  - 手续费:按照不同品种设置
+  - 滑点:按照品种特性设置
+  - 保证金:按照交易所要求设置

+ 1577 - 0
Lib/future/TrendFalseChange_v002.py

@@ -0,0 +1,1577 @@
+# 导入函数库
+from jqdata import *
+from jqdata import finance
+import pandas as pd
+from datetime import date, datetime, timedelta
+from myTT import MA
+import re
+
+# 该版本重点是要实现记录下的2024-12-28以后的11个买入
+
+# 设置以便完整打印 DataFrame
+pd.set_option('display.max_rows', None)  # 设置显示最大行数为无限制
+pd.set_option('display.max_columns', None)  # 设置显示最大列数为无限制
+pd.set_option('display.width', None)  # 设置打印宽度为无限制
+pd.set_option('display.max_colwidth', 20)  # 设置列最大宽度为无限制
+
+## 初始化函数,设定基准等等
+def initialize(context):
+    # 设定沪深300作为基准
+    set_benchmark('000300.XSHG')
+    # 开启动态复权模式(真实价格)
+    set_option('use_real_price', True)
+    # 过滤掉order系列API产生的比error级别低的log
+    # log.set_level('order', 'error')
+    # 输出内容到日志 log.info()
+    log.info('初始函数开始运行且全局只运行一次')
+
+    ### 期货相关设定 ###
+    # 设定账户为金融账户
+    set_subportfolios([SubPortfolioConfig(cash=context.portfolio.starting_cash, type='index_futures')])
+    # 期货类每笔交易时的手续费是: 买入时万分之0.23,卖出时万分之0.23,平今仓为万分之23
+    set_order_cost(OrderCost(open_commission=0.000023, close_commission=0.000023,close_today_commission=0.0023), type='index_futures')
+    # 设定保证金比例
+    # set_option('futures_margin_rate', 0.15)
+
+    # 设置期货交易的滑点
+    set_slippage(StepRelatedSlippage(2))
+    # 运行函数(reference_security为运行时间的参考标的;传入的标的只做种类区分,因此传入'IF8888.CCFX'或'IH1602.CCFX'是一样的)
+    # 注意: before_open/open/close/after_close等相对时间不可用于有夜盘的交易品种,有夜盘的交易品种请指定绝对时间(如9: 30)
+    
+    #初始化全局变量
+    g.usage_percentage = 0.8 # 最大持股数量
+    # 定义默认的保证金比例
+    g.default_margin_rates = {
+        'long': {'A': 0.07, 'AG': 0.04, 'AL': 0.05, 'AO': 0.05, 'AP': 0.08, 'AU': 0.04, 'B': 0.05,
+        'BC': 0.13, 'BR': 0.07, 'BU': 0.04, 'C': 0.07, 'CF': 0.05, 'CJ': 0.07, 'CS': 0.07,
+        'CU': 0.05, 'CY': 0.05, 'EB': 0.12, 'EC': 0.12, 'EG': 0.05, 'FG': 0.05, 'FU': 0.08,
+        'HC': 0.04, 'I': 0.1, 'J': 0.22, 'JD': 0.08, 'JM': 0.22, 
+        'L': 0.07, 'LC': 0.05, 'LH':0.1, 'LR': 0.05, 'LU': 0.15, 'M': 0.07, 'MA': 0.05, 'NI': 0.05, 'NR': 0.13, 'OI': 0.05,
+        'P': 0.05, 'PB': 0.05, 'PF': 0.1, 'PG': 0.05, 'PK': 0.05,
+        'PP': 0.07, 'RB': 0.05, 'RI': 0.05, 'RM': 0.05, 'RU': 0.05,
+        'SA': 0.05, 'SC': 0.12, 'SF': 0.05, 'SH': 0.05, 'SI': 0.13, 'SM': 0.05, 'SN': 0.05, 'SP': 0.1, 'SR': 0.05,
+        'SS': 0.05, 'TA': 0.05, 'UR': 0.09, 'V': 0.07,
+        'Y': 0.05, 'ZC': 0.05, 'ZN': 0.05}, 
+        'short': {'A': 0.07, 'AG': 0.04, 'AL': 0.05, 'AO': 0.05, 'AP': 0.08, 'AU': 0.04, 'B': 0.05,
+        'BC': 0.13, 'BR': 0.07, 'BU': 0.04, 'C': 0.07, 'CF': 0.05, 'CJ': 0.07, 'CS': 0.07,
+        'CU': 0.05, 'CY': 0.05, 'EB': 0.12, 'EC': 0.12, 'EG': 0.05, 'FG': 0.05, 'FU': 0.08,
+        'HC': 0.04, 'I': 0.1, 'J': 0.22, 'JD': 0.08, 'JM': 0.22, 
+        'L': 0.07, 'LC': 0.05, 'LH':0.1, 'LR': 0.05, 'LU': 0.15, 'M': 0.07, 'MA': 0.05, 'NI': 0.05, 'NR': 0.13, 'OI': 0.05,
+        'P': 0.05, 'PB': 0.05, 'PF': 0.1, 'PG': 0.05, 'PK': 0.05,
+        'PP': 0.07, 'RB': 0.05, 'RI': 0.05, 'RM': 0.05, 'RU': 0.05,
+        'SA': 0.05, 'SC': 0.12, 'SF': 0.05, 'SH': 0.05, 'SI': 0.13, 'SM': 0.05, 'SN': 0.05, 'SP': 0.1, 'SR': 0.05,
+        'SS': 0.05, 'TA': 0.05, 'UR': 0.09, 'V': 0.07,
+        'Y': 0.05, 'ZC': 0.05, 'ZN': 0.05}
+    }
+    g.multiplier = {
+        'A': 10, 'AG': 15, 'AL': 5, 'AO': 20, 'AP': 10, 'AU': 1000, 'B': 10,
+        'BC': 5, 'BR': 5, 'BU': 10, 'C': 10, 'CF': 5, 'CJ': 5, 'CS': 10,
+        'CU': 5, 'CY': 5, 'EB': 5, 'EC': 50, 'EG': 10, 'FG': 20, 'FU': 10,
+        'HC': 10, 'I': 100, 'J': 60, 'JD': 5, 'JM': 100, 
+        'L': 5, 'LC': 1, 'LH':16, 'LR': 0.05, 'LU': 10, 'M': 10, 'MA': 10, 'NI': 1, 'NR': 10, 'OI': 10,
+        'P': 10, 'PB': 5, 'PF': 5, 'PG': 20, 'PK': 5,
+        'PP': 5, 'RB': 10, 'RI': 0.05, 'RM': 10, 'RU': 10,
+        'SA': 20, 'SC': 1000, 'SF': 5, 'SH': 30, 'SI': 5, 'SM': 5, 'SN': 1, 'SP': 10, 'SR': 10,
+        'SS': 5, 'TA': 5, 'UR': 20, 'V': 5,
+        'Y': 10, 'ZC': 0.05, 'ZN': 5}  # 假设所有期货合约的乘数为固定值,如果不同合约有不同的乘数,需要进一步调整
+    g.default_days = 10 # 判断趋势线的最小天数,某条K线连续这个天数大于收盘价则该K线为压力线;小于则是支撑线
+    g.continuous_days_length = 5  # 破趋势后的观察天数
+    g.change_direction_days = 5 # 检查是某个天数范围内有多少次均线穿过
+    g.crossed_symbols_history = {}  # 用于存储过去几天的穿越信息
+    g.trade_history = {} # 初始化交易记录
+    g.change_fail_history = {} # 初始化换月建仓失败的记录
+    # 开盘时运行
+    # run_daily(market_open, time='21:01', reference_security='IF8888.CCFX')
+    # run_daily(market_open, time='09:01', reference_security='IF8888.CCFX')
+    g.high_low_ma_relations = {}
+    
+    # 临时止损检查
+    run_daily(loss_control, time='21:15:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='21:45:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='22:15:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='22:45:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='09:15:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='09:45:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='10:15:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='10:45:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='11:15:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='13:15:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='13:45:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='14:15:00', reference_security='IF1808.CCFX')
+    run_daily(loss_control, time='14:56:00', reference_security='IF1808.CCFX')
+
+    # 收盘前运行
+    run_daily(before_market_close, time='14:55', reference_security='IF8888.CCFX')
+
+############################ 主程序中执行函数 ###################################
+#0-0 开盘前运行函数
+def market_open(context):
+    # 输出运行时间
+    # log.info('函数运行时间(before_market_open): '+str(context.current_dt.time()))
+
+    # 给微信发送消息(添加模拟交易,并绑定微信生效)
+    # send_message('美好的一天~')
+    
+    # 获取当下该品种有夜盘和无夜盘主力期货合约
+    potential_night_list = ['AG', 'AL', 'AU', 'BU', 'CU', 'FU', 'HC', 'NI', 'PB', 'RB', 'RU', 'SN', 'ZN', 'SP', 'SS', \
+    'CF', 'CY', 'FG', 'JR', 'LR', 'MA', 'PM', 'RI', 'RM', 'OI', 'SR', 'TA', 'ZC', 'WH', \
+    'WT', 'SA', 'PF', 'A', 'B', 'BB', 'C', 'CS', 'I', 'J', 'JM', 'L', 'M', 'P', 'PP', \
+    'V', 'Y', 'EG', 'EB', 'PG']
+    potential_day_list = ['SF', 'SM', 'UR', 'AP', 'CJ', 'PK', 'FB', 'JD', 'LH', 'SI', 'LC']
+    # potential_night_list = ['OI']
+    # potential_day_list = ['UR']
+    
+    if str(context.current_dt.time())[:2] == '21':
+        potential_icon_list = potential_night_list
+    else:
+        potential_icon_list = potential_day_list
+        
+    potential_future_list = []
+    for i in potential_icon_list:
+        domaint_future = get_dominant_future(i)
+        if len(domaint_future) > 0:
+            potential_future_list.append(domaint_future)
+    # c_data = get_current_data()
+    
+    target_list = []
+    for future in potential_future_list:
+        y_data = attribute_history(future, 2, '1m', ['close'])
+        yesterday_close = y_data.iloc[0,0]
+        # c_data = attribute_history(future, 1, '1m', ['open'])
+        today_open = y_data.iloc[-1,0]
+        # print(future, y_data)
+        open_change_rate = (today_open - yesterday_close)/yesterday_close
+        if open_change_rate >= 0.03:
+            target_list.append(future)
+            # print('future: ', future, today_open, yesterday_close, open_change_rate)
+            # print(today_open, yesterday_close, open_change_rate)
+            # print(y_data)
+    
+    if len(target_list) >= 5:
+        value = context.portfolio.cash * g.usage_percentage / len(target_list)
+        for future in target_list:
+            open_position(future, value, 'short')
+    elif len(target_list) > 0 and len(target_list) < 5:
+        value = context.portfolio.cash * g.usage_percentage / 5
+        for future in target_list:
+            open_position(future, value, 'short')
+
+#0-1 判断是否买入函数  
+def before_market_close(context):
+    print("-"*20 + "New day ending!" + "-"*20)
+    if len(g.trade_history) > 0:
+        print(f'in the main - g.trade_history: {g.trade_history}, g.change_fail_history: {g.change_fail_history}')
+    # loss_control(context)
+    trend_symbols, daily_data_info = check_trend(context)
+    print_list_elements("in the main - trend_symbols", trend_symbols)
+    crossed_symbols = check_shadow_cross(context, trend_symbols, daily_data_info)
+    print_list_elements("in the main - crossed_symbols", crossed_symbols)
+    buy_symbols = check_buy_condition(context, crossed_symbols)
+    print_list_elements("in the main - buy_symbols", buy_symbols)
+    
+    subportfolio = context.subportfolios[0]
+    hold_symbols = set(subportfolio.long_positions.keys()) | set(subportfolio.short_positions.keys())
+    # print(f'hold_symbols: {hold_symbols}')
+    
+    for symbol, line_label, line_type, direction in buy_symbols:
+        if check_symbol_prefix_match(symbol, hold_symbols):
+            print(f'Have similar symbol in hold_symbols: {hold_symbols} and symbol: {symbol}, stop the deal')
+        else: 
+            print(f'No similar symbol in hold_symbols: {hold_symbols} and symbol: {symbol}, continue the deal')
+            value_to_invest = calculate_order_value(context, symbol, direction)
+            open_position(symbol, value_to_invest, direction, line_label)
+
+#0-2 止盈止损
+def loss_control(context):
+    print("-" * 20 + "loss control" + "-" * 20)
+    # 先检查是否有正常的夜盘
+    now = context.current_dt.time()
+    now_hour = now.hour
+    if now_hour >= 21:
+        test_future = get_dominant_future("A")
+        test_data = attribute_history(test_future, 1, '1m', 'close')
+        test_hour = test_data.index[0].hour
+        if test_hour <= 15:
+            print(f'最近的数据小时为: {test_hour},该夜盘不存在,直接停止')
+            return
+    # 先把换月买入失败的重新买入放在这里
+    if len(g.change_fail_history) > 0:
+        print(f'检查换月失败的{g.change_fail_history}')
+        for symbol in g.change_fail_history.keys():
+            direction = g.change_fail_history[symbol]['direction']
+            print(f'检查换月失败的direction: {direction}')
+            value_to_invest = calculate_order_value(context, symbol, direction)
+            open_position(symbol, value_to_invest, g.trade_history[symbol]["direction"], g.trade_history[symbol]["line_label"])
+    
+    # 检查损失
+    target_time = datetime.strptime('14:55:00', '%H:%M:%S').time()
+    potential_future_list = get_potential_future_list(context)
+    # print(f'check g.trade_history: {g.trade_history}, potential_future_list: {potential_future_list}')
+    # print(f'current_hour: {current_hour}')
+    # 遍历所有持仓
+    long_positions_dict = context.portfolio.long_positions
+    for position in list(long_positions_dict.values()):  
+        # print("security:{0}, position:{1}, value:{2}, time:{3},latest price:{4},cost:{5}".format(position.security, position.total_amount, position.value, position.init_time, position.price, position.acc_avg_cost))
+        # 检查是否达到损失限制
+        if position.security in potential_future_list:
+            # print(f'for long position, {position.security} in the potential_future_list: {potential_future_list}')
+            if check_loss_for_close(context, position, 'long'):
+                continue  # 如果已平仓,则跳过后续检查
+            
+            # 检查基于动态跟踪线的平仓条件
+            if now > target_time:
+                # print("Now {now} is after 14:55:00")
+                check_ma_for_close(context, position, 0.01, 4)
+            else:
+                # print("Now {now} is before or equal to 14:55:00")
+                check_ma_for_close(context, position, 0.003, 4)
+    # print(type(context.subportfolios[0].short_positions))
+    short_positions_dict = context.portfolio.short_positions
+    for position in list(short_positions_dict.values()):  
+        # print("security:{0}, position:{1}, value:{2}, time:{3},latest price:{4},cost:{5}".format(position.security, position.total_amount, position.value, position.init_time, position.price, position.acc_avg_cost))
+        # 检查是否达到损失限制
+        if position.security in potential_future_list:
+            # print(f'for short position, {position.security} in the potential_future_list: {potential_future_list}')
+            if check_loss_for_close(context, position, 'short'):
+                continue  # 如果已平仓,则跳过后续检查
+            
+            # 检查基于动态跟踪线的平仓条件
+            if now >= target_time:
+                # print("Now {now} is after 14:55:00")
+                check_ma_for_close(context, position, 0.01, 4)
+            else:
+                # print("Now {now} is before or equal to 14:55:00")
+                check_ma_for_close(context, position, 0.003, 4)
+    
+#0-3 收盘后运行函数
+def after_market_close(context):
+    log.info(str('函数运行时间(after_market_close):'+str(context.current_dt.time())))
+    # 得到当天所有成交记录
+    trades = get_trades()
+    for _trade in trades.values():
+        log.info('成交记录: '+str(_trade))
+    log.info('A day ends')
+    log.info('##############################################################')
+
+############################ 买卖函数 ###################################
+#1-0 交易模块-自定义下单
+#报单成功返回报单(不代表一定会成交),否则返回None,应用于
+def order_target_value_(security, value, direction):
+	if value == 0:
+		log.debug("Selling out %s" % (security))
+	else:
+		log.debug("Order %s to value %f" % (security, value))
+	# 如果股票停牌,创建报单会失败,order_target_value 返回None
+	# 如果股票涨跌停,创建报单会成功,order_target_value 返回Order,但是报单会取消
+	# 部成部撤的报单,聚宽状态是已撤,此时成交量>0,可通过成交量判断是否有成交
+	return order_target_value(security, value, side=direction)
+
+#1-1 交易模块-开仓
+#买入指定价值的证券,报单成功并成交(包括全部成交或部分成交,此时成交量大于0)返回True,报单失败或者报单成功但被取消(此时成交量等于0),返回False
+def open_position(security, value, direction, line_label):
+	order = order_target_value_(security, value, direction)
+    # print(f'order: {order}') # 查看订单的信息
+	if order != None and order.filled > 0:
+	    print(f'Make an order of {security} with {line_label} and {direction}')
+	    g.trade_history[security] = {
+	        'entry_price': get_current_data()[security].last_price,
+	        'position_value': value,
+	        'direction': direction,
+	        'line_label': line_label,
+	        'finish_time': order.finish_time
+	    }
+	    return True
+	return False
+
+#1-2 交易模块-平仓
+#卖出指定持仓,报单成功并全部成交返回True,报单失败或者报单成功但被取消(此时成交量等于0),或者报单非全部成交,返回False
+def close_position(position, direction):
+	security = position.security
+	order = order_target_value_(security, 0, direction)  # 可能会因停牌失败
+	if order != None:
+		if order.status == OrderStatus.held and order.filled == order.amount:
+		    # g.sold_future[security] = 0 # 避免连续重复买入
+			# 如果成功平仓,从交易历史中移除该标的
+		    if security in g.trade_history:
+		        del g.trade_history[security]
+		      #  print(f'after deal, g.trade_history: {g.trade_history}')
+		    return True
+	
+	return False
+	
+#1-3 根据标的情况开仓
+def open_position_by_margin(context, security, direction):
+    value_to_invest = calculate_order_value(context, security, direction)
+    order_value(security, value_to_invest, side=direction, pindex=0)
+
+############################ 判断买卖条件的函数 ###################################
+#2-0 找到可以交易的目标期货标的主连
+def get_potential_future_list(context):
+    # potential_night_list = ['AG', 'AL', 'AU', 'BU', 'CU', 'FU', 'HC', 'NI', 'NR', 'PB', 'RB', 'RU', 'SN', 'ZN', 'SP', 'SS', \
+    # 'CF', 'CY', 'FG', 'LR', 'MA', 'PM', 'RI', 'RM', 'OI', 'SC', 'SH','SR', 'TA', 'ZC', \
+    # 'SA', 'PF', 'A', 'AO', 'B', 'BC', 'BR', 'C', 'CS', 'I', 'J', 'JM', 'L', 'M', 'P', 'PP', \
+    # 'V', 'Y', 'EG', 'EB', 'PG']
+    # potential_day_list = ['EC', 'SF', 'SM', 'UR', 'AP', 'CJ', 'PK', 'JD', 'LH', 'SI', 'LC']
+    # potential_night_list = ['RM']
+    # potential_day_list = ['']
+    potential_night_list = ['NI', 'CF', 'PF', 'Y', 'M', 'B', 'SN', 'RM']
+    potential_day_list = ['JD', 'UR']
+    
+    if str(context.current_dt.time())[:2] in ('21', '22'):
+        potential_icon_list = potential_night_list
+    else:
+        potential_icon_list = potential_day_list + potential_night_list
+    
+    # print(f'potential_icon_list: {potential_icon_list}')
+    potential_future_list = []
+    for i in potential_icon_list:
+        dominant_future = get_dominant_future(i)
+        # print(f'i: {i}, dominant_future: {dominant_future}')
+        if len(dominant_future) > 0:
+            potential_future_list.append(dominant_future)
+    
+    return potential_future_list
+    
+#2-1 检查品类的主连是否满足形成趋势的条件
+def check_trend(context):
+    print("-" * 20 + "Check trends" + "-" * 20)
+    trend_symbols = []
+    daily_data_info = {}  # 用于保存每日数据信息
+    
+    # 获取可以交易的所有标的主连
+    potential_future_list = get_potential_future_list(context)
+    # print(f'potential_future_list: {potential_future_list}')
+    potential_future_list = [item for item in potential_future_list if item not in g.trade_history.keys()] # 移除已有持仓的标的
+    # print(f'potential_future_list: {potential_future_list}')
+    
+    # 针对所有标的需要的基础数据
+    ma_crosses_dict = {} # 用于存储某个symbol过去一段时间均线交叉的数量
+    for symbol in potential_future_list:
+        # 获取50天的收盘价数据
+        close_data = attribute_history(symbol, 50, '1d', ['close', 'high', 'low', 'open'], df=True)
+        close_series = close_data['close']
+        # print(f'Check if future: {symbol} - {close_series}')
+
+        # 计算5日、10日、20日和30日移动平均线
+        ma5 = close_series.rolling(window=5).mean()
+        ma10 = close_series.rolling(window=10).mean()
+        ma20 = close_series.rolling(window=20).mean()
+        ma30 = close_series.rolling(window=30).mean()
+
+        trend_info = {'symbol': symbol, 'trend_lines': []}
+        daily_data = []  # 存储50日的数据
+
+        # 初始化连续天数计数器
+        continuous_days = {
+            'above_ma5': [0] * g.continuous_days_length,
+            'below_ma5': [0] * g.continuous_days_length,
+            'above_ma10': [0] * g.continuous_days_length,
+            'below_ma10': [0] * g.continuous_days_length,
+            'above_ma20': [0] * g.continuous_days_length,
+            'below_ma20': [0] * g.continuous_days_length,
+            'above_ma30': [0] * g.continuous_days_length,
+            'below_ma30': [0] * g.continuous_days_length
+        }
+        # 获取连续在某一个均线上或者下的天数
+        for i in range(len(close_series)):
+            # 针对每一天更新连续天数计数器
+            symbol_prefix = symbol[:-9]
+            if symbol_prefix == 'RM':
+                # log.debug(f'start updating trend details for {symbol}')
+                update_continuous_days(continuous_days, close_series[i], ma5[i], ma10[i], ma20[i], ma30[i], g.continuous_days_length, True)
+            else:
+                update_continuous_days(continuous_days, close_series[i], ma5[i], ma10[i], ma20[i], ma30[i], g.continuous_days_length)
+
+            # 收集每日数据
+            day_data = {
+                'date': close_data.index[i].date(),
+                'close': close_series[i],
+                'high': close_data['high'][i],
+                'low': close_data['low'][i],
+                'open': close_data['open'][i],
+                'ma5': ma5[i],
+                'ma10': ma10[i],
+                'ma20': ma20[i],
+                'ma30': ma30[i],
+                'continuous_above_ma5': continuous_days['above_ma5'],
+                'continuous_below_ma5': continuous_days['below_ma5'],
+                'continuous_above_ma10': continuous_days['above_ma10'],
+                'continuous_below_ma10': continuous_days['below_ma10'],
+                'continuous_above_ma20': continuous_days['above_ma20'],
+                'continuous_below_ma20': continuous_days['below_ma20'],
+                'continuous_above_ma30': continuous_days['above_ma30'],
+                'continuous_below_ma30': continuous_days['below_ma30']
+            }
+            daily_data.append(day_data)
+        
+        # 在处理完所有数据后检查是否有特殊的趋势变化情况出现
+        # adjust_for_special_condition(daily_data, g.continuous_days_length)
+        daily_data_info[symbol] = daily_data
+        # print(f'checking daily data for {symbol}')#: {daily_data}')
+        
+        # 检查过去一定天数内均线的相交次数,如果太多不会被认定为形成趋势
+        # print(f'calculate {symbol}') # ma cross numbers with daily_data_info: {daily_data_info[symbol]}')
+        ma_crosses_dict[symbol] = count_ma_crosses(daily_data_info[symbol], g.change_direction_days)
+                
+        # 检查哪些均线会形成什么类型的趋势
+        for ma_type in ['ma5', 'ma10', 'ma20', 'ma30']:
+            above_days = continuous_days[f'above_{ma_type}']
+            below_days = continuous_days[f'below_{ma_type}']
+        
+            # print(f"Checking {ma_type} for {symbol}, above_days: {above_days}, below_days: {below_days}, g.default_days: {g.default_days}")
+        
+            above_condition_result = [day >= g.default_days for day in above_days]
+            below_condition_result = [day >= g.default_days for day in below_days]
+        
+            # print(f"above_condition_result: {above_condition_result}, below_condition_result: {below_condition_result}")
+        
+            above_condition = any(above_condition_result)
+            below_condition = any(below_condition_result)
+            # print(f"above_condition: {above_condition}, below_condition: {below_condition}")
+        
+            if above_condition:
+                # print(f"Adding {ma_type} support for {symbol}")
+                trend_info['trend_lines'].append((ma_type, 'support'))
+        
+            if below_condition:
+                # print(f"Adding {ma_type} resistance for {symbol}")
+                trend_info['trend_lines'].append((ma_type, 'resistance'))
+
+        if trend_info['trend_lines']:
+            trend_symbols.append(trend_info)
+    
+    # 去除掉在一段时间内均线过于频繁交叉的对象
+    valid_trend_symbols = []
+    # print(f'before checking special circumstances, trend_symbols: {trend_symbols}')
+    
+    for trend_info in trend_symbols:
+        # print(f'find trend_info: {trend_info["symbol"]}')
+        symbol = trend_info["symbol"]
+        ma_crosses = ma_crosses_dict[symbol]
+        # log.debug(f'symbol: {symbol}, ma_crosses: {ma_crosses}')
+        
+        if ma_crosses <= 3:
+            valid_trend_symbols.append(trend_info)
+    
+    # print(f'valid_trend_symbols: {valid_trend_symbols}')
+                
+    # 使用有效的趋势信息更新 trend_symbols
+    trend_symbols = valid_trend_symbols
+    
+    # 创建一个集合来存储所有出现在 trend_info 中的标的
+    trend_symbols_set = set([info['symbol'] for info in trend_symbols])
+    log.debug(f"trend_symbols: {trend_symbols}, trend_symbols_set: {trend_symbols_set}, valid_trend_symbols: {valid_trend_symbols}")
+    
+    # 打印40日的数据和连续天数信息
+    # print(f"trend_symbols: {trend_symbols}, trend_symbols_set: {trend_symbols_set}") #, daily_data_info:{daily_data_info}")
+    for symbol, daily_data in daily_data_info.items():
+        # 只打印出现在 trend_info 中的标的的数据
+        if symbol in trend_symbols_set:
+            # 查找与当前 symbol 对应的 trend_info
+            current_trend_info = next((item for item in trend_symbols if item['symbol'] == symbol), None)
+    
+            # 打印 symbol 和其趋势信息
+            trend_lines_str = ', '.join([f"{line[0]}: {line[1]}" for line in current_trend_info['trend_lines']]) if current_trend_info else "无趋势"
+            # print(f"{symbol} 的趋势情况为: {trend_lines_str},满足条件的每日数据:")
+
+            # print(f"{symbol} 的趋势情况为: ,满足条件的每日数据:")
+            # print(f"daily_data: {daily_data[-1]}")
+            # for data in daily_data:
+                # print(f"  日期: {data['date']}, 收盘价: {data['close']}, MA5: {data['ma5']}, MA10: {data['ma10']}, MA20: {data['ma20']}, \
+                # 连续高于MA5: {data['continuous_above_ma5']}, 连续低于MA5: {data['continuous_below_ma5']}, \
+                # 连续高于MA10: {data['continuous_above_ma10']}, 连续低于MA10: {data['continuous_below_ma10']}, \
+                # 连续高于MA20: {data['continuous_above_ma20']}, 连续低于MA20: {data['continuous_below_ma20']}")
+                # continue
+
+    return valid_trend_symbols, daily_data_info
+
+#2-2 检查行程趋势的主连的趋势是否发生变化
+def check_shadow_cross(context, trend_symbols, daily_data_info):
+    print("-" * 20 + "Check shadow cross" + "-" * 20)
+    
+    # 1. 检查并进行换月
+    current_day = context.current_dt.date()  # 获取当前日期字符串 
+    # print(f'check current_day: {current_day}')
+    switch_result = position_auto_switch(context)
+    if switch_result:
+        print(f'switch_result: {switch_result}, g.trade_history: {g.trade_history}, g.change_fail_history: {g.change_fail_history}')
+    temp_crossed_symbols = {}
+    today_crossed_symbols = {}
+    
+    # 1. 检查并清理过时的穿越信息
+    # for symbol in list(g.crossed_symbols_history.keys()):
+        # 保留最近 g.continuous_days_length 天的数据
+        # print(f'crossed_symbols_history: {g.crossed_symbols_history}')
+        # g.crossed_symbols_history[symbol] = {date: crosses for date, crosses in g.crossed_symbols_history[symbol].items() if (context.current_dt - datetime.strptime(date, '%Y-%m-%d')).days <= g.continuous_days_length}
+        # if not g.crossed_symbols_history[symbol]:
+        #     del g.crossed_symbols_history[symbol]  # 如果没有数据,删除该标的的记录
+
+    # 2. 根据当天的数据更新g.crossed_symbols_history
+    for trend_info in trend_symbols:
+        symbol = trend_info['symbol']
+        # print(f'Checking data for {symbol}')
+
+        # 获取今天的最新数据
+        # 计算并更新今天的完整数据
+        today_daily_data_info = daily_data_info.copy()
+        latest_data_today = get_latest_trading_data(symbol, context)
+        print(f'{symbol} latest_data_today: {latest_data_today}')
+        
+        # 计算并更新今天的完整数据
+        latest_data = calculate_continuous_data(latest_data_today, today_daily_data_info, symbol, g.continuous_days_length)
+        
+        # 根据昨天的价格判断有没有发生突破
+        yesterday_data = daily_data_info[symbol][-1]
+        print(f'find {symbol} yesterday_data: {yesterday_data}')
+
+        # 临时存储当前标的的穿越信息
+        symbol_crosses = []
+
+        for line_label, line_type in trend_info['trend_lines']:
+            print(f'line_label in change: {line_label}')
+            yesterday_day = yesterday_data['date']
+            close = yesterday_data['close']
+            high = yesterday_data['high']
+            low = yesterday_data['low']
+            ma_value = yesterday_data[line_label]
+            close_ma_difference = (close - ma_value) / ma_value
+            
+            # 删除指定的键
+            yesterday_data_copy = yesterday_data.copy()
+            # print(f'check yesterday_data_copy before changing {yesterday_data_copy}')
+            keys_to_remove = ['continuous_above_ma5', 'continuous_below_ma5', 'continuous_above_ma10', 'continuous_below_ma10', 'continuous_above_ma20', 'continuous_below_ma20', 'continuous_above_ma30', 'continuous_below_ma30']
+            for key in keys_to_remove:
+                yesterday_data_copy.pop(key, None)  # 使用pop方法删除键,如果键不存在则不做任何操作
+            # print(f'check yesterday_data_copy after changing {yesterday_data_copy}')
+
+            # 判断上一个交易日完整的数据中是否穿越
+            if (line_type == 'support' and low < ma_value and close_ma_difference <= 0.0005) or (line_type == 'resistance' and high > ma_value and close_ma_difference >= -0.0005):
+                symbol_crosses.append({
+                    'date': yesterday_day,
+                    'symbol': symbol,
+                    'line_label': line_label,
+                    'line_type': line_type,
+                    'direction': 'long' if line_type == 'support' else 'short',
+                    'latest_data': yesterday_data_copy
+                })
+                print(f"{symbol} crossed {line_label} ({line_type}) with ma_value: {ma_value} and close: {close}  and high:{high} and low: {low} and close_ma_difference: {close_ma_difference}, latest_data: {yesterday_data_copy}")
+
+        # 从符合条件的趋势线中选择数字较大的(即优先保留ma10而不是ma5)
+        # print(f'check symbol_crosses: {symbol_crosses}')
+        if symbol_crosses:
+            # today_crossed_symbols.append(symbol) # 记录一下g.crossed_symbols_history已经更新过的symbol
+            # 如果有穿越,选择数字较大的趋势
+            best_cross = max(symbol_crosses, key=lambda x: int(''.join(filter(str.isdigit, x['line_label']))))
+            temp_crossed_symbols[symbol] = best_cross
+            
+    # 3.1 针对没有更新过的g.crossed_symbols_history的symbol进行更新
+    # print(f'check temp_crossed_symbols: {temp_crossed_symbols}, g.crossed_symbols_history: {g.crossed_symbols_history}')
+    if temp_crossed_symbols:
+        # 首先是针对今天出现趋势突破的标的进行历史记录的更新
+        for symbol, best_cross in temp_crossed_symbols.items():
+            # print(f'更新历史symbol: {symbol}, best_cross: {best_cross}')
+            update_crossed_symbols_history(context, best_cross)
+            print(f'针对也在今天出现突破的{symbol}完成g.crossed_symbols_history的更新,{g.crossed_symbols_history[symbol]}')
+
+    #3.2 要针对那些不在今天出现突破的标的但是还在历史趋势里的进行更新,首先将两个字典的键转换成集合
+    keys_temp_crossed_symbols = set(temp_crossed_symbols.keys())
+    keys_g_crossed_symbols_history = set(g.crossed_symbols_history.keys())
+    
+    #3.2.1 找出在g.crossed_symbols_history中但不在temp_crossed_symbols中的键
+    difference_keys = keys_g_crossed_symbols_history - keys_temp_crossed_symbols
+    print(f"Keys that are in g.crossed_symbols_history: {keys_g_crossed_symbols_history} but not in temp_crossed_symbols: {keys_g_crossed_symbols_history}; {difference_keys}")
+    
+    for symbol in difference_keys: 
+        update_record_for_existing_symbol(symbol, 'new_symbol')
+        
+    #3.2.2 找出在在g.crossed_symbols_history和temp_crossed_symbols中都有的key,但是temp里的line_label要小
+    keys_in_both = keys_temp_crossed_symbols & keys_g_crossed_symbols_history
+    for symbol in keys_in_both:
+        print(f'{symbol}, {keys_temp_crossed_symbols}, {keys_g_crossed_symbols_history}, temp_crossed_symbols: {temp_crossed_symbols}')
+        temp_line_label = temp_crossed_symbols[symbol]['line_label']
+        g_line_label = g.crossed_symbols_history[symbol][0]['line_label']
+        
+        temp_line_num = int(temp_line_label.replace('ma', ''))
+        g_line_num = int(g_line_label.replace('ma', ''))
+        
+        # 检查是否需要更新
+        if g_line_num > temp_line_num:
+            update_record_for_existing_symbol(symbol, 'old_symbol')
+    
+    #4. 针对出现趋势变化的检查均线顺序、破趋势天数、收盘价和均线的最高点关系
+    for symbol, records in g.crossed_symbols_history.items():
+        latest_record = max(records, key=lambda x: x['date'])
+        print(f'symbol: {symbol}, records: {records}, latest_record: {latest_record}')
+        # 检查均线得满足一定的条件,以及破趋势的天数和其中收盘价和均线的最高差别
+        relation_check = check_ma_relations(context, symbol, False)
+        duration_check, ma_close = check_cross_details(context, symbol, False)
+        
+        # 只有均线顺序、破趋势天数、收盘价和均线的最高点关系满足条件,才会被加入购买真正的突破趋势清单
+        if relation_check and duration_check and ma_close:
+            print(f'{symbol}满足均线顺序、破趋势天数、收盘价和均线的最高点关系满足条件,才会被加入购买真正的突破趋势清单这三项条件,被视为合格标的')
+            today_crossed_symbols[symbol] = [latest_record]
+    print(f'today_crossed_symbols: {today_crossed_symbols}')
+
+    latest_records = {}
+    # print(f'最后获取还在历史记录里的g.crossed_symbols_history: {g.crossed_symbols_history}')
+    for symbol, records in g.crossed_symbols_history.items():
+        # 找到日期最大的记录
+        latest_record = max(records, key=lambda x: x['date'])
+        # 将找到的记录添加到新字典中
+        latest_records[symbol] = [latest_record]
+    # print(f'latest_records: {len(latest_records)} & {latest_records}, g.crossed_symbols_history: {len(g.crossed_symbols_history)} & {g.crossed_symbols_history}')
+            
+    return [item for sublist in today_crossed_symbols.values() for item in sublist]
+
+#2-3 检查趋势发生变化的主连是否满足买入条件
+def check_buy_condition(context, crossed_symbols, shadow_body_ratio_threshold=0.5):
+    print("-" * 20 + "Check buy condition" + "-" * 20)
+    buy_symbols = []
+    update_margin_rate(context)
+
+    for crossed_symbol in crossed_symbols:
+        # print(f'crossed_symbol: {crossed_symbol}')
+        symbol = crossed_symbol['symbol']
+        line_label = crossed_symbol['line_label']
+        line_type = crossed_symbol['line_type']
+        histroical_data = calculate_daily_data(context, 50, symbol)
+        latest_data_today = histroical_data.iloc[-1]
+        latest_data = {
+            'date': latest_data_today.name,  # 使用索引的 name 属性获取日期
+            'close': latest_data_today['close'],
+            'high': latest_data_today['high'],
+            'low': latest_data_today['low'],
+            'open': latest_data_today['open'],
+            'ma5': latest_data_today['ma5'],
+            'ma10': latest_data_today['ma10'],
+            'ma20': latest_data_today['ma20'],
+            'ma30': latest_data_today['ma30']
+        }
+
+        # 计算并更新今天的完整数据
+        # latest_data = crossed_symbol['latest_data']
+        print(f'check latest_data_today: {latest_data_today}, latest_data: {latest_data}')
+        
+        # 获取突破趋势天数,用来判断突破趋势天数和突破均线的关系
+        all_records = g.crossed_symbols_history[symbol]
+        if not all_records:
+            debug_print(f"No records found for {symbol}.")
+            return False, False
+        first_day = all_records[0]['date']
+        today = context.current_dt.date()
+        all_days = get_trade_days(first_day, today)
+        cross_length = len(all_days)
+        ma_lines_in_range, ma_labels_in_range = count_ma_lines_in_range(symbol, latest_data)
+        diff_days_lines = cross_length - ma_lines_in_range
+        print(f'check diff_days_lines: {diff_days_lines}, cross_length: {cross_length}, latest_data: {latest_data}')
+
+        # 直接使用 latest_data,无需再次更新 MA 关系
+        ma_value = latest_data[line_label]
+        close_price = latest_data['close']
+        today_in_change = (latest_data['close'] - latest_data['open'])/latest_data['open']
+        print(f'symbol: {symbol}, close_price: {close_price}, 1.005: {1.005*ma_value}, 1.02: {1.02*ma_value}, 0.98: {0.98*ma_value}, 0.995: {0.995*ma_value}, today_in_change: {today_in_change}')
+
+        if line_label != "ma5":
+            if line_type == 'support' and  1.005*ma_value <= close_price <= 1.02*ma_value and today_in_change >= -0.002:
+                buy_symbols.append((symbol, line_label, 'support', 'long'))
+            elif line_type == 'resistance' and 0.98*ma_value <= close_price <= 0.995*ma_value and today_in_change <= 0.002:
+                buy_symbols.append((symbol, line_label, 'resistance', 'short'))
+
+    return buy_symbols
+
+#2-4 根据均线止损
+def check_ma_for_close(context, position, offset_ratio, days_for_adjustment):
+    # print("-" * 20 + "MA closing check" + "-" * 20)
+
+    security = position.security
+    if security not in g.trade_history:
+        # print(f'security: {security} not in trade history')
+        return False
+    
+    trade_info = g.trade_history.get(position.security, {})
+    finish_time = trade_info.get('finish_time')
+    line_label = trade_info.get('line_label')
+    
+    # 获取所有交易日并计算持仓的交易日数
+    all_trade_days = get_all_trade_days()
+    holding_days = 0
+    if finish_time:
+        finish_date = finish_time.date()
+        current_date = context.current_dt.date()
+        holding_days = sum((finish_date <= d <= current_date) for d in all_trade_days)
+    
+    # 计算变化率
+    today_price = get_current_data()[position.security].last_price
+    avg_daily_change_rate = calculate_average_daily_change_rate(position.security)
+    historical_data = attribute_history(position.security, 1, '1d', ['close'])
+    yesterday_close = historical_data['close'].iloc[-1]
+    today_change_rate = abs((today_price - yesterday_close) / yesterday_close)
+
+    # 检查是否符合直接使用 ma5 和 ma10 规则的条件
+    close_line = None
+    if today_change_rate >= 1.5 * avg_daily_change_rate:
+        close_line = 'ma5'
+    elif holding_days <= days_for_adjustment:
+        close_line = line_label
+    else:
+        close_line = 'ma5' if today_change_rate >= 1.2 * avg_daily_change_rate else 'ma10'
+    
+    # 计算 MA 值并进行平仓判断
+    ma_values = calculate_ma_values(position.security, [5, 10, 20, 30])
+    adjusted_ma_value = ma_values[close_line] * (1 + offset_ratio if position.side == 'short' else 1 - offset_ratio)
+    # print(f'security: {position.security}, close_line: {close_line}, today_price: {today_price}, ma_price: {ma_values[close_line]}, check_point: {adjusted_ma_value}, offset_ratio: {offset_ratio}')
+
+    if (position.side == 'long' and today_price < adjusted_ma_value) or \
+       (position.side == 'short' and today_price > adjusted_ma_value):
+        # print(f"Closing position in {position.security} based on {close_line} with position_side: {position.side} and adjusted_ma_value: {adjusted_ma_value}")
+        close_position(position, g.trade_history[position.security]['direction'])
+        return True
+
+    return False
+    
+#2-5 根据最大止损线进行止损
+def check_loss_for_close(context, position, direction, initial_loss_limit=-4000, loss_increment_per_day=200):
+    print("-" * 20 + "Last line control" + "-" * 20)
+    
+    trade_info = g.trade_history.get(position.security, {})
+    finish_time = trade_info.get('finish_time')
+    
+    # 获取所有交易日并计算持仓的交易日数
+    all_trade_days = get_all_trade_days()
+    holding_days = 0
+    if finish_time:
+        finish_date = finish_time.date()
+        current_date = context.current_dt.date()
+        holding_days = sum((finish_date <= d <= current_date) for d in all_trade_days)
+        
+    # 调整损失限制
+    adjusted_loss_limit = initial_loss_limit + holding_days * loss_increment_per_day
+    # print(f"Adjusted Loss Limit for {position.security}: {adjusted_loss_limit}")
+    # print(f'position: {position}, direction: {direction}')
+    
+    if direction == 'long':
+        current_price = position.price
+        multiplier = position.value/(position.total_amount * position.price)
+        revenue = multiplier * (position.price - position.acc_avg_cost) * position.total_amount
+        # print(f"security: {position.security}, current_price: {current_price}, cost: {position.acc_avg_cost}, multiplier: {multiplier}, revenue: {revenue}")
+        
+        if revenue < adjusted_loss_limit:
+            close_position(position, g.trade_history[position.security]['direction'])
+            return True
+    elif direction == 'short':
+        current_price = position.price
+        multiplier = position.value/(position.total_amount * position.price)
+        revenue = multiplier * (position.acc_avg_cost - position.price) * position.total_amount
+        # print(f"security: {position.security}, current_price: {current_price}, cost: {position.acc_avg_cost}, multiplier: {multiplier}, revenue: {revenue}")
+
+        if revenue < adjusted_loss_limit:
+            close_position(position, g.trade_history[position.security]['direction'])
+            return True
+
+    return False
+
+############################ 一些辅助函数 ###################################
+#3-0 计算可以用于交易的金额
+def calculate_order_value(context, security, direction):
+    # 获取最新的合约价格
+    current_price = get_current_data()[security].last_price
+
+    # 获取合约的品种(从合约代码中提取)
+    underlying_symbol = security.split('.')[0][:-4]  # 例如从 'IF1412.CCFX' 提取 'IF'
+
+    # 获取保证金比例
+    print(f'检查保证金比例{security}, direction: {direction}')
+    margin_rate = g.default_margin_rates.get(direction, {}).get(underlying_symbol, 0.10)  # 默认值作为后备
+
+    # 获取合约乘数
+    multiplier = g.multiplier.get(underlying_symbol, 10)
+
+    # 计算单手保证金
+    single_hand_margin = current_price * multiplier * margin_rate
+
+    # 根据单手保证金决定购买手数
+    if single_hand_margin <= 20000:
+        total_margin = 20000
+    else:
+        total_margin = single_hand_margin
+    print(f'calculate margin for {security} multiplier: {multiplier}, margin_rate: {margin_rate}, single_hand_margin: {single_hand_margin}, total_margin: {total_margin}')
+
+    return total_margin
+
+#3-1 更新收盘价和4条ma均线之间的关系
+def update_high_low_ma_relations(symbol, high, low, ma5, ma10, ma20, ma30):
+    if symbol not in g.high_low_ma_relations:
+        g.high_low_ma_relations[symbol] = []
+    
+    today_relations = {
+        'ma5': {'low_below': low <= 0.997*ma5, 'high_above': high >= 1.003*ma5},
+        'ma10': {'low_below': low <= 0.997*ma10, 'high_above': high >= 1.003*ma10},
+        'ma20': {'low_below': low <= 0.997*ma20, 'high_above': high >= 1.003*ma20},
+        'ma30': {'low_below': low <= 0.997*ma30, 'high_above': high >= 1.003*ma30}
+    }
+    
+    # 保持最近三天的数据
+    if len(g.high_low_ma_relations[symbol]) >= 3:
+        g.high_low_ma_relations[symbol].pop(0)
+    g.high_low_ma_relations[symbol].append(today_relations)
+
+#3-2 更新收盘价是否够购买条件
+def check_ma_buy_conditions(symbol, latest_data, line_label, line_type):
+    relations = g.high_low_ma_relations.get(symbol, [])
+    print(f'checking {symbol}, relations: {relations}, line_label: {line_label}, line_type: {line_type}')
+    if not relations:
+        return False
+    
+    # 通过line_label确定是对ma5, ma10还是ma20进行判断
+    ma_value = latest_data[line_label]
+    close_price = latest_data['close']
+
+    # 检查今天的关系是否满足特定条件
+    for today_relations in relations:
+        if line_type == 'support' and today_relations[line_label]['low_below'] and 1.005*ma_value <= close_price < 1.02*ma_value:
+            return True
+        elif line_type == 'resistance' and today_relations[line_label]['high_above'] and 0.98*ma_value < close_price <= 0.995*ma_value:
+            return True
+    
+    return False
+
+#更新和均线关系连续天数的方法
+def update_continuous_days(continuous_days, close, ma5, ma10, ma20, ma30, length, message=False):
+    # log.warning(f"before update_continuous_days: {continuous_days}")
+    # 更新连续天数逻辑
+    for key in ['above_ma5', 'below_ma5', 'above_ma10', 'below_ma10', 'above_ma20', 'below_ma20', 'above_ma30', 'below_ma30']:
+        condition_met = False
+        if ((key == 'above_ma5' and close > ma5) or
+            (key == 'below_ma5' and close < ma5) or
+            (key == 'above_ma10' and close > ma10) or
+            (key == 'below_ma10' and close < ma10) or
+            (key == 'above_ma20' and close > ma20) or
+            (key == 'below_ma20' and close < ma20) or
+            (key == 'above_ma30' and close > ma30) or
+            (key == 'below_ma30' and close < ma30)):
+            condition_met = True
+        
+        # 基本更新
+        if condition_met:
+            continuous_days[key].insert(0, continuous_days[key][0] + 1)
+        else:
+            continuous_days[key].insert(0, 0)
+        continuous_days[key] = continuous_days[key][:length]
+        
+    # log.warning(f"before dealing ma30: {continuous_days}")
+        
+    # 特殊处理针对ma30
+    for ma_key in ['above_ma30', 'below_ma30']:
+        max_days = max(continuous_days[ma_key]) > 10
+        first_day_positive = continuous_days[ma_key][0] > 0
+        zeros_count = continuous_days[ma_key].count(0) <= 2
+        message = False
+        if message:
+            log.debug(f'check {ma_key}: max_days-{max_days}, first_day_positive-{first_day_positive}, zeros_count-{zeros_count}')
+        if ma_key == 'below_ma30' and close > ma30 or ma_key == 'above_ma30' and close < ma30:
+            max_ratio = abs((close - ma30) / ma30)
+            
+            # 检查是否满足特殊处理的条件
+            if max_days and first_day_positive and zeros_count and max_ratio <= 0.015:
+                # log.debug(f"Meet the condition: {ma_key} for {continuous_days[ma_key]}")
+                # 恢复继续计数的情况
+                if ma_key == 'below_ma30':
+                    continuous_days['below_ma30'] = [sum(x > 0 for x in continuous_days['below_ma30'][:i+1]) for i in range(length)]
+                    continuous_days['above_ma30'] = [0] * length
+                elif ma_key == 'above_ma30':
+                    continuous_days['above_ma30'] = [sum(x > 0 for x in continuous_days['above_ma30'][:i+1]) for i in range(length)]
+                    continuous_days['below_ma30'] = [0] * length
+                    
+    # log.warning(f"by the end of update_continuous_days: {continuous_days}")
+
+#3-4 更新crossed_symbol
+def create_crossed_symbol_info(symbol, line_label, line_type, latest_data):
+    """Helper function to create crossed symbol info dictionary"""
+    return {
+        'symbol': symbol,
+        'line_label': line_label,
+        'line_type': line_type,
+        'direction': 'long' if line_type == 'support' else 'short',
+        'latest_data': latest_data,
+        'historical_condition_tracker': {
+            'low_below_ma10': False,
+            'low_below_ma20': False,
+            'low_below_ma30': False,
+            'high_above_ma10': False,
+            'high_above_ma20': False,
+            'high_above_ma30': False,
+            'close_above_threshold': False,
+            'close_below_threshold': False
+        }
+    }
+
+#3-5 获取今天的交易数据
+def get_latest_trading_data(symbol, context):
+    # 获取今天日期
+    today = context.current_dt.date()
+    
+    # 获取昨天的日期和夜盘开始时间(假设从21:00开始)
+    yesterday = today - timedelta(days=1)
+    night_session_start_date = get_night_session_start_date(today)
+    night_session_start = datetime.combine(night_session_start_date, datetime.strptime("21:00", "%H:%M").time())
+    night_session_start = pd.to_datetime(night_session_start)
+    # print(f'night_session_start: {night_session_start}, {type(night_session_start)}')
+
+    # 获取分钟级别的数据
+    minute_data = attribute_history(symbol, 600, '1m', ['close', 'high', 'low', 'open'], df=True)
+    # 打印数据类型和索引的前几个值
+    # print("minute_data earliest index:", minute_data.index.min())
+    # print("minute_data latest index:", minute_data.index.max())
+
+    # 保留属于当前交易日的数据,包括夜盘
+    # print("Is night_session_start in minute_data index range:", 
+    #   night_session_start >= minute_data.index.min() and night_session_start <= minute_data.index.max())
+    latest_data = minute_data[minute_data.index >= night_session_start]
+    # print("latest_data index after filtering first few values:", latest_data.index[:5])
+    # print("latest_data index after filtering last few values:", latest_data.index[-5:])
+    
+    # 计算截至当前时刻的 close, high, low, open, ma5, ma10, ma20
+    close = latest_data['close'][-1]
+    high = latest_data['high'].max()
+    low = latest_data['low'].min()
+    open_price = latest_data['open'][0]
+    # ma5 = latest_data['close'].rolling(window=5).mean()[-1]
+    # ma10 = latest_data['close'].rolling(window=10).mean()[-1]
+    # ma20 = latest_data['close'].rolling(window=20).mean()[-1]
+    
+    # print(f'symbol: {symbol}, close: {close}, high: {high}, low: {low}, open: {open_price}')#, ma5: {ma5}, ma10: {ma10}, ma20: {ma20}')
+    return {'close': close, 'high': high, 'low': low, 'open': open_price}#, 'ma5': ma5, 'ma10': ma10, 'ma20': ma20}
+
+#3-6 获取今天的交易时间
+def get_night_session_start_date(current_date):
+    # 如果今天是周一(weekday() == 0),夜盘开始于上一个周五
+    if current_date.weekday() == 0:
+        return current_date - timedelta(days=3)
+    else:
+        return current_date - timedelta(days=1)
+
+#3-7 将当日的数据融入之前的数据里        
+def calculate_continuous_data(today_data, today_daily_data_info, symbol, continuous_days_length):
+    # 先计算今天的近似MA,获取包括今天在内的近日数据,至少需要 20 天数据来计算 MA20
+    # print(f'for calculate_continuous_data, today_daily_data_info: {today_daily_data_info}')
+    recent_data = today_daily_data_info[symbol][-29:] + [today_data]
+    # print(f'under calculate_continuous_data, recent_data: {recent_data}')
+    # print(f'today_daily_data_info[symbol]: {today_daily_data_info[symbol]}')
+    # print(f'original today_data: {today_data}')
+
+    # 计算 MA 值
+    df_recent = pd.DataFrame(recent_data)
+    
+    # print(f'df_recent: {df_recent}')
+    today_data['ma5'] = df_recent['close'].rolling(window=5).mean().iloc[-1]
+    today_data['ma10'] = df_recent['close'].rolling(window=10).mean().iloc[-1]
+    today_data['ma20'] = df_recent['close'].rolling(window=20).mean().iloc[-1]
+    today_data['ma30'] = df_recent['close'].rolling(window=30).mean().iloc[-1]
+    # print(f'new today_data: {today_data}')
+
+    # 获取昨天的数据
+    last_day_data = today_daily_data_info[symbol][-1]
+    # print(f'checking calculate_continuous_data: symbol-{symbol}')
+    # print(f'df_recent: {df_recent.info()}')
+    # print(f'last_day_data: {last_day_data}')
+
+    # 初始化今天的连续天数数据
+    new_continuous_data = {
+        'above_ma5': last_day_data['continuous_above_ma5'].copy(),
+        'below_ma5': last_day_data['continuous_below_ma5'].copy(),
+        'above_ma10': last_day_data['continuous_above_ma10'].copy(),
+        'below_ma10': last_day_data['continuous_below_ma10'].copy(),
+        'above_ma20': last_day_data['continuous_above_ma20'].copy(),
+        'below_ma20': last_day_data['continuous_below_ma20'].copy(),
+        'above_ma30': last_day_data['continuous_above_ma30'].copy(),
+        'below_ma30': last_day_data['continuous_below_ma30'].copy()
+    }
+    # print(f'new_continuous_data: {new_continuous_data}')
+
+    # 更新今天的连续天数数据
+    for ma_label in ['ma5', 'ma10', 'ma20', 'ma30']:
+        # print(f"today close: {today_data['close']}, today ma_label: {today_data[ma_label]}")
+        if today_data['close'] > today_data[ma_label]:
+            new_continuous_data[f'above_{ma_label}'].insert(0, new_continuous_data[f'above_{ma_label}'][0] + 1)
+            new_continuous_data[f'below_{ma_label}'].insert(0, 0)
+        elif today_data['close'] < today_data[ma_label]:
+            new_continuous_data[f'below_{ma_label}'].insert(0, new_continuous_data[f'below_{ma_label}'][0] + 1)
+            new_continuous_data[f'above_{ma_label}'].insert(0, 0)
+        else:
+            # 如果收盘价等于MA值,则重置连续天数
+            new_continuous_data[f'above_{ma_label}'].insert(0, 0)
+            new_continuous_data[f'below_{ma_label}'].insert(0, 0)
+
+    # 确保长度不超过 continuous_days_length
+    for key in new_continuous_data:
+        if len(new_continuous_data[key]) > continuous_days_length:
+            new_continuous_data[key] = new_continuous_data[key][:continuous_days_length]
+
+    # 构建今天的完整数据并返回
+    today_full_data = {**today_data, **{
+        'continuous_above_ma5': new_continuous_data['above_ma5'],
+        'continuous_below_ma5': new_continuous_data['below_ma5'],
+        'continuous_above_ma10': new_continuous_data['above_ma10'],
+        'continuous_below_ma10': new_continuous_data['below_ma10'],
+        'continuous_above_ma20': new_continuous_data['above_ma20'],
+        'continuous_below_ma20': new_continuous_data['below_ma20'],
+        'continuous_above_ma30': new_continuous_data['above_ma30'],
+        'continuous_below_ma30': new_continuous_data['below_ma30']
+    }}
+    # print(f'today_full_data: {today_full_data}')
+    
+    return today_full_data
+
+#3-8 计算日均变化率
+def calculate_average_daily_change_rate(security, days=30):
+    historical_data = attribute_history(security, days + 1, '1d', ['close'])  # 获取额外一天的数据以便计算变化率
+    daily_change_rates = abs(historical_data['close'].pct_change()).iloc[1:]  # 排除第一条数据
+    avg_daily_change_rate = daily_change_rates.mean()
+    # print(f"Average Daily Change Rate for {security}: {avg_daily_change_rate}")
+    return avg_daily_change_rate
+
+#3-9 计算所需MA数据
+def calculate_ma_values(security, ma_periods):
+    historical_data = attribute_history(security, max(ma_periods), '1d', ['close'])
+    today_price = get_current_data()[security].last_price
+    close_prices = historical_data['close'].tolist() + [today_price]
+    # print(f'check ma prices: security-{security}, close_prices-{close_prices}')
+    ma_values = {f'ma{period}': np.mean(close_prices[-period:]) for period in ma_periods}
+    return ma_values
+    
+#3-10 判断过去一段时间不包括当天4条ma均线在过去几天有多少次交叉
+def count_ma_crosses(future_data, days, if_details=False):
+    recent_data = future_data[-days:]
+    # print(f'future_data: {future_data}, recent_data: {recent_data}')
+    ma5 = [day['ma5'] for day in recent_data]
+    ma10 = [day['ma10'] for day in recent_data]
+    ma20 = [day['ma20'] for day in recent_data]
+    ma30 = [day['ma30'] for day in recent_data]
+    # print(f'ma5: {ma5}, ma10: {ma10}, ma20: {ma20}, ma30: {ma30}')
+    
+    cross_5_10 = sum([1 for i in range(1, len(ma5)) if ((ma5[-i] > ma10[-i] and ma5[-i-1] < ma10[-i-1]) or 
+                                                      (ma5[-i] < ma10[-i] and ma5[-i-1] > ma10[-i-1]))])
+    cross_5_20 = sum([1 for i in range(1, len(ma5)) if ((ma5[-i] > ma20[-i] and ma5[-i-1] < ma20[-i-1]) or 
+                                                      (ma5[-i] < ma20[-i] and ma5[-i-1] > ma20[-i-1]))])
+    cross_5_30 = sum([1 for i in range(1, len(ma5)) if ((ma5[-i] > ma30[-i] and ma5[-i-1] < ma30[-i-1]) or 
+                                                      (ma5[-i] < ma30[-i] and ma5[-i-1] > ma30[-i-1]))])
+    cross_10_20 = sum([1 for i in range(1, len(ma10)) if ((ma10[-i] > ma20[-i] and ma10[-i-1] < ma20[-i-1]) or 
+                                                      (ma10[-i] < ma20[-i] and ma10[-i-1] > ma20[-i-1]))])
+    cross_10_30 = sum([1 for i in range(1, len(ma10)) if ((ma10[-i] > ma30[-i] and ma10[-i-1] < ma30[-i-1]) or 
+                                                      (ma10[-i] < ma30[-i] and ma10[-i-1] > ma30[-i-1]))])
+    cross_20_30 = sum([1 for i in range(1, len(ma20)) if ((ma20[-i] > ma30[-i] and ma20[-i-1] < ma30[-i-1]) or 
+                                                      (ma20[-i] < ma30[-i] and ma20[-i-1] > ma30[-i-1]))])
+    crosses = cross_5_10 + cross_5_20 + cross_5_30 + cross_10_20 + cross_10_30 + cross_20_30
+    # log.debug(f'crosses: {crosses}, cross_5_10: {cross_5_10}, cross_5_20: {cross_5_20}, cross_5_30: {cross_5_30}, cross_10_20: {cross_10_20}, cross_10_30: {cross_10_30}, cross_20_30: {cross_20_30}')
+    
+    if if_details:
+        check_ma_cross_details(recent_data, days)
+    
+    return crosses
+
+#3-11 查看具体均线交叉的日期和价格
+def check_ma_cross_details(recent_data, days):
+    # 初始化交叉信息列表
+    cross_info = []
+    
+    # 遍历days天的数据
+    for i in range(1, days):
+        # 检查MA线的交叉
+        current_date = recent_data[-i]['date']
+        previous_date = recent_data[-i - 1]['date']
+        if (ma5[-i] > ma10[-i] and ma5[-i-1] < ma10[-i-1]) or (ma5[-i] < ma10[-i] and ma5[-i-1] > ma10[-i-1]):
+            cross_info.append({
+                'Date': previous_date,  # 前一天的日期
+                'MA5': ma5[-i],  # MA5的值
+                'MA10': ma10[-i],  # MA10的值
+                'Date_Next': current_date,  # 当天的日期
+                'MA5_Next': ma5[-i-1],
+                'MA10_Next': ma10[-i-1],
+                'Cross_Type': 'MA5_MA10'
+            })
+    
+        if (ma5[-i] > ma20[-i] and ma5[-i-1] < ma20[-i-1]) or (ma5[-i] < ma20[-i] and ma5[-i-1] > ma20[-i-1]):
+            cross_info.append({
+                'Date': previous_date,  # 前一天的日期
+                'MA5': ma5[-i],  # MA5的值
+                'MA20': ma20[-i],  # MA20的值
+                'Date_Next': current_date,  # 当天的日期
+                'MA5_Next': ma5[-i-1],
+                'MA20_Next': ma20[-i-1],
+                'Cross_Type': 'MA5_MA20'
+            })
+    
+        if (ma5[-i] > ma30[-i] and ma5[-i-1] < ma30[-i-1]) or (ma5[-i] < ma30[-i] and ma5[-i-1] > ma30[-i-1]):
+            cross_info.append({
+                'Date': previous_date,  # 前一天的日期
+                'MA5': ma5[-i],  # MA5的值
+                'MA30': ma30[-i],  # MA30的值
+                'Date_Next': current_date,  # 当天的日期
+                'MA5_Next': ma5[-i-1],
+                'MA30_Next': ma30[-i-1],
+                'Cross_Type': 'MA5_MA30'
+            })
+    
+        # 检查MA10与其他MA线的交叉
+        if (ma10[-i] > ma20[-i] and ma10[-i-1] < ma20[-i-1]) or (ma10[-i] < ma20[-i] and ma10[-i-1] > ma20[-i-1]):
+            cross_info.append({
+                'Date': previous_date,  # 前一天的日期
+                'MA10': ma10[-i],  # MA10的值
+                'MA20': ma20[-i],  # MA20的值
+                'Date_Next': current_date,  # 当天的日期
+                'MA10_Next': ma10[-i-1],
+                'MA20_Next': ma20[-i-1],
+                'Cross_Type': 'MA10_MA20'
+            })
+    
+        if (ma10[-i] > ma30[-i] and ma10[-i-1] < ma30[-i-1]) or (ma10[-i] < ma30[-i] and ma10[-i-1] > ma30[-i-1]):
+            cross_info.append({
+                'Date': previous_date,  # 前一天的日期
+                'MA10': ma10[-i],  # MA10的值
+                'MA30': ma30[-i],  # MA30的值
+                'Date_Next': current_date,  # 当天的日期
+                'MA10_Next': ma10[-i-1],
+                'MA30_Next': ma30[-i-1],
+                'Cross_Type': 'MA10_MA30'
+            })
+            
+        if (ma20[-i] > ma30[-i] and ma20[-i-1] < ma30[-i-1]) or (ma20[-i] < ma30[-i] and ma20[-i-1] > ma30[-i-1]):
+            cross_info.append({
+                'Date': previous_date,  # 前一天的日期
+                'MA20': ma10[-i],  # MA20的值
+                'MA30': ma30[-i],  # MA30的值
+                'Date_Next': current_date,  # 当天的日期
+                'MA20_Next': ma20[-i-1],
+                'MA30_Next': ma30[-i-1],
+                'Cross_Type': 'MA10_MA30'
+            })
+    
+    # 将交叉信息转换为DataFrame
+    cross_info_df = pd.DataFrame(cross_info)
+    
+    # 打印交叉信息
+    # print(f'crosses: {crosses}, cross_info: {cross_info}, cross_info_df: {cross_info_df}')
+    
+#3-12 查看一下保证金的情况
+def update_margin_rate(context):
+    today = context.current_dt.date()
+    margin_rate_df = finance.run_query(query(finance.FUT_MARGIN).filter(finance.FUT_MARGIN.day == '2022-12-22'))
+    # print(f'today date: {today}, margin_rate_df: {margin_rate_df}')
+ 
+#3-13 检查趋势是否有特殊情况
+def adjust_for_special_condition(daily_data, continuous_days_length):
+    # 为每个均线条件检查是否满足特殊情况
+    print("Daily Data:", daily_data)
+    for key in ['above_ma5', 'below_ma5', 'above_ma10', 'below_ma10', 'above_ma20', 'below_ma20', 'above_ma30', 'below_ma30']:
+        for i in range(len(daily_data) - continuous_days_length + 1):
+            print("Processing Date:", daily_data[i]['date'])
+            # 提取特定范围内的数据
+            subset = daily_data[i:i + continuous_days_length]
+            # 检查最大连续天数是否大于等于10
+            max_continuous_days = max([day[f'continuous_{key}'][0] for day in subset])
+            if max_continuous_days >= 10:
+                # 检查连续天数为0的天数
+                days_with_zero = [day for day in subset if day[f'continuous_{key}'][0] == 0]
+                if len(days_with_zero) <= 3:
+                    # 检查收盘价与均线价格之间的比值是否在-0.005到0.005之间
+                    valid_ratio_days = all(
+                        -0.005 <= (day['close'] - day[key.split('_')[1]]) / day[key.split('_')[1]] <= 0.005
+                        for day in days_with_zero
+                    )
+                    if valid_ratio_days:
+                        # 如果满足特殊情况,根据需要调整逻辑
+                        pass  # 在这里实现具体的调整逻辑
+
+#3-14 更新突破的列表
+def update_crossed_symbols_history(context, new_record):
+    symbol = new_record['symbol']
+    print(f'更新突破的历史列表,这次是{symbol}')
+    date = new_record['date']
+    line_label = new_record['line_label']
+    line_type = new_record['line_type']
+    
+    # 检查历史穿越记录里是否有相同的标的或者同一族的标的
+    if len(g.crossed_symbols_history) == 0:
+        print('g.crossed_symbols_history为空')
+        # 这里也要改,要增加该记录
+        g.crossed_symbols_history[symbol] = [new_record]
+        return
+    
+    if symbol in g.crossed_symbols_history:
+        pass
+    else:
+        similar_symbol = next((s for s in g.crossed_symbols_history if s.split('.')[0] == symbol.split('.')[0]), None)
+        if similar_symbol:
+            pass
+        else:
+            # 这里也要改,要增加该记录
+            print(f'g.crossed_symbols_history没有{symbol},所以要增加')
+            g.crossed_symbols_history[symbol] = [new_record]
+            return
+
+    # 情况1:检查是否存在一模一样的symbol
+    if symbol in g.crossed_symbols_history:
+        existing_records = g.crossed_symbols_history[symbol]
+        print(f'temp里现有{symbol}对应的记录: {existing_records}')
+
+        for record in existing_records:
+            print(f'开始逐条处理{symbol}的记录: {record}和最新的记录{new_record}')
+            # 情况1.1:检查line_label和line_type是否一致
+            if record['line_label'] == line_label and record['line_type'] == line_type:
+                # 情况1.1.2:检查date是否一样
+                if record['date'] == date:
+                    # 情况1.1.1:更新相同date的数据
+                    print(f'情况1.1.1 查到相同标的{symbol}、趋势线{line_label} & {line_type}、日期{date}的记录{g.crossed_symbols_history[symbol]},删除日期日期相同的那一条,并添加最新的')
+                    record.update(new_record)
+                    print(f'更新之后的记录{record}')
+                    return 
+                # 情况1.1.2.1: 添加新记录
+                else:
+                    print(f'情况1.1.2 查到相同标的{symbol}、趋势线{line_label} & {line_type}、不同日期{date}的记录,直接添加新纪录。g.crossed_symbols_history[symbol]: {g.crossed_symbols_history[symbol]}')
+                    g.crossed_symbols_history[symbol].append(new_record)
+                    print(f'更新之后的记录{g.crossed_symbols_history[symbol]}')
+                
+                break
+            else:
+                # 情况1.1.1: 比较line_label,只保留数字较大的
+                print(f"情况1.2.1 查到相同标的{symbol},不同趋势线{line_label} & {line_type}和{record['line_label']} & {record['line_type']},更新所有原始记录: {g.crossed_symbols_history[symbol]},添加新纪录")
+                max_label_record = max(existing_records, key=lambda x: int(''.join(filter(str.isdigit, x['line_label']))))
+                max_label_number = int(''.join(filter(str.isdigit, max_label_record['line_label'])))
+                new_label_number = int(''.join(filter(str.isdigit, line_label)))
+                print(f'max_label_record: {max_label_record}, max_label_number: {max_label_number}, new_label_number: {new_label_number}')
+                if new_label_number > max_label_number:
+                    # 创建一个新的列表来保存所有记录的日期
+                    dates_to_save = [record['date'] for record in existing_records]
+                    print(f'dates_to_save: {dates_to_save}')
+                    # 根据上述的dates_to_save更新新的趋势线的历史数据,首先天数不能超过3
+                    if len(dates_to_save) + 1 <=3:
+                        close_data = attribute_history(symbol, 50, '1d', ['close', 'high', 'low', 'open'], df=True)
+                        close_series = close_data['close']
+                        # print(f'Check if future: {symbol} - {close_series}')
+                
+                        # 计算5日、10日、20日和30日移动平均线
+                        ma5 = close_series.rolling(window=5).mean()
+                        ma10 = close_series.rolling(window=10).mean()
+                        ma20 = close_series.rolling(window=20).mean()
+                        ma30 = close_series.rolling(window=30).mean()
+                        print(f'updating data for new lines MA{new_label_number}')
+                        
+                    # 更新为数字较大的记录
+                    # for record in existing_records:
+                    #     # 替换 existing_records 中每个记录的 'line_label' 和 'line_type'
+                    #     record['line_label'] = new_record['line_label']
+                    #     record['line_type'] = new_record['line_type']
+                    existing_records.clear()
+                    print(f'去除原有{max_label_record}的记录后: {existing_records}')
+                    existing_records.append(new_record)
+                    print(f'增加最新的记录:{g.crossed_symbols_history[symbol]}')
+            return 
+    else: 
+        # 情况1.2:检查是否有字母相同的symbol
+        similar_symbol = next((s for s in g.crossed_symbols_history if s.split('.')[0] == symbol.split('.')[0]), None)
+        if similar_symbol:
+            print(f'情况1.2 有字母相同的symbol,similar_symbol: {similar_symbol}, g.crossed_symbols_history: {g.crossed_symbols_history}')
+            # 情况1.2.2:更新为最新的symbol,并继承原始symbol的数据
+            g.crossed_symbols_history[symbol] = g.crossed_symbols_history.pop(similar_symbol)
+            print(f'更新完之后g.crossed_symbols_history: {g.crossed_symbols_history}')
+            # 递归调用以重新处理新记录
+            return update_crossed_symbols_history(g.crossed_symbols_history, new_record)
+        else:
+            # 情况1.2.1:直接添加新记录
+            g.crossed_symbols_history[symbol] = [new_record]
+            print(f'没有查询到该标的{symbol}的相关记录,满足情况1.2.1,增加新纪录{new_record}')
+    
+        return
+
+#3-15 检查MA均线之间的关系
+def check_ma_relations(context, symbol, debug=False):
+    def debug_print(message):
+        if debug:
+            print(message)
+    
+    debug_print(f'检查均线之间的关系')
+    relation_check = False
+    latest_record = g.crossed_symbols_history[symbol][-1]
+    debug_print(f'latest_record: {latest_record}')
+    
+    line_label = latest_record['line_label'].lower()  # 确保大小写一致
+    line_type = latest_record['line_type']
+    MA_values = latest_record['latest_data']
+    MA5 = MA_values['ma5']
+    MA10 = MA_values['ma10']
+    MA20 = MA_values['ma20']
+    MA30 = MA_values['ma30']
+    
+    debug_print(f'line_label: {line_label}, line_type: {line_type}, MA5: {MA5}, MA10: {MA10}, MA20: {MA20}, MA30: {MA30}')
+    
+    conditions = {
+        ('ma10', 'resistance'): MA5 > MA10 > MA20 > MA30,
+        ('ma10', 'support'): MA30 > MA20 > MA10 > MA5,
+        ('ma20', 'resistance'): (MA30 >= MA20 * 0.999 and MA5 >= MA10 * 0.999) or (MA30 > MA20 > MA10 > MA5),
+        ('ma20', 'support'): (MA20 >= MA30 * 0.999 and MA10 >= MA5 * 0.999) or (MA5 > MA10 > MA20 > MA30),
+        ('ma30', 'resistance'): (MA30 >= MA20 * 0.999 and MA5 >= MA10 * 0.999) or (MA30 > MA20 > MA10 > MA5),
+        ('ma30', 'support'): (MA20 >= MA30 * 0.999 and MA10 >= MA5 * 0.999) or (MA5 > MA10 > MA20 > MA30),
+    }
+    
+    relation_check = conditions.get((line_label, line_type), False)
+    condition_status = "满足" if relation_check else "不满足"
+    debug_print(f'均线为{line_label} & {line_type},{condition_status}MA指定的条件,MA5: {MA5}, MA10: {MA10}, MA20: {MA20}, MA30: {MA30}')
+    
+    return relation_check
+    
+#3-16 检查破趋势的天数和其中收盘价和均线的最高差别
+def check_cross_details(context, symbol, debug=False):
+    def debug_print(message):
+        if debug:
+            print(message)
+
+    all_records = g.crossed_symbols_history[symbol]
+    if not all_records:
+        debug_print(f"No records found for {symbol}.")
+        return False, False
+
+    # 检查破趋势的天数
+    first_day = all_records[0]['date']
+    today = context.current_dt.date()
+    all_days = get_trade_days(first_day, today)
+    duration_length = len(all_days)
+    cross_duration = duration_length <= 6
+    debug_print(f"[检查破趋势的天数] first_day: {first_day}, today: {today}, duration_length: {duration_length}, cross_duration: {cross_duration}")
+
+    # 检查破趋势范围内收盘价和均价差距的情况
+    ma_close = False
+    for record in all_records:
+        line_label = record['line_label']
+        ma_price = record['latest_data'][line_label]
+        close = record['latest_data']['close']
+        ma_close_rate = abs((close - ma_price) / ma_price)
+        if ma_close_rate <= 0.02:
+            ma_close = True
+        debug_print(f"[检查破趋势内收盘价和均线的关系] record: {record}, ma_price: {ma_price}, close: {close}, ma_close_rate: {ma_close_rate}, ma_close: {ma_close}")
+
+    return cross_duration, ma_close
+
+#3-17 计算开盘到收盘之间穿过几条均线
+def count_ma_lines_in_range(symbol, latest_data):
+    # 获取开盘和收盘价格
+    open_price = latest_data['open']
+    close_price = latest_data['close']
+
+    # 确定价格区间的上限和下限
+    lower_bound = min(open_price, close_price)
+    upper_bound = max(open_price, close_price)
+
+    # 计算哪些MA线在开盘和收盘价格之间
+    ma_lines_in_range = 0
+    ma_labels_in_range = []
+    for key, value in latest_data.items():
+        if 'ma' in key and lower_bound <= value <= upper_bound:
+            ma_lines_in_range += 1
+            ma_labels_in_range.append(key)
+
+    print(f"在{symbol}的{lower_bound}到{upper_bound}之间,有{ma_lines_in_range}条MA线:{', '.join(ma_labels_in_range)}")
+    return ma_lines_in_range, ma_labels_in_range
+
+#3-18 更新不在今天出现或者今天出现但是趋势线不如历史大的记录更新最新一天的数据
+def update_record_for_existing_symbol(symbol, case):
+    # 假设 attribute_history, g.crossed_symbols_history 等都已正确定义和初始化
+    # 根据case参数,执行特定的处理逻辑
+    if case == 'difference_keys':
+        print(f"处理差异键情况的{symbol}")
+        # 这里是处理差异键情况的逻辑...
+    elif case == 'keys_in_both':
+        print(f"处理line_label差异的{symbol}")
+
+    # 获取symbol的最新记录
+    historical_record = max(g.crossed_symbols_history[symbol], key=lambda x: x['date'])
+    historical_record_copy = historical_record.copy()
+    
+    # 获取过去50天的数据
+    close_data = attribute_history(symbol, 50, '1d', ['close', 'high', 'low', 'open'], df=True)
+    close_series = close_data['close']
+
+    # 计算移动平均线
+    ma5 = close_series.rolling(window=5).mean().iloc[-1]
+    ma10 = close_series.rolling(window=10).mean().iloc[-1]
+    ma20 = close_series.rolling(window=20).mean().iloc[-1]
+    ma30 = close_series.rolling(window=30).mean().iloc[-1]
+    yesterday_data = close_data.iloc[-1]
+    
+    # 更新记录
+    historical_record_copy['date'] = yesterday_data.name.date()  # 假设'name'是一个datetime对象
+    historical_record_copy['latest_data'] = {
+        'date': yesterday_data.name.date(),
+        'close': yesterday_data['close'],
+        'high': yesterday_data['high'],
+        'low': yesterday_data['low'],
+        'open': yesterday_data['open'],
+        'ma5': ma5,
+        'ma10': ma10,
+        'ma20': ma20,
+        'ma30': ma30,
+    }
+    
+    # 将更新后的记录添加到g.crossed_symbols_history[symbol]
+    g.crossed_symbols_history[symbol].append(historical_record_copy)
+    # print(f'根据{case}更新{symbol}的g.crossed_symbols_history[symbol]: {g.crossed_symbols_history[symbol]}, historical_record_copy: {historical_record_copy}')
+
+#3-19 检查有没有相似元素
+def check_symbol_prefix_match(symbol, hold_symbols):
+    # 去掉右侧9个字符得到symbol的左侧部分
+    symbol_prefix = symbol[:-9]
+    
+    # 遍历hold_symbols中的每个元素
+    for hold_symbol in hold_symbols:
+        # 对每个元素同样去掉右侧9个字符
+        hold_symbol_prefix = hold_symbol[:-9]
+        # 检查左侧部分是否一致
+        if symbol_prefix == hold_symbol_prefix:
+            return True  # 找到一致的情况,返回True
+    return False  # 遍历完成,没有找到一致的情况,返回False
+
+#3-20 计算过去一段时间的日内以及均线数据
+def calculate_daily_data(context, day, symbol, combine_today=True):
+    """
+    计算指定股票的移动平均线并返回一个包含这些数据的DataFrame。
+    """
+    # 使用attribute_history函数获取过去50天的交易数据
+    historical_data = attribute_history(symbol, day, '1d', ['close', 'high', 'low', 'open'], df=True)
+    
+    if combine_today:
+        # 使用get_latest_trading_data函数获取最新的交易数据
+        latest_data_today = get_latest_trading_data(symbol, context)
+        print(f'check latest_data_today under buy check: {latest_data_today}')
+    
+        # 将最新的交易数据转换成DataFrame并追加到historical_data中
+        latest_data_df = pd.DataFrame([latest_data_today], index=[context.current_dt.date()])
+        full_data = pd.concat([historical_data, latest_data_df])
+    else:
+        full_data = historical_data
+
+    # 计算移动平均线并将其添加到DataFrame中
+    full_data['ma5'] = full_data['close'].rolling(window=5).mean()
+    full_data['ma10'] = full_data['close'].rolling(window=10).mean()
+    full_data['ma20'] = full_data['close'].rolling(window=20).mean()
+    full_data['ma30'] = full_data['close'].rolling(window=30).mean()
+
+    return full_data
+
+#3-21 辅助打印list
+def print_list_elements(title, elements):
+    """
+    打印列表中的元素,每个元素占一行。
+
+    :param title: 打印前显示的标题
+    :param elements: 要打印的元素列表
+    """
+    print(f"{title}:")
+    for item in elements:
+        print(item)
+########################## 自动移仓换月函数 #################################
+def position_auto_switch(context,pindex=0,switch_func=None, callback=None):
+    """
+    期货自动移仓换月。默认使用市价单进行开平仓。
+    :param context: 上下文对象
+    :param pindex: 子仓对象
+    :param switch_func: 用户自定义的移仓换月函数.
+        函数原型必须满足: func(context, pindex, previous_dominant_future_position, current_dominant_future_symbol)
+    :param callback: 移仓换月完成后的回调函数。
+        函数原型必须满足: func(context, pindex, previous_dominant_future_position, current_dominant_future_symbol)
+    :return: 发生移仓换月的标的。类型为列表。
+    """
+    import re
+    subportfolio = context.subportfolios[pindex]
+    symbols = set(subportfolio.long_positions.keys()) | set(subportfolio.short_positions.keys())
+    switch_result = []
+    for symbol in symbols:
+        match = re.match(r"(?P<underlying_symbol>[A-Z]{1,})", symbol)
+        if not match:
+            # raise ValueError("未知期货标的: {}".format(symbol))
+            raise ValueError("Unknow target: {}".format(symbol))
+        else:
+            dominant = get_dominant_future(match.groupdict()["underlying_symbol"])
+            cur = get_current_data()
+            symbol_last_price = cur[symbol].last_price
+            dominant_last_price = cur[dominant].last_price
+            print(f'current_hold_symbol: {symbol}, current_main_symbol: {dominant}')
+            
+            if dominant > symbol:
+                for positions_ in (subportfolio.long_positions, subportfolio.short_positions):
+                    if symbol not in positions_.keys():
+                        continue
+                    else :
+                        p = positions_[symbol]
+
+                    if switch_func is not None:
+                        switch_func(context, pindex, p, dominant)
+                    else:
+                        amount = p.total_amount
+                        # 跌停不能开空和平多,涨停不能开多和平空。
+                        if p.side == "long":
+                            symbol_low_limit = cur[symbol].low_limit
+                            dominant_high_limit = cur[dominant].high_limit
+                            if symbol_last_price <= symbol_low_limit:
+                                # log.warning("标的{}跌停,无法平仓。移仓换月取消。".format(symbol))
+                                log.warning("Can’t close {} position due to the limit up. Cancelling the exchange.".format(symbol))
+                                continue
+                            elif dominant_last_price >= dominant_high_limit:
+                                # log.warning("标的{}涨停,无法开仓。移仓换月取消。".format(symbol))
+                                log.warning("Can’t close {} position due to the limit down. Cancelling the exchange.".format(symbol))
+                                continue
+                            else:
+                                # log.info("进行移仓换月: ({0},long) -> ({1},long)".format(symbol, dominant))
+                                log.info("Start the exchange: ({0},long) -> ({1},long)".format(symbol, dominant))
+                                order_old = order_target(symbol,0,side='long')
+                                if order_old != None and order_old.filled > 0:
+                                    order_new = order_target(dominant,amount,side='long')
+                                    if order_new != None and order_new.filled >0:
+                                        switch_result.append({"before": symbol, "after":dominant, "side": "long"})
+                                        # 换月中的买卖都成功了,则增加新的记录去掉旧的记录
+                                        g.trade_history[dominant] = g.trade_history[symbol]
+                                        del g.trade_history[symbol]
+                                    else:
+                                        # log.warning("标的{}交易失败,无法开仓。移仓换月取消。".format(domaint))
+                                        log.warning("Trade of {} failed, no new positions. Cancelling the exchange.".format(dominant))
+                                        # 换月中的买成功了,卖失败了,则在换月记录里增加新的记录,在交易记录里去掉旧的记录
+                                        print(f'换月多头失败{dominant}, {g.trade_history[symbol]}')
+                                        g.change_fail_history[domaint] = g.trade_history[symbol]
+                                        del g.trade_history[symbol]
+                            if callback:
+                                callback(context, pindex, p, dominant)
+                        if p.side == "short":
+                            symbol_high_limit = cur[symbol].high_limit
+                            dominant_low_limit = cur[dominant].low_limit
+                            if symbol_last_price >= symbol_high_limit:
+                                # log.warning("标的{}涨停,无法平仓。移仓换月取消。".format(symbol))
+                                log.warning("Can’t close {} position due to the limit up. Cancelling the exchange.".format(symbol))
+                                continue
+                            elif dominant_last_price <= dominant_low_limit:
+                                # log.warning("标的{}跌停,无法开仓。移仓换月取消。".format(symbol))
+                                log.warning("Can’t close {} position due to the limit down. Cancelling the exchange.".format(symbol))
+                                continue
+                            else:
+                                # log.info("进行移仓换月: ({0},short) -> ({1},short)".format(symbol, dominant))
+                                log.info("Start the exchange: ({0},short) -> ({1},short)".format(symbol, dominant))
+                                order_old = order_target(symbol,0,side='short')
+                                if order_old != None and order_old.filled > 0:
+                                    print(f'换月做空{dominant},数量位{amount}')
+                                    order_new = order_target(dominant,amount,side='short')
+                                    if order_new != None and order_new.filled >0:
+                                        switch_result.append({"before": symbol, "after":dominant, "side": "short"})
+                                        # 换月中的买卖都成功了,则增加新的记录去掉旧的记录
+                                        g.trade_history[dominant] = g.trade_history[symbol]
+                                        del g.trade_history[symbol]
+                                    else:
+                                        # log.warning("标的{}交易失败,无法开仓。移仓换月取消。".format(dominant))
+                                        log.warning("Trade of {} failed, no new positions. Cancelling the exchange.".format(dominant))
+                                        # 换月中的买成功了,卖失败了,则在换月记录里增加新的记录,在交易记录里去掉旧的记录
+                                        print(f'换月空头失败{dominant}, {g.trade_history[symbol]}')
+                                        g.change_fail_history[dominant] = g.trade_history[symbol]
+                                        print(f'失败记录{g.change_fail_history}')
+                                        del g.trade_history[symbol]
+                                # order_target(symbol,0,side='short')
+                                # order_target(dominant,amount,side='short')
+                                # switch_result.append({"before": symbol, "after": dominant, "side": "short"})
+                                if callback:
+                                    callback(context, pindex, p, dominant)
+    return switch_result

+ 275 - 0
Lib/research/future_all_change_trend.py

@@ -0,0 +1,275 @@
+import pandas as pd
+import numpy as np
+from jqdata import *
+import datetime
+# from datetime import timedelta, datetime
+import matplotlib.pyplot as plt
+
+def get_all_key_info(the_type):
+    main = get_all_securities(types=[the_type]).reset_index()
+    the_main = main[main.display_name.str.endswith('主力合约')]
+    the_main.rename(columns={'index': 'code'}, inplace=True)
+    
+    # 若类型是期货,提取期货代码列表
+    if the_type == 'futures':
+        all_future_list = the_main['code'].unique()
+        return the_main, all_future_list
+    else:
+        return the_main, None
+
+    
+def get_the_period_of_year_range(start_year, end_year):
+    now = datetime.datetime.now()
+    start_date = datetime.datetime(start_year, 1, 1)
+    end_date = datetime.datetime(end_year, 12, 31)
+
+    # 如果结束年份是当前年份,则将结束日期调整为当前日期
+    if end_year == now.year and now < end_date:
+        end_date = now
+
+    # 获取交易日历
+    trade_days = get_trade_days(start_date=start_date, end_date=end_date)
+
+    # 获取实际的开始和结束日期
+    actual_start_date = trade_days[0]
+    actual_end_date = trade_days[-1]
+    print(f'start_date: {actual_start_date}, end_date: {actual_end_date}')
+
+    return actual_start_date, actual_end_date
+
+
+def get_one_future(future, unit, start_date, end_date, export=False):
+    if unit == 'daily':
+        data = get_price(future, start_date=start_date, end_date=end_date, frequency=unit, 
+                         fields=['close','open','low','high','volume','money'], skip_paused=False,
+                         panel=False).dropna()
+        if export:
+            filename = f"{future.split('.')[0]}_{unit}_{end_date.strftime('%Y-%m-%d')}.csv"
+            data.to_csv(filename)
+    return data
+
+def get_predict_input(key_list, start_day, latest_day, period, export=False):
+    all_data = []
+
+    for key in key_list:
+        idata = get_one_future(key, 'daily', start_day, latest_day, False)
+        if len(idata) >= period + 1:
+            idata['Code'] = key
+            all_data.append(idata)
+        else:
+            print(f'The future {key} has only {len(idata)} data points')
+
+    data = pd.concat(all_data)
+    data.reset_index(inplace=True)
+    data.rename(columns={'index': 'Date'}, inplace=True)
+    data.set_index(['Code', 'Date'], inplace=True)
+
+    if export:
+        filename = f'Input_{period}_{latest_day.strftime("%Y-%m-%d")}.csv'
+        data.to_csv(filename)
+
+    return data
+
+
+def get_train_input(work_future_list, all_future_df, latest_day, limit, export=False):
+    all_data = []
+
+    for future in work_future_list:
+        start_date = all_future_df.loc[all_future_df.code == future, 'start_date'].values[0]
+        idata = get_one_future(future, 'daily', start_date, latest_day, False)
+        if len(idata) >= limit:
+            idata['Code'] = future
+            all_data.append(idata)
+        else:
+            print(f'The future {future} has only {len(idata)} data points')
+
+    data = pd.concat(all_data)
+    data.reset_index(inplace=True)
+    data.rename(columns={'index': 'Date'}, inplace=True)
+    data.set_index(['Code', 'Date'], inplace=True)
+
+    if export:
+        filename = f'TrainData_{latest_day.strftime("%Y-%m-%d")}.csv'
+        data.to_csv(filename)
+
+    return data
+
+def process_data(df, std_dev_multiplier=3):
+    # 计算价格变化百分比
+    df['change'] = df.groupby('Code')['close'].pct_change()
+
+    # 计算开盘价变化的绝对值
+    df['openChange'] = df.groupby('Code').apply(lambda x: (x['open'] - x['close'].shift(1)).abs()).reset_index(level=0, drop=True)
+
+    # 去除 NaN 值
+    df.dropna(subset=['change', 'openChange'], inplace=True)
+
+    # 计算每日的平均变化、正负变化数量
+    grouped = df.groupby('Date')
+    summary = grouped['change'].agg(['mean', 'count', positive_change_count, negative_change_count])
+    summary.rename(columns={'mean': 'Average Change', 'count': 'Total Count'}, inplace=True)
+
+    # 计算滚动平均值
+    for col in ['Average Change']:
+        for window in [5, 10]:
+            summary[f'{col}_{window}'] = summary[col].rolling(window=window).mean()
+
+    # 去除滚动平均值的 NaN 值
+    summary.dropna(inplace=True)
+
+    # 过滤异常值
+    for col in ['Average Change_5', 'Average Change_10']:
+        mean = summary[col].mean()
+        std = summary[col].std()
+        threshold = std_dev_multiplier * std
+        outliers = summary[(summary[col] < mean - threshold) | (summary[col] > mean + threshold)]
+        if not outliers.empty:
+            print(f"异常数据 in '{col}':\n{outliers}")
+
+        summary = summary[(summary[col] > mean - threshold) & (summary[col] < mean + threshold)]
+
+    return summary
+
+
+# 正负变化计数的辅助函数
+def positive_change_count(series):
+    return (series > 0).sum()
+
+def negative_change_count(series):
+    return (series < 0).sum()
+
+def plot_data(data, columns):
+    plt.figure(figsize=(10, 6))
+    for col in columns:
+        plt.plot(data.index, data[col], label=col)
+
+    plt.axhline(0, color='red', linestyle='--', label='Zero')
+    plt.xlabel('Date')
+    plt.ylabel('Value')
+    plt.title('Trends Over Time')
+    plt.legend()
+    plt.xticks(rotation=45)
+    plt.grid(True)
+    plt.show()
+
+
+# 使用新的函数处理数据
+the_start_day, latest_day = get_the_period_of_year_range(2023, 2023)
+
+# df = get_predict_input(work_future_list, the_start_day, latest_day, period)
+df = get_predict_input(all_future_list, the_start_day, latest_day, period)
+df.reset_index(inplace=True)
+
+df.to_csv('test.csv', index=False)
+
+all_future_df, all_future_list = get_all_key_info('futures')
+except_futures = ['BC9999.XINE', 'CJ9999.XZCE', 'EB9999.XDCE', 'EG9999.XDCE', 'ER9999.XZCE', 'GN9999.XZCE', 'LH9999.XDCE', 
+                  'LU9999.XINE', 'ME9999.XZCE', 'NR9999.XINE', 'PF9999.XZCE', 'PG9999.XDCE', 'PK9999.XZCE', 'RO9999.XZCE', 
+                  'RR9999.XDCE', 'SA9999.XZCE', 'SC9999.XINE', 'SP9999.XSGE', 'SS9999.XSGE', 'T9999.CCFX', 'TC9999.XZCE',
+                  'TF9999.CCFX', 'TS9999.CCFX', 'UR9999.XZCE', 'WS9999.XZCE', 'WT9999.XZCE'] # 这部分是没有模型的标的
+work_future_list = [i for i in all_future_list if not i in except_futures] # 获取有模型的代码
+period = 130 # 设定需要的数据周期
+
+# 使用新的函数处理数据
+the_start_day, latest_day = get_the_period_of_year_range(2023, 2023)
+
+# df = get_predict_input(work_future_list, the_start_day, latest_day, period)
+df = get_predict_input(all_future_list, the_start_day, latest_day, period)
+
+processed_data = process_data(df, 4)
+
+# 打印处理后的数据
+print(f'Processed Data:\n{processed_data.head()}')
+
+# 绘制图表
+plot_data(processed_data, ['Average Change_5', 'Average Change_10'])
+
+# 针对当前年的数据
+all_future_df, all_future_list = get_all_key_info('futures')
+except_futures = ['BC9999.XINE', 'CJ9999.XZCE', 'EB9999.XDCE', 'EG9999.XDCE', 'ER9999.XZCE', 'GN9999.XZCE', 'LH9999.XDCE', 
+                  'LU9999.XINE', 'ME9999.XZCE', 'NR9999.XINE', 'PF9999.XZCE', 'PG9999.XDCE', 'PK9999.XZCE', 'RO9999.XZCE', 
+                  'RR9999.XDCE', 'SA9999.XZCE', 'SC9999.XINE', 'SP9999.XSGE', 'SS9999.XSGE', 'T9999.CCFX', 'TC9999.XZCE',
+                  'TF9999.CCFX', 'TS9999.CCFX', 'UR9999.XZCE', 'WS9999.XZCE', 'WT9999.XZCE'] # 这部分是没有模型的标的
+work_future_list = [i for i in all_future_list if not i in except_futures] # 获取有模型的代码
+period = 130 # 设定需要的数据周期
+the_start_day, latest_day = get_the_period_of_this_year(period) # 获取预测用的开始和结束时间
+print(f'开始时间:{the_start_day}, 结束时间:{latest_day}')
+df = get_predict_input(work_future_list, the_start_day, latest_day, period)
+print(f'初始数据结构为:{df.info()}')
+
+# 将"index"列设置为索引
+# df['change'] = df.groupby('Code')['close'].apply(lambda x: x - x.shift(1)) # 变化绝对值
+df['change'] = df.groupby('Code')['close'].apply(lambda x: x.pct_change()) # 变化百分比
+df = df.dropna(subset=['change'])
+
+# 每天change的平均值
+average_change_per_day = df.groupby('index')['change'].mean()
+
+# 每天change为正和负的数量
+df['positive_change'] = df['change'].apply(lambda x: 1 if x > 0 else 0)
+df['negative_change'] = df['change'].apply(lambda x: 1 if x < 0 else 0)
+
+positive_change_count_per_day = df.groupby('index')['positive_change'].sum()
+negative_change_count_per_day = df.groupby('index')['negative_change'].sum()
+
+# 正和负的各自平均值
+positive_changes_df = df[df['change'] > 0]
+negative_changes_df = df[df['change'] < 0]
+
+average_positive_change_per_day = positive_changes_df.groupby('index')['change'].mean()
+average_negative_change_per_day = negative_changes_df.groupby('index')['change'].mean()
+
+data = pd.concat([
+    average_change_per_day,
+    positive_change_count_per_day,
+    negative_change_count_per_day,
+    average_positive_change_per_day,
+    average_negative_change_per_day
+], axis=1)
+
+data.columns = [
+    'Average Change',
+    'Positive Change Count',
+    'Negative Change Count',
+    'Average Positive Change',
+    'Average Negative Change'
+]
+
+# 计算'Average Change'的5日滚动平均值
+data['Average Change_5'] = data['Average Change'].rolling(window=5).mean()
+
+# 计算'Average Change'的10日滚动平均值
+data['Average Change_10'] = data['Average Change'].rolling(window=10).mean()
+
+# 计算'Positive Change Count'的5日滚动平均值
+data['Positive Change Count_5'] = data['Positive Change Count'].rolling(window=5).mean()
+
+# 计算'Positive Change Count'的10日滚动平均值
+data['Positive Change Count_10'] = data['Positive Change Count'].rolling(window=10).mean()
+
+# 计算'Negative Change Count'的5日滚动平均值
+data['Negative Change Count_5'] = data['Negative Change Count'].rolling(window=5).mean()
+
+# 计算'Negative Change Count'的10日滚动平均值
+data['Negative Change Count_10'] = data['Negative Change Count'].rolling(window=10).mean()
+
+data = data.dropna()
+
+# 打印修改后的dataFrame
+print(f'最后完成的数据:{data.head()}')
+
+# 绘制第一张折线图
+plt.figure(figsize=(10, 6))
+# plt.plot(data.index, data['Average Change'], label='Average Change')
+plt.plot(data.index, data['Average Change_5'], label='Average Change_5')
+plt.plot(data.index, data['Average Change_10'], label='Average Change_10')
+
+# 绘制纵轴上值为0的红色线段
+plt.axhline(0, color='red', linestyle='--', label='Zero')
+plt.xlabel('Date')
+plt.ylabel('Value')
+plt.title('Average Change Comparison')
+plt.legend()
+plt.xticks(rotation=45)
+plt.grid(True)
+plt.show()

+ 175 - 0
Lib/research/future_ma_cross_analysis.py

@@ -0,0 +1,175 @@
+import pandas as pd
+import numpy as np
+from jqdata import *
+import datetime
+import matplotlib.pyplot as plt
+
+def get_all_key_info(the_type='futures'):
+    """Get all futures main contracts information"""
+    main = get_all_securities(types=[the_type]).reset_index()
+    the_main = main[main.display_name.str.endswith('主力合约')]
+    the_main.rename(columns={'index': 'code'}, inplace=True)
+    all_future_list = the_main['code'].unique()
+    return the_main, all_future_list
+
+def get_period_range(start_year, end_year):
+    """Get trading period range between years"""
+    now = datetime.datetime.now()
+    start_date = datetime.datetime(start_year, 1, 1)
+    end_date = datetime.datetime(end_year, 12, 31)
+
+    if end_year == now.year and now < end_date:
+        end_date = now
+
+    trade_days = get_trade_days(start_date=start_date, end_date=end_date)
+    actual_start_date = trade_days[0]
+    actual_end_date = trade_days[-1]
+    print(f'Analysis period: {actual_start_date} to {actual_end_date}')
+    return actual_start_date, actual_end_date
+
+def get_future_data(future_code, start_date, end_date):
+    """Get price data for a single future contract"""
+    data = get_price(future_code, 
+                    start_date=start_date, 
+                    end_date=end_date, 
+                    frequency='daily',
+                    fields=['open', 'close', 'high', 'low', 'volume'],
+                    skip_paused=False,
+                    panel=False)
+    
+    if data is None or len(data) == 0:
+        return None
+    
+    # Create a copy to avoid SettingWithCopyWarning
+    data = data.copy()
+        
+    # Calculate 5-day MA
+    data['MA5'] = data['close'].rolling(window=5).mean()
+    return data
+
+def analyze_ma_crosses(data):
+    """Analyze MA crosses and calculate statistics"""
+    if data is None or len(data) < 5:  # Need at least 5 days for MA5
+        return pd.DataFrame()
+    
+    # Create a copy of the data to avoid warnings
+    data = data.copy()
+        
+    # Initialize cross detection
+    data['cross_up'] = (data['open'] < data['MA5']) & (data['close'] > data['MA5'])
+    data['cross_down'] = (data['open'] > data['MA5']) & (data['close'] < data['MA5'])
+    
+    # Get indices where crosses occur
+    cross_dates = data.index[data['cross_up'] | data['cross_down']].tolist()
+    
+    results = []
+    for i in range(len(cross_dates) - 1):
+        current_date = cross_dates[i]
+        next_date = cross_dates[i + 1]
+        
+        # Get cross type
+        is_up_cross = data.loc[current_date, 'cross_up']
+        
+        # Calculate trading days between crosses
+        trading_days = len(data.loc[current_date:next_date].index) - 1
+        
+        # Calculate price change
+        start_price = data.loc[current_date, 'close']
+        end_price = data.loc[next_date, 'close']
+        price_change_pct = (end_price - start_price) / start_price * 100
+        
+        results.append({
+            'cross_date': current_date,
+            'next_cross_date': next_date,
+            'cross_type': 'Upward' if is_up_cross else 'Downward',
+            'trading_days': trading_days,
+            'price_change_pct': price_change_pct
+        })
+    
+    return pd.DataFrame(results)
+
+def analyze_all_futures(start_year, end_year):
+    """Analyze MA crosses for all futures contracts"""
+    start_date, end_date = get_period_range(start_year, end_year)
+    all_future_df, all_future_list = get_all_key_info()
+    
+    all_results = []
+    
+    for future in all_future_list:
+        print(f'Analyzing {future}...')
+        data = get_future_data(future, start_date, end_date)
+        if data is not None and len(data) >= 5:
+            results = analyze_ma_crosses(data)
+            if not results.empty:
+                results['future_code'] = future
+                all_results.append(results)
+    
+    if not all_results:
+        return pd.DataFrame()
+        
+    combined_results = pd.concat(all_results, ignore_index=True)
+    return combined_results
+
+def generate_statistics(results):
+    """Generate summary statistics for the analysis"""
+    if results.empty:
+        print("No results to analyze")
+        return
+        
+    # Group by future code and cross type
+    stats = results.groupby(['future_code', 'cross_type']).agg({
+        'trading_days': ['count', 'mean', 'std', 'min', 'max'],
+        'price_change_pct': ['mean', 'std', 'min', 'max']
+    }).round(2)
+    
+    return stats
+
+def plot_results(results):
+    """Plot distribution of trading days and price changes"""
+    if results.empty:
+        print("No results to plot")
+        return
+        
+    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
+    
+    # Plot trading days distribution
+    for cross_type in ['Upward', 'Downward']:
+        data = results[results['cross_type'] == cross_type]['trading_days']
+        ax1.hist(data, bins=30, alpha=0.5, label=cross_type)
+    ax1.set_title('Distribution of Trading Days Between Crosses')
+    ax1.set_xlabel('Trading Days')
+    ax1.set_ylabel('Frequency')
+    ax1.legend()
+    
+    # Plot price change distribution
+    for cross_type in ['Upward', 'Downward']:
+        data = results[results['cross_type'] == cross_type]['price_change_pct']
+        ax2.hist(data, bins=30, alpha=0.5, label=cross_type)
+    ax2.set_title('Distribution of Price Changes')
+    ax2.set_xlabel('Price Change (%)')
+    ax2.set_ylabel('Frequency')
+    ax2.legend()
+    
+    plt.tight_layout()
+    plt.show()
+
+def main():
+    # Set analysis period (e.g., last 2 years)
+    current_year = datetime.datetime.now().year
+    results = analyze_all_futures(current_year - 1, current_year)
+    
+    if not results.empty:
+        # Generate and display statistics
+        stats = generate_statistics(results)
+        print("\nSummary Statistics:")
+        print(stats)
+        
+        # Plot distributions
+        plot_results(results)
+        
+        # Export results to CSV
+        results.to_csv('ma_cross_analysis_results.csv', index=False)
+        stats.to_csv('ma_cross_analysis_stats.csv')
+
+if __name__ == "__main__":
+    main() 

+ 100 - 0
Lib/stock/README.md

@@ -0,0 +1,100 @@
+# 股票投资策略说明
+
+## 低位首板低开+首阳倒挂策略
+
+### 核心思路
+该策略结合了两个子策略:低位首板低开和首阳倒挂。通过严格的量价筛选和多重优化机制,在把握低位机会的同时控制风险。策略重点关注低位、放量、强势等特征,通过竞价数据进行精准买入。
+
+1. [策略来源](https://www.joinquant.com/post/53374)
+2. 策略名称:低位首板低开+首阳倒挂策略
+
+### 主要特点
+1. 双策略组合(可独立使用)
+2. 多维度筛选机制
+3. 基于竞价数据的精准买入
+4. 动态量价优化
+5. 灵活的参数配置
+
+### 具体策略逻辑
+
+#### 1. 基础筛选标准
+- 剔除ST股票、退市股票、科创板股票、停牌股票
+- 剔除上市时间少于20天的新股
+- 可选择全市场、中证系列或单一股票池
+
+#### 2. 策略一:低位首板低开
+
+##### 2.1 筛选条件
+- 核心思路:捕捉低位股票的首次涨停后的低开机会,通过竞价数据精准介入
+- 选股流程:
+  1. 先筛选出近期(**g.check_dura_1**,默认5天内)首次涨停的股票
+  2. 确保股票处于相对低位(20天涨幅小于**g.pre_rise_1**,110%;就是涨幅小于10%的)
+  3. 通过板型优化(**g.optimz_control**,可选)和量能分析,筛选出高质量的首板标的
+  4. 在竞价阶段,关注这些股票的开盘情况,寻找低开机会
+
+##### 2.2 买入条件
+- 竞价开盘价低于昨日收盘价的**g.auction_open_highlimit_1**,98%(低开)
+- 不能过度低开(有下限控制,**g.auction_open_lowlimit_1**,默认0)
+- 在指定的竞价时间窗口(**g.begin_times** 到 **g.end_times**)内满足条件即可买入
+
+#### 3. 策略二:首阳倒挂
+
+##### 3.1 筛选条件
+- 核心思路:捕捉强势股回调的买入机会,通过"高开回落"形态精准介入
+- 选股流程:
+  1. 筛选处于低位的股票(**g.check_dura_2**,20天涨幅小于**g.pre_rise_2**,110%,实际涨幅也就是不超过10%)
+  2. 寻找出现强势上涨(**g.strong_limit**,涨幅>6%)的交易日
+  3. 次日高开后出现回落(**g.trap_limit**,高开>6%且未能收回),形成"倒挂"形态
+  4. 二次优化筛选:
+     - 量能要求:根据**g.volume_control_2**设置的模式进行筛选
+       - 模式1:周期放量(相对**g.volume_period_2**天内的平均量)
+       - 模式2:周期倍量(是**g.volume_period_2**天内最高量的**g.volume_ratio_2**倍)
+       - 模式3:相对昨日倍量
+       - 模式4:放量(240天-0.9)加倍量(20天-5)的叠加控制
+     - 流通市值限制:不超过**g.cirm_up**(45亿)
+     - 优化类型(**g.filt_type**):
+       - 'B':基本面(PE、PB等指标)
+       - 'V':量价面(成交量、涨跌幅等)
+       - 'S':静态(以流通市值为主要参考)
+
+##### 3.2 买入条件
+- 竞价开盘价低于昨日收盘价的**g.auction_open_highlimit_2**(98%)
+- 按照换手率排序,选择换手率最低的前50%标的(防止追高)
+
+#### 4. 仓位管理
+
+##### 4.1 持仓数量控制
+- 参数 **g.stocknum** 控制最大持仓数:
+  - 0:不限制持仓数量,根据满足条件的股票数量决定
+  - 2/4/8:固定持仓数量,因为两个策略池各自独立,所以实际持仓会是这个数字的2倍
+
+##### 4.2 买入资金分配
+- 标准仓位计算:
+  - 基础标准仓位 = 可用资金 / **g.stocknum**(如果**g.stocknum**为0,则按实际数量)
+  - 单个标的最大持仓限制 = 基础标准仓位 * 3
+- 实际买入金额 = min(单个标的最大持仓限制, max(标的10日平均成交额/10, 基础标准仓位))
+
+#### 5. 风险控制
+
+##### 5.1 单股风控
+- 止损条件:单只股票亏损超过**g.loss_limit**(10%)时强制止损
+- 冷却期:止损后**g.drop_limit_days**(20个交易日)内不再买入该股票
+
+##### 5.2 整体风控
+- 回撤监控:监控最近**g.total_limit_days**(30天)的最大回撤
+- 清仓条件:当整体回撤超过**g.total_limit_rate**(15%)时清仓
+- 冷却期:清仓后设置**g.control_days** = **g.cool_days**进行冷却期控制
+
+##### 5.3 市场状态监控
+- 折价股票占比:维护最近10天的折价股票占比列表**g.rate_list**
+- 清仓条件:当**g.rate_list**的平均值低于0.1(10%)时,清仓观望
+- 节假日风控:节假日前自动清仓,节假日期间不开新仓
+
+### 交易规则
+- 采用异步报单模式
+- 使用真实价格(动态复权)
+- 考虑交易成本:
+  - 买入佣金:0.03%
+  - 卖出佣金:0.03%
+  - 印花税:0.1%
+  - 最低佣金:5元/笔

+ 713 - 0
Lib/stock/daxiaowai_and_guojiuxiao.py

@@ -0,0 +1,713 @@
+# 克隆自聚宽文章:https://www.joinquant.com/post/50551
+# 标题:大小外择时小市值2.0
+# 作者:DanD
+
+from jqdata import *
+from jqfactor import *
+import numpy as np
+import pandas as pd
+import pickle
+import talib
+import warnings
+from jqlib.technical_analysis import *
+warnings.filterwarnings("ignore")
+
+# 初始化函数
+def initialize(context):
+    # 设定基准
+    set_benchmark('000300.XSHG')
+    # 用真实价格交易
+    set_option('use_real_price', True)
+    # 打开防未来函数
+    set_option("avoid_future_data", True)
+    # 将滑点设置为0.246%
+    set_slippage(PriceRelatedSlippage(0.00246),type='stock')
+    # 设置交易成本万分之三,不同滑点影响可在归因分析中查看
+    set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003,
+                             close_today_commission=0, min_commission=5), type='stock')
+    # 过滤order中低于error级别的日志
+    log.set_level('order', 'error')
+    # 初始化全局变量
+    g.no_trading_today_signal = False
+    g.market_temperature = "warm"
+    g.stock_num = 3
+    g.highest = 50 
+    g.buy_stock_count = 5
+    g.hold_list = []  # 当前持仓的全部股票
+    g.yesterday_HL_list = []  # 记录持仓中昨日涨停的股票
+    g.bought_stocks = {} #记录补跌的股票和金额
+    g.foreign_ETF = [
+        '518880.XSHG', # 黄金ETF
+        '513030.XSHG', # 德国ETF
+        '513100.XSHG', # 纳指ETF
+        '164824.XSHE', # 印度LOF
+        '159866.XSHE', # 日经ETF
+        ]
+    # 设置交易运行时间
+    run_daily(prepare_stock_list, '9:05')
+    run_monthly(singal, 1, '9:00')
+    run_weekly(clear, 1, '9:30')
+    run_weekly(monthly_adjustment, 1, '9:30')
+    run_daily(stop_loss, '14:00')
+
+
+def clear(context):#卖出补跌的仓位
+    log.info(f"g.bought_stocks: {g.bought_stocks}")
+    if g.bought_stocks!={}:
+        for stock, amount in g.bought_stocks.items():
+            if stock in context.portfolio.positions:
+                order_value(stock, -amount)  # 卖出股票至目标价值为0
+                log.info("Sell cover stocks: %s, value: %s" % (stock, amount))
+            # 清空记录
+        g.bought_stocks.clear()
+
+def prepare_stock_list(context):
+    """
+    准备每日股票列表,包括持仓股票列表和昨日涨停股票列表。
+
+    1. 遍历当前持仓中的所有股票,将其加入到 g.hold_list 中。
+    2. 如果持仓列表不为空:
+       - 获取持仓股票在前一个交易日的收盘价和涨停价。
+       - 过滤出收盘价等于涨停价的股票,存入昨日涨停列表 g.yesterday_HL_list。
+    3. 如果持仓列表为空,昨日涨停列表 g.yesterday_HL_list 也置为空。
+    
+    返回:
+     - g.hold_list: 持仓股票清单
+     - g.yesterday_HL_list: 持仓的昨日涨停清单
+    """
+    log.info('Daily run is enabled')
+    # 获取已持有列表
+    g.hold_list = []
+    for position in list(context.portfolio.positions.values()):
+        stock = position.security
+        g.hold_list.append(stock)
+    log.info(f"Day position {len(g.hold_list)}:{g.hold_list}")
+    # 获取昨日涨停列表
+    if g.hold_list != []:
+        df = get_price(g.hold_list, end_date=context.previous_date, frequency='daily', fields=['close', 'high_limit'],
+                       count=1, panel=False, fill_paused=False)
+        df = df[df['close'] == df['high_limit']]
+        g.yesterday_HL_list = list(df.code)
+    else:
+        g.yesterday_HL_list = []
+        
+def stop_loss(context):
+    """
+    执行止损策略,根据条件卖出股票或进行补仓。
+
+    1. 初始化计数器 num 用于记录卖出股票的数量和获取当前时间 now_time。
+    2. 如果昨日涨停列表 g.yesterday_HL_list 不为空:
+       - 对于每只昨日涨停的股票,获取当前价格和涨停价。
+       - 如果当前价格低于涨停价,打印日志信息并卖出该股票,计数器 num 加1。
+       - 如果当前价格等于涨停价,打印日志信息并继续持有该股票。
+    3. 初始化两个列表 SS 和 S 用于记录持仓股票的跌幅。
+    4. 遍历持仓列表 g.hold_list:
+       - 如果股票在当前持仓中,并且当前价格低于平均成本价的92%,执行止损卖出操作,计数器 num 加1。
+       - 否则,将股票加入列表 S,并计算当前价格相对于平均成本价的涨跌幅,存入 SS。
+    5. 如果有股票被止损卖出(num >= 1),且 SS 列表不为空:
+       - 重置 num 为3。
+       - 找出 SS 中跌幅最大的 num 支股票。
+       - 计算每只股票的补仓金额为当前现金除以 num。
+       - 对于跌幅最大的股票,执行补仓操作,并记录日志信息。如果股票不在已买入列表 g.bought_stocks 中,更新其买入金额。
+    """
+    num = 0
+    now_time = context.current_dt
+    if g.yesterday_HL_list != []:
+        # 对昨日涨停股票观察到尾盘如不涨停则提前卖出,如果涨停即使不在应买入列表仍暂时持有
+        for stock in g.yesterday_HL_list:
+            current_data = get_price(stock, end_date=now_time, frequency='1m', fields=['close', 'high_limit'],
+                                     skip_paused=False, fq='pre', count=1, panel=False, fill_paused=True)
+            if current_data.iloc[0, 0] < current_data.iloc[0, 1]:
+                log.info("[%s]Limit ends, sell" % (stock))
+                position = context.portfolio.positions[stock]
+                close_position(position)
+                num = num+1
+            else:
+                log.info("[%s]Limit continues, continue to hold" % (stock))
+    SS=[]
+    S=[]
+    for stock in g.hold_list:
+        if stock in list(context.portfolio.positions.keys()):
+            if context.portfolio.positions[stock].price < context.portfolio.positions[stock].avg_cost * 0.92:
+                order_target_value(stock, 0)
+                log.info("Loss control: Selling out %s" % (stock))
+                num = num+1
+            else:
+                S.append(stock)
+                NOW = (context.portfolio.positions[stock].price - context.portfolio.positions[stock].avg_cost)/context.portfolio.positions[stock].avg_cost
+                SS.append(np.array(NOW))
+    else:
+        log.info(f"stop_loss else after for loop, num: {num}, SS: {len(SS)}")
+        if num >=1:
+            if len(SS) > 0:
+                # 清空记录
+                num=3
+                min_values = sorted(SS)[:num]
+                min_indices = [SS.index(value) for value in min_values]
+                min_strings = [S[index] for index in min_indices]
+                log.info(f"Need to cover positions are min_strings: {min_strings}, min_indices: {min_indices}")
+                cash = context.portfolio.cash/num
+                for ss in min_strings:
+                    cover_order = order_value(ss, cash)
+                    if cover_order:
+                        log.info("Cover the n that fell the most with Order %s and %.2f" % (ss, cash))
+                        log.debug(f"cover_order: {cover_order}")
+                        amount = cover_order.amount
+                        log.debug(f"amount: {type(amount)}, {amount}")
+                        if ss not in g.bought_stocks:
+                            g.bought_stocks[ss] = amount
+                        else:
+                            g.bought_stocks[ss] = amount + g.bought_stocks[ss]
+                log.info(f"g.bought_stocks: {g.bought_stocks}")
+
+def filter_roic(context,stock_list):
+    """
+    根据最近一期的ROIC(投入资本回报率)过滤股票列表。
+
+    1. 获取前一个交易日的日期。
+    2. 遍历输入的股票列表 stock_list,对于每只股票:
+       - 获取该股票最近一期的 ROIC 值。
+       - 如果 ROIC 大于 0.08,将该股票添加到 list 中。
+    """
+    yesterday = context.previous_date
+    list=[]
+    for stock in stock_list:
+        roic=get_factor_values(stock, 'roic_ttm', end_date=yesterday,count=1)['roic_ttm'].iloc[0,0]
+        if roic>0.08:
+            list.append(stock)
+    return list
+    
+def filter_highprice_stock(context,stock_list):
+    # 过滤出最近一分钟的收盘价低于10元的股票,或当前已持有的股票。
+	last_prices = history(1, unit='1m', field='close', security_list=stock_list)
+	return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
+			or last_prices[stock][-1] < 10]
+			
+def filter_highprice_stock2(context,stock_list):
+    # 过滤出最近一分钟的收盘价低于300元的股票,或当前已持有的股票。
+	last_prices = history(1, unit='1m', field='close', security_list=stock_list)
+	return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
+			or last_prices[stock][-1] < 300]
+			
+def get_recent_limit_up_stock(context, stock_list, recent_days):
+    # 获取最近指定天数内出现过涨停的股票。
+    stat_date = context.previous_date
+    new_list = []
+    for stock in stock_list:
+        df = get_price(stock, end_date=stat_date, frequency='daily', fields=['close','high_limit'], count=recent_days, panel=False, fill_paused=False)
+        df = df[df['close'] == df['high_limit']]
+        if len(df) > 0:
+            new_list.append(stock)
+    return new_list
+    
+def get_recent_down_up_stock(context, stock_list, recent_days):
+    """获取最近指定天数内出现过跌停的股票。"""
+    stat_date = context.previous_date
+    new_list = []
+    for stock in stock_list:
+        df = get_price(stock, end_date=stat_date, frequency='daily', fields=['close','low_limit'], count=recent_days, panel=False, fill_paused=False)
+        df = df[df['close'] == df['low_limit']]
+        if len(df) > 0:
+            new_list.append(stock)
+    return new_list
+
+#1-2 选股模块
+def get_stock_list(context):
+    """
+    小市值选股逻辑:
+    针对399101里的股票,过滤次新、科创北交、st,先获取实质5-30的最小市值100支股票
+    再过滤掉停牌、当前涨停和跌停的股票
+    最后获得市值最小的50支股票
+    """
+    final_list = []
+    MKT_index = '399101.XSHE'
+    initial_list = get_index_stocks(MKT_index)
+    initial_list = filter_new_stock(context, initial_list)
+    initial_list = filter_kcbj_stock(initial_list)
+    initial_list = filter_st_stock(initial_list)
+    
+    q = query(valuation.code,valuation.market_cap).filter(valuation.code.in_(initial_list),valuation.market_cap.between(5,30)).order_by(valuation.market_cap.asc())
+    df_fun = get_fundamentals(q)
+    df_fun = df_fun[:100]
+    
+    initial_list = list(df_fun.code)
+    initial_list = filter_paused_stock(initial_list)
+    initial_list = filter_limitup_stock(context, initial_list)
+    initial_list = filter_limitdown_stock(context, initial_list)
+    #log.info('initial_list has {} stocks'.format(len(initial_list)))
+    q = query(valuation.code,valuation.market_cap).filter(valuation.code.in_(initial_list)).order_by(valuation.market_cap.asc())
+    df_fun = get_fundamentals(q)
+    df_fun = df_fun[:50]
+    final_list  = list(df_fun.code)
+    return final_list
+
+
+#1-2 选股模块
+def get_stock_list_2(context):
+    """
+    1. 获取综小中指的成分股 initial_list。
+    2. 对 initial_list 进行一系列过滤操作:
+       - 过滤掉新股。
+       - 过滤掉科创北交股票。
+       - 过滤掉ST股票。
+    5. 构建查询条件 q,筛选出符合以下条件的股票:
+       - 股票代码在 initial_list 中。
+       - 总市值在5亿到30亿之间。
+       - 归属于母公司所有者的净利润为正。
+       - 净利润为正。
+       - 营业收入大于1亿。
+    6. 按总市值升序排序,限制返回最多50只股票。
+    7. 获取基本面数据 df,并从中提取股票代码列表 final_list。
+    8. 获取 final_list 中股票最近一天的收盘价。
+    9. 返回符合以下条件的股票列表:
+       - 当前持有的股票。
+       - 最近一天收盘价不高于全局变量 g.highest 的股票。
+    """
+    final_list = []
+    MKT_index = '399101.XSHE'
+    initial_list = get_index_stocks(MKT_index)
+    initial_list = filter_new_stock(context, initial_list)
+    initial_list = filter_kcbj_stock(initial_list)
+    initial_list = filter_st_stock(initial_list)
+    # 国九更新:过滤近一年净利润为负且营业收入小于1亿的
+    # 国九更新:过滤近一年期末净资产为负的 (经查询没有为负数的,所以直接pass这条)
+    # 国九更新:过滤近一年审计建议无法出具或者为负面建议的 (经过净利润等筛选,审计意见几乎不会存在异常)
+    q = query(
+        valuation.code,
+        valuation.market_cap,  # 总市值 circulating_market_cap/market_cap
+        income.np_parent_company_owners,  # 归属于母公司所有者的净利润
+        income.net_profit,  # 净利润
+        income.operating_revenue  # 营业收入
+        #security_indicator.net_assets
+    ).filter(
+        valuation.code.in_(initial_list),
+        valuation.market_cap.between(5,30),
+        income.np_parent_company_owners > 0,
+        income.net_profit > 0,
+        income.operating_revenue > 1e8
+    ).order_by(valuation.market_cap.asc()).limit(50)
+    
+    df = get_fundamentals(q)
+    
+    final_list = list(df.code)
+    last_prices = history(1, unit='1d', field='close', security_list=final_list)
+    
+
+    return [stock for stock in final_list if stock in g.hold_list or last_prices[stock][-1] <= g.highest]
+
+    
+def SMALL(context, choice):
+    """
+    获取经过多重筛选后的股票列表,并根据财务指标进行进一步筛选。
+
+    1. 获取两组初步筛选的股票列表 target_list_1 和 target_list_2。
+    2. 合并去重这两组股票列表,得到 target_list。
+    3. 限制 target_list 的长度为全局变量 g.stock_num 的三倍。
+    4. 构建查询条件,筛选出符合以下条件的股票:
+       - 股票代码在 target_list 中。
+       - 按总市值升序排序。
+    5. 获取基本面数据,并提取股票代码列表 final_list。
+    6. 返回最终筛选的股票列表 final_list。
+    """
+    target_list_1 = get_stock_list(context)
+    target_list_2 = get_stock_list_2(context)
+    target_list= list(dict.fromkeys(target_list_1 + target_list_2))
+    target_list=target_list[:g.stock_num*3]
+    #target_list = get_stock_list_2(context)[:g.stock_num*3]
+    final_list = get_fundamentals(query(
+        valuation.code,
+        indicator.roe,
+        indicator.roa,
+    ).filter(
+        valuation.code.in_(target_list),
+        #valuation.pb_ratio<1
+    ).order_by(
+        valuation.market_cap.asc()
+    )).set_index('code').index.tolist()
+    return final_list
+    
+def singal(context):
+    """
+    1. 首先获得B_stocks,沪深300指数的成分股和S_stocks,中小综指的成分股,去掉科创北上、st、次新股
+    2. 从B_stocks当中获得市值最大的20支股,从S_stocks当中获得市值最小的20支股
+    3. 针对B_stocks和S_stocks,使用最新的价格和过去的价格比值*100,各自获得一个平均值B_mean,S_mean
+    4. g.signal的4种情况
+     - B_mean>S_mean且B_mean>5:small
+     - B_mean>S_mean且5>=B_mean>0:big
+     - B_mean<S_mean且S_mean>0:small
+     - 所有其他:etf (黄金或外盘)
+     
+    返回:
+     - g.singal: small, big, etf三种投资标的的选择
+    """
+    today = context.current_dt
+    dt_last = context.previous_date
+    N=10
+    B_stocks = get_index_stocks('000300.XSHG', dt_last)
+    B_stocks = filter_kcbj_stock(B_stocks)
+    B_stocks = filter_st_stock(B_stocks)
+    B_stocks = filter_new_stock(context, B_stocks)
+    
+    S_stocks = get_index_stocks('399101.XSHE', dt_last)
+    S_stocks = filter_kcbj_stock(S_stocks)
+    S_stocks = filter_st_stock(S_stocks)
+    S_stocks = filter_new_stock(context, S_stocks)
+    
+    q = query(
+        valuation.code, valuation.circulating_market_cap
+    ).filter(
+        valuation.code.in_(B_stocks)
+    ).order_by(
+        valuation.circulating_market_cap.desc()
+    )
+    df = get_fundamentals(q, date=dt_last)
+    Blst = list(df.code)[:20]
+    
+    q = query(
+        valuation.code, valuation.circulating_market_cap
+    ).filter(
+        valuation.code.in_(S_stocks)
+    ).order_by(
+        valuation.circulating_market_cap.asc()
+    )
+    df = get_fundamentals(q, date=dt_last)
+    Slst = list(df.code)[:20]
+    #
+    B_ratio = get_price(Blst, end_date=dt_last, frequency='1d', fields=['close'], count=N, panel=False
+                        ).pivot(index='time', columns='code', values='close')
+    change_BIG = (B_ratio.iloc[-1] / B_ratio.iloc[0] - 1) * 100
+    A1 = np.array(change_BIG)
+    A1 = np.nan_to_num(A1)  
+    B_mean = np.mean(A1)
+    # log.info(f"B_ratio: {B_ratio}")
+    # log.info(f"change_BIG: {change_BIG}")
+    # log.info(f"A1: {A1}")
+    # log.info(f"B_mean: {B_mean}")
+    
+    S_ratio = get_price(Slst, end_date=dt_last, frequency='1d', fields=['close'], count=N, panel=False
+                        ).pivot(index='time', columns='code', values='close')
+    change_SMALL = (S_ratio.iloc[-1] / S_ratio.iloc[0] - 1) * 100
+    A1 = np.array(change_SMALL)
+    A1 = np.nan_to_num(A1)
+    S_mean = np.mean(A1)
+    if B_mean>S_mean and B_mean>0:
+        if B_mean>5:
+           g.signal='small'
+           log.info('Big has ended, change to Small') 
+        else:
+            g.signal='big'
+            log.info('Big')
+    elif B_mean < S_mean and S_mean > 0:
+        g.signal='small'
+        log.info('Small')
+    else:
+        log.info('Foreign')
+        g.signal='etf'
+
+        
+# 1-3 整体调整持仓
+def monthly_adjustment(context):
+    """
+    每月(目前是每周)进行整体持仓调整,根据市场信号调整股票组合,进行买入和卖出操作。
+
+    1. 根据全局信号 g.signal 确定调整策略:
+       - 如果信号为 'big',调用 White_Horse 方法获取大市值股票的目标列表。
+       - 如果信号为 'small',从中小综指获取成分股,过滤掉科创板、ST和次新股后,调用 SMALL 方法获取小市值股票的目标列表。
+       - 如果信号为 'etf',使用全局变量 g.foreign_ETF 作为目标列表。
+       - 如果信号不在预期范围内,打印提示信息。
+    5. 打印目标股票列表 target_list。
+    6. 对目标列表进行进一步的过滤:
+       - 使用 filter_limitup_stock 过滤掉涨停的股票。
+       - 使用 filter_limitdown_stock 过滤掉跌停的股票。
+       - 使用 filter_paused_stock 过滤掉停牌的股票。
+    7. 遍历当前持仓的股票 g.hold_list:
+       - 对于不在目标列表和昨日持仓列表 g.yesterday_HL_list 中的股票,平掉其仓位。
+    8. 获取当前持仓数量 position_count 和目标股票数量 target_num。
+    9. 如果目标股票数量大于当前持仓数量:
+       - 计算每个新股票的投资金额 value 为当前现金除以新增目标股票数量。
+       - 遍历目标股票列表 target_list,对于未持有的股票,尝试开仓。
+       - 如果开仓成功且持仓数量达到目标数量,停止操作。
+    """
+    log.info("Make weekly overall position adjustments")
+    today = context.current_dt
+    dt_last = context.previous_date
+    target_list=[]
+    log.info(f"g.signal: {g.signal}")
+    if g.signal=='big':
+        target_list = White_Horse(context)
+        
+    elif g.signal=='small':
+        S_stocks = get_index_stocks('399101.XSHE', dt_last)
+        S_stocks = filter_kcbj_stock(S_stocks)
+        S_stocks = filter_st_stock(S_stocks)
+        S_stocks = filter_new_stock(context, S_stocks)       
+        choice = S_stocks
+        target_list = SMALL(context,choice)
+
+        
+    elif g.signal=='etf':
+        target_list = g.foreign_ETF
+    else:
+        log.info("g.signal is not the one expected")    
+    
+    log.info(f"target_list before filter: {target_list}")
+    target_list = filter_limitup_stock(context,target_list)
+    target_list = filter_limitdown_stock(context,target_list)
+    target_list = filter_paused_stock(target_list)
+    log.info(f"target_list after filter: {target_list}")
+    # target_list = target_list[:5] # 可以控制持股数量到5
+    for stock in g.hold_list:
+        if (stock not in target_list) and (stock not in g.yesterday_HL_list):
+            position = context.portfolio.positions[stock]
+            close_position(position)
+    position_count = len(context.portfolio.positions)
+    target_num = len(target_list)
+    if target_num > position_count:
+        value = context.portfolio.cash / (target_num - position_count)
+        for stock in target_list:
+            if stock not in list(context.portfolio.positions.keys()):
+                if open_position(stock, value):
+                    if len(context.portfolio.positions) == target_num:
+                        break
+            
+def boll_filter(stocks,date):
+    """
+    使用布林带策略过滤股票列表,返回符合条件的股票。
+
+    1. 获取指定日期 date 的股票数据,包括高价、高价和收盘价。
+       - 使用 get_bars 函数获取 stocks 列表中每只股票的最近一天数据。
+       - 将数据的索引设置为股票代码。
+
+    2. 计算布林带指标:
+       - 使用 Bollinger_Bands 函数计算上轨(upperband)、中轨(middleband)和下轨(lowerband)。
+       - 参数设置为:时间周期为20,标准差倍数为2,单位为每日。
+
+    3. 将布林带指标结果添加到数据框 x 中:
+       - 将 upperband、middleband 和 lowerband 的结果分别存储到 x 数据框中的 'up'、'mid' 和 'lowe' 列。
+
+    4. 过滤符合条件的股票:
+       - 筛选出收盘价低于上轨且最低价高于下轨的股票。
+       - 这些股票符合布林带策略的买入信号。
+
+    5. 返回符合条件的股票代码列表。
+
+    参数:
+    - stocks: 股票代码列表。
+    - date: 筛选的日期。
+
+    返回:
+    - 符合布林带策略条件的股票代码列表。
+    """
+    x=get_bars(stocks, 1, unit='1d', fields=['high','low','close'],end_dt=date,df=True)
+    x.index=stocks
+    upperband, middleband, lowerband=Bollinger_Bands(stocks, date, timeperiod=20, 
+                        nbdevup=2, nbdevdn=2, unit = '1d', include_now = True, fq_ref_date = None)
+    log.info(f"result for bollinger_bands, upperband: {type(upperband)}, {upperband}; middleband: {type(middleband)}, {middleband}; lowerband: {type(lowerband)}, {lowerband}")
+    x['up']= pd.DataFrame(upperband, index=[0]).T.values
+    x['mid']=pd.DataFrame(middleband, index=[0]).T.values
+    x['lowe']=pd.DataFrame(lowerband, index=[0]).T.values
+    x=x[(x['close']<x['up'])&(x['lowe']<x['low'])]
+    return(list(x.index))
+
+# 3-1 交易模块-自定义下单
+def order_target_value_(security, value):
+    if value == 0:
+        log.info("Selling out %s" % (security))
+    else:
+        log.info("Order %s to value %f" % (security, value))
+    return order_target_value(security, value)
+
+# 3-2 交易模块-开仓
+def open_position(security, value):
+    order = order_target_value_(security, value)
+    if order != None and order.filled > 0:
+        return True
+    return False
+
+# 3-3 交易模块-平仓
+def close_position(position):
+    security = position.security
+    order = order_target_value_(security, 0)  # 可能会因停牌失败
+    if order != None:
+        if order.status == OrderStatus.held and order.filled == order.amount:
+            return True
+    return False
+
+
+def filter_paused_stock(stock_list):
+    current_data = get_current_data()
+    return [stock for stock in stock_list if not current_data[stock].paused]
+
+# 2-2 过滤ST及其他具有退市标签的股票
+def filter_st_stock(stock_list):
+    current_data = get_current_data()
+    return [stock for stock in stock_list
+            if not current_data[stock].is_st
+            and 'ST' not in current_data[stock].name
+            and '*' not in current_data[stock].name
+            and '退' not in current_data[stock].name]
+
+
+# 2-3 过滤科创北交股票
+def filter_kcbj_stock(stock_list):
+    for stock in stock_list[:]:
+        if stock[0] == '4' or stock[0] == '8' or stock[:2] == '68' or stock[0] == '3':
+            stock_list.remove(stock)
+    return stock_list
+
+
+# 2-4 过滤涨停的股票
+def filter_limitup_stock(context, stock_list):
+    last_prices = history(1, unit='1m', field='close', security_list=stock_list)
+    current_data = get_current_data()
+    return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
+            or last_prices[stock][-1] < current_data[stock].high_limit]
+
+
+# 2-5 过滤跌停的股票
+def filter_limitdown_stock(context, stock_list):
+    last_prices = history(1, unit='1m', field='close', security_list=stock_list)
+    current_data = get_current_data()
+    return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
+            or last_prices[stock][-1] > current_data[stock].low_limit]
+
+
+# 2-6 过滤次新股
+def filter_new_stock(context, stock_list):
+    yesterday = context.previous_date
+    return [stock for stock in stock_list if
+            not yesterday - get_security_info(stock).start_date < datetime.timedelta(days=375)]
+
+## 开盘前运行函数
+def White_Horse(context):
+    """
+    该方法根据市场温度筛选出符合特定财务指标的股票列表。
+
+    1. 首先调用 Market_temperature 函数获取市场温度。
+    2. 初始化一个空列表 check_out_lists 用于存储筛选出的股票。
+    3. 获取当前的股票数据 current_data,以及200天前的所有股票列表 all_stocks。
+    4. 将 all_stocks 更新为沪深300指数的成分股。
+    5. 根据以下条件过滤股票:
+       - 排除涨停或跌停开盘的股票。
+       - 排除停牌的股票。
+       - 排除 ST、*ST、退市风险股。
+       - 排除创业板、科创板、北交所的股票。
+    6. 根据市场温度(冷、暖、热)设置不同的筛选条件和排序规则:
+       - 冷:筛选市净率(PB)在0到1之间,经营活动现金流入大于0,调整后利润大于0,现金流入与调整后利润比大于2.0,收益增长率大于1.5,净利润同比增长率大于-15的股票,并按 ROA/PB 降序排列。
+       - 暖:筛选市净率(PB)在0到1之间,经营活动现金流入大于0,调整后利润大于0,现金流入与调整后利润比大于1.0,收益增长率大于2.0,净利润同比增长率大于0的股票,并按 ROA/PB 降序排列。
+       - 热:筛选市净率(PB)大于3,经营活动现金流入大于0,调整后利润大于0,现金流入与调整后利润比大于0.5,收益增长率大于3.0,净利润同比增长率大于20的股票,并按 ROA 降序排列。
+    7. 将符合条件的股票代码存入 check_out_lists。
+    8. 返回筛选出的股票列表 check_out_lists,并记录日志信息。
+    """
+    Market_temperature(context)
+    log.info(f"This month's temperature is:{g.market_temperature}")
+    check_out_lists = []
+    current_data = get_current_data()
+    check_date = context.previous_date - datetime.timedelta(days=200)
+    # all_stocks = list(get_all_securities(date=check_date).index)
+    # log.info(f"first all_stocks: {len(all_stocks)}, {all_stocks}")
+    all_stocks = get_index_stocks("000300.XSHG")
+    # log.info(f"second all_stocks: {len(all_stocks)}, {all_stocks}")
+    # 过滤创业板、ST、停牌、当日涨停
+    all_stocks = [stock for stock in all_stocks if not (
+            (current_data[stock].day_open == current_data[stock].high_limit) or  # 涨停开盘
+            (current_data[stock].day_open == current_data[stock].low_limit) or  # 跌停开盘
+            current_data[stock].paused or  # 停牌
+            current_data[stock].is_st or  # ST
+            ('ST' in current_data[stock].name) or
+            ('*' in current_data[stock].name) or
+            ('退' in current_data[stock].name) or
+            (stock.startswith('30')) or  # 创业
+            (stock.startswith('68')) or  # 科创
+            (stock.startswith('8')) or  # 北交
+            (stock.startswith('4'))   # 北交
+    )]
+    if g.market_temperature == "cold":
+        q = query(
+            valuation.code, 
+            ).filter(
+            valuation.pb_ratio > 0,
+            valuation.pb_ratio < 1,
+            cash_flow.subtotal_operate_cash_inflow > 0,
+            indicator.adjusted_profit > 0,
+            cash_flow.subtotal_operate_cash_inflow/indicator.adjusted_profit>2.0,
+            indicator.inc_return > 1.5,
+            indicator.inc_net_profit_year_on_year > -15,
+        	valuation.code.in_(all_stocks)
+        	).order_by(
+        	(indicator.roa/valuation.pb_ratio).desc()
+        ).limit(
+        	g.buy_stock_count + 1
+        )
+    elif g.market_temperature == "warm":
+        q = query(
+            valuation.code, 
+            ).filter(
+            valuation.pb_ratio > 0,
+            valuation.pb_ratio < 1,
+            cash_flow.subtotal_operate_cash_inflow > 0,
+            indicator.adjusted_profit > 0,
+            cash_flow.subtotal_operate_cash_inflow/indicator.adjusted_profit>1.0,
+            indicator.inc_return > 2.0,
+            indicator.inc_net_profit_year_on_year > 0,
+        	valuation.code.in_(all_stocks)
+        	).order_by(
+        	(indicator.roa/valuation.pb_ratio).desc()
+        ).limit(
+        	g.buy_stock_count + 1
+        )
+    elif g.market_temperature == "hot":
+        q = query(
+            valuation.code, 
+            ).filter(
+ 
+            valuation.pb_ratio > 3,
+            cash_flow.subtotal_operate_cash_inflow > 0,
+            indicator.adjusted_profit > 0,
+            cash_flow.subtotal_operate_cash_inflow/indicator.adjusted_profit>0.5,
+            indicator.inc_return > 3.0,
+            indicator.inc_net_profit_year_on_year > 20,
+        	valuation.code.in_(all_stocks)
+        	).order_by(
+        	indicator.roa.desc()
+        ).limit(
+        	g.buy_stock_count + 1
+        )
+                
+    check_out_lists = list(get_fundamentals(q).code)
+    # 取需要的只数
+    #check_out_lists = check_out_lists[:g.buy_stock_count]
+    log.info("Today's stock pool:%s" % check_out_lists)
+    return check_out_lists
+    #  tttttttttttt
+    
+def Market_temperature(context):
+    """
+    根据沪深300过去220天的收盘价,计算一个权重market_height:
+    (最新5个的平均值-最小值)/(最大值-最小值)
+     - 冷:market_height小于0.2,temp为200
+     - 热:market_height大于0.9,temp为400
+     - 温:最新60个的平均值/最小值>1.2,temp为300
+    """
+    
+    index300 = attribute_history('000300.XSHG', 220, '1d', ('close'), df=False)['close']
+    market_height = (mean(index300[-5:]) - min(index300)) / (max(index300) - min(index300))
+    if market_height < 0.20:
+        g.market_temperature = "cold"
+
+    elif market_height > 0.90:
+        g.market_temperature = "hot"
+
+    elif max(index300[-60:]) / min(index300) > 1.20:
+        g.market_temperature = "warm"
+
+    
+    if g.market_temperature == "cold":
+        temp = 200
+    elif g.market_temperature == "warm":
+        temp = 300
+    else:
+        temp = 400
+        
+    if context.run_params.type != 'sim_trade': # 不是模拟交易
+        record(temp=temp)

+ 576 - 0
Lib/stock/strategy_1.py

@@ -0,0 +1,576 @@
+# 克隆自聚宽文章:https://www.joinquant.com/post/53374
+# 标题:机位有限,实盘策略送人
+# 作者:韶华不负
+
+# 导入函数库
+from jqdata import *
+from six import BytesIO
+from jqlib.technical_analysis  import *
+from jqfactor import get_factor_values
+import numpy as np
+import pandas as pd 
+import time
+
+# 初始化函数,设定基准等等
+def after_code_changed(context):
+    # 输出内容到日志 log.info()
+    log.info('初始函数开始运行且全局只运行一次')
+    unschedule_all()
+    # 过滤掉order系列API产生的比error级别低的log
+    # log.set_level('order', 'error')
+    set_params()    #1 设置策略参数
+    set_variables() #2 设置中间变量
+    set_backtest()  #3 设置回测条件
+    disable_cache()
+
+    ### 股票相关设定 ###
+    # 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱
+    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
+
+    ## 运行函数(reference_security为运行时间的参考标的;传入的标的只做种类区分,因此传入'000300.XSHG'或'510300.XSHG'是一样的)
+      # 开盘前运行
+    run_daily(before_market_open, time='6:00')
+      # 竞价时运行
+    run_daily(call_auction, time='09:25:15')
+      # 开盘时运行-测试不同的买入时间
+    #run_daily(market_open, time='09:45')
+      # 收盘时运行
+    run_daily(market_run, time='14:55')
+      # 收盘后运行
+    #run_daily(after_market_close, time='20:00')
+          # 收盘后运行
+    #run_daily(after_market_analysis, time='21:00')
+
+#1 设置策略参数
+def set_params():
+    #设置全局参数
+    g.index ='all'          #all-zz-300-500-1000,single-个股信号诊断
+    g.code = '000679.XSHE'
+    
+    g.switch = 10            #1-低位首板低开,2-首阳倒挂,10-两者合并
+    
+    #池1-低板低开
+    #买前量价过滤
+    g.check_dura_1 =5        #默认是20,可以测试10-60
+    g.pre_rise_1 = 1.1        #前期(20天)的涨幅上限,10等同不设限
+    
+    g.optimz_control = 1     #0-默认不控制,1-采用板型过滤
+    g.volume_control_1 = 2    #0-默认不控制,1-周期放量控制,2-周期倍量控制,3,倍量控制(相对昨日),4-放量(240-0.9)加倍量(20-5)的最佳回测叠加
+    g.volume_period_1 = 20   #放量控制周期,240-120-90-60
+    g.volume_ratio_1 = 5    #放量控制和周期最高量的比值,0.9/0.8
+    #竞价面
+    g.auction_open_highlimit_1 = 0.98  #竞价开盘上限,999-不设限
+    g.auction_open_lowlimit_1 = 0 #竞价开盘下限,0-不设限,1-相当于高开
+    
+    g.begin_times = ' 09:24:55'
+    g.end_times =  ' 09:25:10'
+    
+    #池2-低板倒挂
+    g.check_dura_2 =20        #默认是20,可以测试10-60
+    g.pre_rise_2 = 1.1        #前期(20天)的涨幅上限,10等同不设限
+    g.strong_limit = 1.06   #D日的涨幅下限
+    g.trap_limit = 1.06     #D1日的高开套下限
+    
+    g.filt_type = 'S'       #进行二次择优,B-基本面,V-量价面,S-静态(流通盘)
+    g.cirm_up = 45         #流通盘过滤上线,9999-不设限
+    
+    g.volume_control_2 = 2    #0-默认不控制,1-周期放量控制,2-周期倍量控制,3,倍量控制(相对昨日),4-放量(240-0.9)加倍量(20-5)的最佳回测叠加
+    g.volume_period_2 = 20   #放量控制周期,240-120-90-60
+    g.volume_ratio_2 = 5    #放量控制和周期最高量的比值,0.9/0.8
+    
+    #竞价面
+    g.auction_open_highlimit_2 = 0.98  #竞价开盘上限,999-不设限;设定低开更有效
+    g.auction_turn_highlimit_2 = 0 #竞价开盘下限,0-不设限,1-相当于高开
+
+#2 设置中间变量
+def set_variables():
+    #暂时未用,测试用全池
+    g.stocknum = 0              #持仓数,0-代表全取,2/4/8,因为地板倒和低板低各取1
+    
+#3 设置回测条件
+def set_backtest():
+    ## 设定g.index作为基准
+    if g.index == 'all':
+        set_benchmark('000001.XSHG')
+    elif g.index == 'single':
+        set_benchmark(g.code)
+    else:
+        set_benchmark(g.index)
+    # 开启动态复权模式(真实价格)
+    set_option('use_real_price', True)
+    set_option("avoid_future_data", True)
+    #显示所有列
+    pd.set_option('display.max_columns', None)
+    #显示所有行
+    pd.set_option('display.max_rows', None)
+    log.set_level('order', 'error')    # 设置报错等级
+    
+## 开盘前运行函数
+def before_market_open(context):
+    log.info('----------------------------------------------------------------')
+    log.info('------------------------美好的一天开始了------------------------')
+    log.info('----------------------------------------------------------------')
+    # 输出运行时间
+    log.info('函数运行时间(before_market_open):'+str(context.current_dt.time()))
+    #0,预置全局参数
+    today_date = context.current_dt.date()
+    lastd_date = context.previous_date
+    befor_date = get_trade_days(end_date=today_date, count=3)[0]
+    all_data = get_current_data()
+    g.pool_1 = []
+    g.pool_2 = []
+
+    num1,num2,num3,num4,num5,num6=0,0,0,0,0,0    #用于过程追踪
+            
+    #0,构建基准指数票池,三去+去新
+    start_time = time.time()
+    if g.index =='all':
+        stocklist = list(get_all_securities(['stock']).index)   #取all
+    elif g.index == 'zz':
+        stocklist = get_index_stocks('000300.XSHG', date = None) + get_index_stocks('000905.XSHG', date = None) + get_index_stocks('000852.XSHG', date = None)
+    elif g.index =='single':
+        stocklist = [g.code]    #个股信号诊断
+    else:
+        stocklist = get_index_stocks(g.index, date = None)
+    
+    num1 = len(stocklist)    
+    stocklist = [stockcode for stockcode in stocklist if not all_data[stockcode].paused]
+    stocklist = [stockcode for stockcode in stocklist if not all_data[stockcode].is_st]
+    stocklist = [stockcode for stockcode in stocklist if'退' not in all_data[stockcode].name]
+    stocklist = [stockcode for stockcode in stocklist if stockcode[0:3] != '688']
+    stocklist = [stockcode for stockcode in stocklist if (today_date-get_security_info(stockcode).start_date).days>20]
+    #stocklist = [stockcode for stockcode in stocklist if stockcode[0:1] == '3'] #专为3字头
+    num2 = len(stocklist)
+    
+    end_time = time.time()
+    print('Step0,基准%s,原始%d只,四去后共%d只,构建耗时:%.1f 秒' % (g.index,num1,num2,end_time-start_time))
+    
+    
+    if g.switch ==1 or g.switch ==10:
+        #1.1,池1过滤
+        start_time = time.time()
+        list_201 = get_up_filter_jiang(context,stocklist,lastd_date,1,1,0)
+        list_201 = get_up_filter_jiang(context,list_201,lastd_date,20,1,0)
+        num3 = len(list_201)
+        
+        end_time = time.time()
+        print('Step1.1,%d日首板共%d只,相对位置过滤共%d只,构建耗时:%.1f 秒' % (20,num3,len(list_201),end_time-start_time))
+        log.info(list_201)
+
+        #2.1,池1优化
+        start_time = time.time()
+        list_201 = get_rise_filter(context,list_201,lastd_date,g.check_dura_1,g.pre_rise_1,-1)
+        
+        if g.optimz_control !=0:
+            g.pool_1 = get_up_optimize_filter(context,list_201)
+            num4 = len(g.pool_1)
+        else:
+            g.pool_1 = list_201
+        
+        if g.volume_control_1 !=0:
+            g.pool_1 = get_highvolume_filter(context,g.pool_1,g.volume_control_1,g.volume_period_1,g.volume_ratio_1)
+        else:
+            g.pool_1 = list_201    
+
+        end_time = time.time()
+        print('Step2.1,涨幅限制%.2f共%d只,板型优化共%d只,量能控制共%d只,构建耗时:%.1f 秒' % (g.pre_rise_1,len(list_201),num4,len(g.pool_1),end_time-start_time))
+        log.info('地板待选:%s' % g.pool_1)
+    
+    if g.switch ==2 or g.switch ==10:
+        #1.2,低位票(ROC20<1.1),D日穿线大阳(7点以上)或涨停后,D1日高开套人(开0点上下落7点以上)+不能反包D日,
+        start_time = time.time()
+        list_66 = get_rise_filter(context,stocklist,lastd_date,g.check_dura_2,g.pre_rise_2,-1)
+        num3 = len(list_66)
+        list_66 = get_rainbow_strong_filter(context,list_66,befor_date,g.strong_limit)
+        num4 = len(list_66)
+        list_66 = get_trap_filter(context,list_66,lastd_date,g.trap_limit)
+    
+        end_time = time.time()
+        print('Step1.21,周期涨幅低于%.2f共%d只,前日%.2f强势共%d只,昨日发套%.2f共%d只,构建耗时:%.1f 秒' % (g.pre_rise_2,num3,g.strong_limit,num4,g.trap_limit,len(list_66),end_time-start_time))
+        log.info(list_66)
+        
+        if g.volume_control_2 !=0:
+            list_66 = get_highvolume_filter(context,list_66,g.volume_control_2,g.volume_period_2,g.volume_ratio_2)
+            end_time = time.time()
+            print('Step1.22,量能控制共%d只,构建耗时:%.1f 秒' % (len(list_66),end_time-start_time))
+            log.info(list_66)
+        #2.2,从基本面,量价面进行择优
+        start_time = time.time()
+        if g.filt_type !=False:
+            g.pool_2 = get_trap_optimize_filter(context,list_66,lastd_date,g.filt_type)
+    
+        end_time = time.time()
+        print('Step2.2,%s择优共%d只,构建耗时:%.1f 秒' % (g.filt_type,len(g.pool_2),end_time-start_time))
+        log.info('半岛待选:%s' % g.pool_2)
+
+    
+def call_auction(context):
+    log.info('函数运行时间(Call_auction):'+str(context.current_dt.time()))
+    
+    open_list=[]
+    current_data = get_current_data()
+    today_date = context.current_dt.date()
+    lastd_date = context.previous_date
+    g.buy_list=[]
+    
+    if len(g.pool_2) !=0:
+        df_trap= pd.DataFrame(columns=['code','price','open','turn'])
+        
+        df_auction = get_call_auction(g.pool_2,start_date=today_date,end_date=today_date,fields=['time','current','volume','money'])
+        for i in range(len(df_auction)):
+            stockcode = df_auction.code.values[i]
+            price = df_auction.current.values[i]
+            df_price = get_price(stockcode,end_date=lastd_date,frequency='daily',fields=['close','high'],count=5)
+            open = price/df_price.close[-1]
+            turn = price/df_price.high[-1]
+            df_trap = df_trap.append({'code': stockcode,'price': price,'open': open,'turn': turn}, ignore_index=True)
+            
+        df_trap = df_trap[(df_trap.open < g.auction_open_highlimit_2)] #竞开过滤
+
+        df_trap.sort_values(by='turn', ascending=True, inplace=True)
+        turn_list = list(df_trap.code)[:round(0.5*len(g.pool_2))] #发版
+ 
+
+        g.buy_list = turn_list
+        
+    if len(g.pool_1) !=0:
+        df_auction = get_call_auction(g.pool_1,start_date=today_date,end_date=today_date,fields=['time','current','volume','money'])
+        
+        for i in range(len(df_auction)):
+            stockcode = df_auction.code.values[i]
+            price = df_auction.current.values[i]
+            df_price = get_price(stockcode,end_date=lastd_date,frequency='daily',fields=['close'],count=5)
+            if (price/df_price.close[-1] <g.auction_open_highlimit_1) and  (price/df_price.close[-1] >g.auction_open_lowlimit_1):
+                g.buy_list.append(stockcode)
+    
+    if len(g.buy_list) ==0:
+    #if len(g.buy_list) <=1: #测试
+        log.info('今日无买信')
+        return
+    else:
+        log.info('*****今日买信共%d只*****:' % len(g.buy_list))
+        log.info(g.buy_list)
+    
+    #因信号收集屏蔽,回测打开
+    total_value = context.portfolio.total_value
+    buy_cash = round(0.5*total_value/len(g.buy_list))
+    for stockcode in g.buy_list:
+        if stockcode in list(context.portfolio.positions.keys()):
+            continue
+        buy_stock(context,stockcode,buy_cash)
+    
+    return
+
+## 开盘时运行函数
+def market_open(context):
+    log.info('函数运行时间(market_open):'+str(context.current_dt.time()))
+    today_date = context.current_dt.date()
+    lastd_date = context.previous_date
+    current_data = get_current_data()
+    
+    if len(g.buy_list) ==0:
+        log.info('今日无买信')
+        return
+    else:
+        log.info('*****今日买信共%d只*****:' % len(g.buy_list))
+        log.info(g.buy_list)
+
+    total_value = context.portfolio.total_value
+    buy_cash = 0.5*total_value/len(g.buy_list)
+    for stockcode in g.buy_list:
+        if stockcode in list(context.portfolio.positions.keys()):
+            continue
+        buy_stock(context,stockcode,buy_cash)
+
+## 收盘时运行函数
+def market_run(context):
+    log.info('函数运行时间(market_close):'+str(context.current_dt.time()))
+    today_date = context.current_dt.date()
+    lastd_date = context.previous_date
+    current_data = get_current_data()
+    
+    #尾盘只卖
+    hour = context.current_dt.hour
+    minute = context.current_dt.minute
+    
+    for stockcode in context.portfolio.positions:
+        if current_data[stockcode].paused == True:
+            continue
+        if context.portfolio.positions[stockcode].closeable_amount ==0:
+            continue
+
+        #非停出
+        if current_data[stockcode].last_price != current_data[stockcode].high_limit:
+            log.info('非涨停即出%s' % stockcode)
+            sell_stock(context,stockcode,0)
+            continue
+        
+## 收盘后运行函数
+def after_market_close(context):
+    log.info(str('函数运行时间(after_market_close):'+str(context.current_dt.time())))
+    g.trade_days = g.trade_days +1
+    
+    log.info(g.trade_days)
+    log.info(g.open_num,g.open_nums_days)
+    log.info(g.turn_num,g.turn_nums_days)
+
+
+"""
+---------------------------------函数定义-主要策略-----------------------------------------------
+"""
+ 
+#周期涨跌幅(1/-1)过滤
+def get_rise_filter(context, stocklist, check_date, check_duration,rise_level,direction):
+    # 输出运行时间
+    log.info('-函数运行时间(get_rise_filter):'+str(context.current_dt.time()))
+    poollist =[]
+    
+    if len(stocklist)==0:
+        log.info("输入为空")
+        return poollist
+    
+    by_date = get_trade_days(end_date=check_date, count=check_duration)[0]
+
+    h = get_price(security=stocklist, end_date=check_date, frequency='1d', fields=['close','paused'], count=check_duration, panel=False)
+    if direction == -1:
+        temp = h.groupby('code')['close'].apply(lambda x: x.values[-1]/x.values[-check_duration] <rise_level)
+    elif direction == 1:
+        temp = h.groupby('code')['close'].apply(lambda x: x.values[-1]/x.values[-check_duration] >rise_level)
+        
+    poollist = temp[temp].index.tolist()
+    return poollist
+
+#蒋的方法,N天M涨停过滤
+def get_up_filter_jiang(context,stocklist,check_date,check_duration,up_num,direction):
+    # 输出运行时间
+    log.info('-函数运行时间(get_up_filter_jiang):'+str(context.current_dt.time()))
+    #0,预置,今天是D日
+    all_data = get_current_data()
+    poollist=[]
+    
+    if len(stocklist)==0:
+        log.info("输入为空")
+        return poollist
+    
+    # 交易日历
+    trd_days = get_trade_days(end_date=check_date, count=check_duration)  # array[datetime.date]
+    s_trd_days = pd.Series(range(len(trd_days)), index=trd_days)  # Series[index:交易日期,value:第几个交易日]
+    back_date = trd_days[0]
+    
+    #2,形态过滤,一月内两次以上涨停(盘中过10%也算)
+    start_time = time.time()
+    # 取数
+    df_price = get_price(stocklist,end_date=check_date,frequency='1d',fields=['pre_close','open','close','high','high_limit','low_limit','paused']
+    ,skip_paused=False,fq='pre',count=check_duration,panel=False,fill_paused=True)
+    
+    # 过滤出涨停的股票,按time索引
+    df_up = df_price[(df_price.close == df_price.high_limit) & (df_price.paused == 0)].set_index('time')
+    # 标注出df_up中的time对应的是第几个交易日(ith)
+    df_up['ith'] = s_trd_days
+    
+    code_set = set(df_up.code.values)
+    if direction ==1:
+        poollist =[stockcode for stockcode in code_set if ((len(df_up[df_up.code ==stockcode]) > up_num))]
+    elif direction ==-1:
+        poollist =[stockcode for stockcode in code_set if ((len(df_up[df_up.code ==stockcode]) < up_num))]
+    else:
+        poollist =[stockcode for stockcode in code_set if ((len(df_up[df_up.code ==stockcode]) == up_num))]
+    
+    end_time = time.time()
+    #log.info('---%d天(%s--%s)%d次涨停过滤出%d只标的,构建耗时:%.1f 秒' % (check_duration,back_date,check_date,up_num,len(poollist),end_time-start_time))        
+    #log.info(poollist)
+
+    return poollist
+
+#根据定义的选股条件选股
+def get_rainbow_strong_filter(context,stocklist,check_date,strong_limit):
+    log.info('-函数运行时间(get_rainbow_strong_filter):'+str(context.current_dt.time()))
+    all_data = get_current_data()
+    poollist=[]
+    
+    if len(stocklist)==0:
+        log.info("输入为空")
+        return poollist
+    # 取数
+    df_price = get_price(stocklist,end_date=check_date,frequency='1d',fields=['pre_close','open','close','low','high','high_limit','low_limit','paused']
+    ,skip_paused=False,fq='pre',count=1,panel=False,fill_paused=True)
+    
+    df_up = df_price[(df_price.close > df_price.open) & (df_price.close == df_price.high_limit) & (df_price.paused == 0)]
+    up_list = df_up.code.values.tolist()
+    print('---涨停板过滤后共%d只' % len(up_list))
+    
+    #排除一字板
+    df_strong = df_price[(df_price.close != df_price.high_limit) & (df_price.close > strong_limit*df_price.pre_close) & (df_price.paused == 0)]
+    strong_list = df_strong.code.values.tolist()
+    print('---大阳过滤后共%d只' % len(strong_list))
+    
+    MA5 = MA(strong_list, check_date=check_date, timeperiod=5)
+    MA10 = MA(strong_list, check_date=check_date, timeperiod=10)
+    MA20 = MA(strong_list, check_date=check_date, timeperiod=20)
+
+    strong_list = [stockcode for stockcode in strong_list if ((df_price[df_price.code == stockcode].low.values < MA5[stockcode] and df_price[df_price.code == stockcode
+    ].close.values > MA5[stockcode]) or (df_price[df_price.code == stockcode].low.values < MA10[stockcode] and df_price[df_price.code == stockcode].close.values > MA10[
+    stockcode]) or (df_price[df_price.code == stockcode].low.values < MA20[stockcode] and df_price[df_price.code == stockcode].close.values > MA20[stockcode]))]
+    print('---均线过滤后共%d只' % len(strong_list))
+    
+    poollist = list(set(up_list+strong_list))
+    print('---合并后共%d只' % len(poollist))
+    
+    return poollist
+    
+#根据定义的选股条件选股
+#D1日高开套人(开0点上下落7点以上)+不能反包D日
+def get_trap_filter(context,stocklist,check_date,trap_limit):
+    log.info('-函数运行时间(get_trap_filter):'+str(context.current_dt.time()))
+    all_data = get_current_data()
+    poollist=[]
+    
+    if len(stocklist)==0:
+        log.info("输入为空")
+        return poollist
+    
+    # 取数
+    df_price = get_price(stocklist,end_date=check_date,frequency='1d',fields=['pre_close','open','close','low','high','high_limit','low_limit','paused']
+    ,skip_paused=False,fq='pre',count=1,panel=False,fill_paused=True)
+    
+    df_trap = df_price[(df_price.open > df_price.pre_close) & (df_price.open > df_price.close) & (df_price.high/df_price.close >trap_limit) & (df_price.close/df_price.pre_close >0.93)]
+    poollist = df_trap.code.values.tolist()
+    
+    return poollist
+        
+#针对输入进行基本面/量价面的过滤择优
+def get_trap_optimize_filter(context,stocklist,check_date,filt_type):
+    log.info('-函数运行时间(get_trap_optimize_filter):'+str(context.current_dt.time()))
+    all_data = get_current_data()
+    poollist=[]
+    
+    if len(stocklist)==0:
+        log.info("输入为空")
+        return stocklist
+        
+    #静态,流通盘<40
+    if filt_type =="S":
+        df_value = get_valuation(stocklist, end_date=check_date, count=1, fields=['circulating_market_cap']) #先新后老
+        df_value = df_value.dropna()
+        #df_value.sort_values(by='circulating_market_cap', ascending=True, inplace=True)
+        #cirm_list = list(df_value.code)[:int(0.6*len(stocklist))]
+        poollist = list(df_value[df_value.circulating_market_cap < g.cirm_up].code)   #绝对
+    elif filt_type == 'A':
+        df_value = get_valuation(stocklist, end_date=check_date, count=1, fields=['circulating_market_cap']) #先新后老
+        df_value = df_value.dropna()
+        poollist = list(df_value[df_value.circulating_market_cap < g.cirm_up].code)   #绝对
+        
+        df_money = get_money_flow(poollist, end_date=check_date, count=1, fields=['sec_code','date','net_pct_main'])
+        poollist = list(df_money[df_money.net_pct_main <-1].sec_code)
+        
+    return poollist
+#去除T日是一字/T字/尾盘封板弱
+def get_up_optimize_filter(context,stocklist):
+    log.info('-函数运行时间(get_up_optimize_filter):'+str(context.current_dt.time()))
+    today_date = context.current_dt.date()
+    lastd_date = context.previous_date
+    all_data = get_current_data()
+    poollist =[]
+    
+    if len(stocklist)==0:
+        log.info("输入为空")
+        return poollist
+    
+    #其他条件,在循环中过滤
+    for stockcode in stocklist:
+        #过滤掉一字板,T字板;
+        df_lastd = get_price(stockcode,end_date=lastd_date,frequency='daily',fields=['open','close','high','high_limit','low_limit'],count=1)
+        if (df_lastd['open'][0] == df_lastd['high_limit'][0] and df_lastd['close'][0] == df_lastd['high_limit'][0]):
+            continue
+        #过滤掉尾盘封板弱的;
+        df_last30 = get_bars(stockcode, count=60, unit='1m', fields=['open','close','high','low'],include_now=True,df=True)
+        if (df_last30['low'][:].min() != df_lastd['high_limit'][0]) & (df_last30['high'][:].max() == df_lastd['high_limit'][0]):
+            continue
+        """
+        df_value = get_valuation(stockcode, end_date=lastd_date, count=1, fields=['circulating_market_cap','pe_ratio','pb_ratio']) #先新后老
+        if len(df_value) ==0 or df_value['pe_ratio'].values[0] <0:  #存在数据缺失
+            continue
+        """
+        poollist.append(stockcode)
+    return poollist
+
+##过滤N天内M倍最高量,X-买入前量能过滤,1X-为持仓的量能过滤
+def get_highvolume_filter(context,stocklist,control_mode,check_dura,volume_ratio):
+    log.info('-函数运行时间(get_highvolume_filter):'+str(context.current_dt.time()))
+    lastd_date = context.previous_date
+    poollist =[]
+    
+    if len(stocklist)==0:
+        log.info("输入为空")
+        return poollist
+        
+    for stockcode in stocklist:
+        if control_mode ==1:
+            df_price = get_price(stockcode,end_date=lastd_date,frequency='daily',fields=['volume'],count=check_dura)
+            if df_price['volume'][-1] > volume_ratio*df_price['volume'].max():
+                continue
+            poollist.append(stockcode)
+        elif control_mode ==2:
+            df_price = get_price(stockcode,end_date=lastd_date,frequency='daily',fields=['volume'],count=check_dura)
+            if df_price['volume'][-1] > volume_ratio*df_price['volume'].mean():
+                continue
+            poollist.append(stockcode)
+        elif control_mode ==3:
+            df_price = get_price(stockcode,end_date=lastd_date,frequency='daily',fields=['volume'],count=check_dura)
+            if df_price['volume'][-1] > volume_ratio*df_price['volume'][-2]:
+                continue
+            poollist.append(stockcode)
+        elif control_mode ==4:
+            df_price = get_price(stockcode,end_date=lastd_date,frequency='daily',fields=['volume'],count=240)
+            if df_price['volume'][-1] > 0.9*df_price['volume'].max():
+                continue
+            if df_price['volume'][-1] > 5*df_price['volume'][-20:].mean():
+                continue
+            poollist.append(stockcode)
+
+    
+    print('---量能控制%d-%d天放%.1f量过滤后共%d只' % (control_mode,check_dura,volume_ratio,len(poollist)))
+    return poollist
+    
+#按比例过滤
+def get_factor_filter_list(context,stock_list,jqfactor,sort,p1,p2):
+    yesterday = context.previous_date
+    score_list = get_factor_values(stock_list, jqfactor, end_date=yesterday, count=1)[jqfactor].iloc[0].tolist()
+    df = pd.DataFrame(columns=['code','score'])
+    df['code'] = stock_list
+    df['score'] = score_list
+    df = df.dropna()
+    df.sort_values(by='score', ascending=sort, inplace=True)
+    filter_list = list(df.code)[int(p1*len(stock_list)):int(p2*len(stock_list))]
+    return filter_list
+
+"""
+---------------------------------函数定义-辅助函数-----------------------------------------------
+"""
+##买入函数
+def buy_stock(context,stockcode,cash):
+    today_date = context.current_dt.date()
+    current_data = get_current_data()
+    
+    if stockcode[0:2] == '68':
+        last_price = current_data[stockcode].last_price
+        if order_target_value(stockcode,cash,MarketOrderStyle(1.1*last_price)) != None: #科创板需要设定限值
+            log.info('%s买入%s' % (today_date,stockcode))
+    else:
+        if order_target_value(stockcode, cash) != None:
+            log.info('%s买入%s' % (today_date,stockcode))
+            
+##卖出函数
+def sell_stock(context,stockcode,cash):
+    today_date = context.current_dt.date()
+    current_data = get_current_data()
+    
+    if stockcode[0:2] == '68':
+        last_price = current_data[stockcode].last_price
+        if order_target_value(stockcode,cash,MarketOrderStyle(0.9*last_price)) != None: #科创板需要设定限值
+            log.info('%s卖出%s' % (today_date,stockcode))
+    else:
+        if order_target_value(stockcode,cash) != None:
+            log.info('%s卖出%s' % (today_date,stockcode))
+        
+        

+ 293 - 0
Lib/strategy/au_long_term_strategy.py

@@ -0,0 +1,293 @@
+# 导入函数库
+from jqdata import *
+import pandas as pd
+import numpy as np
+import datetime
+import re
+
+## 初始化函数,设定基准等等
+def initialize(context):
+    # 设定沪深300作为基准
+    set_benchmark('000300.XSHG')
+    # 开启动态复权模式(真实价格)
+    set_option('use_real_price', True)
+    # 过滤掉order系列API产生的比error级别低的log
+    # log.set_level('order', 'error')
+    # 输出内容到日志 log.info()
+    log.info('初始函数开始运行且全局只运行一次')
+
+    ### 期货相关设定 ###
+    # 设定账户为金融账户
+    set_subportfolios([SubPortfolioConfig(cash=context.portfolio.starting_cash, type='index_futures')])
+    # 期货类每笔交易时的手续费是:买入时万分之0.23,卖出时万分之0.23,平今仓为万分之23
+    set_order_cost(OrderCost(open_commission=0.000023, close_commission=0.000023,close_today_commission=0.0023), type='index_futures')
+    # 设定保证金比例
+    set_option('futures_margin_rate', 0.15)
+
+    # 设置期货交易的滑点
+    set_slippage(StepRelatedSlippage(2))
+    # 运行函数(reference_security为运行时间的参考标的;传入的标的只做种类区分,因此传入'IF8888.CCFX'或'IH1602.CCFX'是一样的)
+    # 注意:before_open/open/close/after_close等相对时间不可用于有夜盘的交易品种,有夜盘的交易品种请指定绝对时间(如9:30)
+    
+    # 合约乘数
+    g.multiplier = {
+        'A': 10, 'AG': 15, 'AL': 5, 'AO': 20, 'AP': 10, 'AU': 1000, 'B': 10,
+        'BC': 5, 'BR': 5, 'BU': 10, 'C': 10, 'CF': 5, 'CJ': 5, 'CS': 10,
+        'CU': 5, 'CY': 5, 'EB': 5, 'EC': 50, 'EG': 10, 'FG': 20, 'FU': 10,
+        'HC': 10, 'I': 100, 'J': 60, 'JD': 5, 'JM': 100, 
+        'L': 5, 'LC': 1, 'LH':16, 'LR': 0.05, 'LU': 10, 'M': 10, 'MA': 10, 'NI': 1, 'NR': 10, 'OI': 10,
+        'P': 10, 'PB': 5, 'PF': 5, 'PG': 20, 'PK': 5,
+        'PP': 5, 'RB': 10, 'RI': 0.05, 'RM': 10, 'RU': 10,
+        'SA': 20, 'SC': 1000, 'SF': 5, 'SH': 30, 'SI': 5, 'SM': 5, 'SN': 1, 'SP': 10, 'SR': 10,
+        'SS': 5, 'TA': 5, 'UR': 20, 'V': 5,
+        'Y': 10, 'ZC': 0.05, 'ZN': 5}
+
+    # 设置AU期货为交易标的
+    g.security = get_dominant_future('AU')
+    
+    # 设置交易参数
+    g.position_size = 0.8  # 仓位比例
+    g.is_traded = False    # 当日是否已交易标志
+    
+    # 设置明确买入日期,格式为YYYY-MM-DD字符串列表
+    g.buy_dates = ['2020-08-10']
+    
+    # 设置止损参数
+    g.stop_loss_pct = 0.05  # 止损比例,5%
+    
+    # 设置自动换月参数
+    g.days_before_expiry = 5  # 到期前多少天换月
+    
+    # 用于收益跟踪的变量
+    g.trade_records = []    # 交易记录
+    g.initial_positions = {}  # 初始持仓信息
+    g.last_roll_date = None   # 上次换月日期
+    g.switch_records = []     # 换月交易记录
+    
+      # 开盘前运行
+    run_daily( before_market_open, time='09:00', reference_security='IF8888.CCFX')
+      # 开盘时运行
+    run_daily( market_open, time='09:30', reference_security='IF8888.CCFX')
+    # 每天14:30检查是否需要换月(仅在持有仓位时运行)
+    run_daily(check_and_switch_position, time='14:30', reference_security='IF8888.CCFX')
+      # 收盘后运行
+    run_daily( after_market_close, time='15:30', reference_security='IF8888.CCFX')
+
+
+## 开盘前运行函数
+def before_market_open(context):
+    # 更新主力合约
+    g.security = get_dominant_future('AU')
+    # log.info("当前主力合约: {}".format(g.security))
+
+## 开盘时运行函数
+def market_open(context):
+    # 如果是买入日期且没有持仓,则买入
+    if is_buy_date(context) and not has_position(context) and not g.is_traded:
+        buy_contract(context)
+
+def is_buy_date(context):
+    current_date = context.current_dt.date()
+    date_str = current_date.strftime('%Y-%m-%d')
+    
+    # 检查当前日期是否在买入日期列表中
+    if date_str in g.buy_dates:
+        log.info("今天 {} 是设定的买入日期".format(date_str))
+        return True
+    return False
+    
+# 买入合约
+def buy_contract(context):
+    # 选择子账户
+    subportfolio = context.subportfolios[0]
+    
+    # 计算买入数量
+    cash_to_use = subportfolio.available_cash * g.position_size
+    price = get_price(g.security, count=1, fields='close')['close'][0]
+    # contract_multiplier = get_contract_multiplier(g.security)
+    # margin_rate = get_future_margin_rate(g.security)
+    
+    # 计算最多可买入的手数
+    # max_amount = int(cash_to_use / (price * contract_multiplier * margin_rate))
+    max_amount = 1
+    
+    if max_amount > 0:
+        # 买入合约
+        order(g.security, max_amount, side='long')
+        log.info("买入 {} 合约,数量: {} 手".format(g.security, max_amount))
+        
+        # 记录交易信息用于收益跟踪
+        g.trade_records.append({
+            'date': context.current_dt.strftime('%Y-%m-%d'),
+            'action': 'buy',
+            'contract': g.security,
+            'price': price,
+            'amount': max_amount
+        })
+        
+        g.is_traded = True
+
+# 检查是否有持仓
+def has_position(context):
+    for subportfolio in context.subportfolios:
+        positions = list(subportfolio.long_positions.keys()) + list(subportfolio.short_positions.keys())
+        # log.info(f'当前持仓为: {positions}')
+        if len(positions) > 0:
+            return True
+    return False
+    
+# 检查是否换月
+def check_and_switch_position(context):
+    """每天14:30检查持仓并执行换月操作"""
+    if has_position(context):
+        # log.info("检测到持仓,开始执行换月检查...")
+        position_auto_switch(context)
+    else:
+        log.info("无持仓,跳过换月检查")
+
+
+## 收盘后运行函数
+def after_market_close(context):
+    # 得到当天所有成交记录
+    trades = get_trades()
+    for _trade in trades.values():
+        log.info('成交记录:'+str(_trade))
+        log.info('##############################################################')
+
+########################## 获取期货合约信息,请保留 #################################
+# 获取金融期货合约到期日
+def get_CCFX_end_date(future_code):
+    # 获取金融期货合约到期日
+    return get_security_info(future_code).end_date
+
+
+########################## 自动移仓换月函数 #################################
+def position_auto_switch(context,pindex=0,switch_func=None, callback=None):
+    """
+    期货自动移仓换月。默认使用市价单进行开平仓。
+    :param context: 上下文对象
+    :param pindex: 子仓对象
+    :param switch_func: 用户自定义的移仓换月函数.
+        函数原型必须满足:func(context, pindex, previous_dominant_future_position, current_dominant_future_symbol)
+    :param callback: 移仓换月完成后的回调函数。
+        函数原型必须满足:func(context, pindex, previous_dominant_future_position, current_dominant_future_symbol)
+    :return: 发生移仓换月的标的。类型为列表。
+    """
+    import re
+    subportfolio = context.subportfolios[pindex]
+    symbols = set(subportfolio.long_positions.keys()) | set(subportfolio.short_positions.keys())
+    switch_result = []
+    for symbol in symbols:
+        match = re.match(r"(?P<underlying_symbol>[A-Z]{1,})", symbol)
+        if not match:
+            raise ValueError("未知期货标的:{}".format(symbol))
+        else:
+            dominant = get_dominant_future(match.groupdict()["underlying_symbol"])
+            cur = get_current_data()
+            # log.info(f'主力合约:{dominant},持仓:{symbol}')
+            symbol_last_price = cur[symbol].last_price
+            dominant_last_price = cur[dominant].last_price
+            if dominant > symbol:
+                log.debug(f'需要换月')
+                for positions_ in (subportfolio.long_positions, subportfolio.short_positions):
+                    if symbol not in positions_.keys():
+                        continue
+                    else :
+                        p = positions_[symbol]
+
+                    if switch_func is not None:
+                        switch_func(context, pindex, p, dominant)
+                    else:
+                        amount = p.total_amount
+                        # 跌停不能开空和平多,涨停不能开多和平空。
+                        if p.side == "long":
+                            symbol_low_limit = cur[symbol].low_limit
+                            dominant_high_limit = cur[dominant].high_limit
+                            if symbol_last_price <= symbol_low_limit:
+                                log.warning("标的{}跌停,无法平仓。移仓换月取消。".format(symbol))
+                                continue
+                            elif dominant_last_price >= dominant_high_limit:
+                                log.warning("标的{}涨停,无法开仓。移仓换月取消。".format(symbol))
+                                continue
+                            else:
+                                log.info("进行移仓换月:({0},long) -> ({1},long)".format(symbol, dominant))
+                                close_order =order_target(symbol,0,side='long')
+                                open_order = order_target(dominant,amount,side='long')
+                                # log.info(f'close_order: {close_order}')
+                                # log.info(f'open_order: {open_order}')
+                                
+                                # 记录换月交易信息
+                                key_symbol = re.match(r"([A-Z]+)", symbol).group(1)
+                                contract_multiplier = g.multiplier[key_symbol]
+                                close_money = close_order.amount * close_order.price * contract_multiplier
+                                open_money = open_order.amount * open_order.price * contract_multiplier
+                                switch_cost = abs(close_money - open_money)
+                                log.info(f'contract_multiplier: {contract_multiplier}, close_money: {close_money}, open_money: {open_money}, switch_cost: {switch_cost}')
+                                
+                                switch_record = {
+                                    'date': context.current_dt.strftime('%Y-%m-%d'),
+                                    'close_contract': symbol,
+                                    'close_price': close_order.price,
+                                    'close_money': close_money,
+                                    'open_contract': dominant,
+                                    'open_price': open_order.price,
+                                    'open_money': open_money,
+                                    'switch_cost': switch_cost,
+                                    'side': 'long'
+                                }
+                                g.switch_records.append(switch_record)
+                                log.info("换月记录:{}".format(switch_record))
+                                # 计算总换月次数和总换月成本
+                                total_switches = len(g.switch_records)
+                                total_cost = sum(record['switch_cost'] for record in g.switch_records)
+                                log.info(f"总换月次数: {total_switches}, 总换月成本: {total_cost}")
+                                
+                                switch_result.append({"before": symbol, "after":dominant, "side": "long"})
+                            if callback:
+                                callback(context, pindex, p, dominant)
+                        if p.side == "short":
+                            symbol_high_limit = cur[symbol].high_limit
+                            dominant_low_limit = cur[dominant].low_limit
+                            if symbol_last_price >= symbol_high_limit:
+                                log.warning("标的{}涨停,无法平仓。移仓换月取消。".format(symbol))
+                                continue
+                            elif dominant_last_price <= dominant_low_limit:
+                                log.warning("标的{}跌停,无法开仓。移仓换月取消。".format(symbol))
+                                continue
+                            else:
+                                log.info("进行移仓换月:({0},short) -> ({1},short)".format(symbol, dominant))
+                                close_order = order_target(symbol,0,side='short')
+                                open_order = order_target(dominant,amount,side='short')
+                                # log.info(f'close_order: {close_order}')
+                                # log.info(f'open_order: {open_order}')
+                                
+                                # 记录换月交易信息
+                                key_symbol = re.match(r"([A-Z]+)", symbol).group(1)
+                                contract_multiplier = g.multiplier[key_symbol]
+                                close_money = close_order.amount * close_order.price * contract_multiplier
+                                open_money = open_order.amount * open_order.price * contract_multiplier
+                                switch_cost = abs(close_money - open_money)
+                                log.info(f'contract_multiplier: {contract_multiplier}, close_money: {close_money}, open_money: {open_money}, switch_cost: {switch_cost}')
+                                
+                                switch_record = {
+                                    'date': context.current_dt.strftime('%Y-%m-%d'),
+                                    'close_contract': symbol,
+                                    'close_price': close_order.price,
+                                    'close_money': close_money,
+                                    'open_contract': dominant,
+                                    'open_price': open_order.price,
+                                    'open_money': open_money,
+                                    'switch_cost': switch_cost,
+                                    'side': 'short'
+                                }
+                                g.switch_records.append(switch_record)
+                                log.info("换月记录:{}".format(switch_record))
+                                # 计算总换月次数和总换月成本
+                                total_switches = len(g.switch_records)
+                                total_cost = sum(record['switch_cost'] for record in g.switch_records)
+                                log.info(f"总换月次数: {total_switches}, 总换月成本: {total_cost}")
+                                
+                                switch_result.append({"before": symbol, "after": dominant, "side": "short"})
+                                if callback:
+                                    callback(context, pindex, p, dominant)
+    return switch_result

+ 62 - 0
Lib/utils/read_log.py

@@ -0,0 +1,62 @@
+import chardet
+
+def detect_file_encoding(file_path):
+    """
+    检测文件编码
+    
+    Args:
+        file_path: 文件路径
+        
+    Returns:
+        检测到的编码类型和置信度
+    """
+    # 读取文件的二进制内容
+    with open(file_path, 'rb') as file:
+        raw_data = file.read()
+    
+    # 使用chardet检测编码
+    result = chardet.detect(raw_data)
+    
+    return result
+
+def read_file_with_encoding(file_path, encoding=None):
+    """
+    使用指定编码读取文件,如果没有指定编码则先检测编码
+    
+    Args:
+        file_path: 文件路径
+        encoding: 指定的编码类型(可选)
+        
+    Returns:
+        文件内容
+    """
+    if not encoding:
+        detected = detect_file_encoding(file_path)
+        encoding = detected['encoding']
+        print(f"检测到的编码: {detected}")
+    
+    try:
+        with open(file_path, 'r', encoding=encoding) as file:
+            content = file.read()
+        return content
+    except UnicodeDecodeError as e:
+        print(f"使用 {encoding} 解码失败: {e}")
+        return None
+
+file_path = '/Users/maxfeng/Downloads/log.txt'
+# 先检测编码
+result = detect_file_encoding(file_path)
+print(f"文件编码检测结果: {result}")
+
+code = result['encoding']
+
+# 然后用检测到的编码读取文件
+content = read_file_with_encoding(file_path, code)
+print("\n文件内容:")
+print(content)
+
+# with open('/Users/maxfeng/Downloads/log.txt', 'r', encoding='GB2312') as f:
+#     content = f.read()
+    
+with open('/Users/maxfeng/Downloads/log2.txt', 'w', encoding='utf-8') as f:
+    f.write(content)

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff