From 245979e3907d34bcd88ac0c4547f399bf33a44de Mon Sep 17 00:00:00 2001
From: Administrator <admin@example.com>
Date: 星期三, 18 六月 2025 18:13:30 +0800
Subject: [PATCH] bug修复/策略完善

---
 strategy/test.py                      |   39 +++++--
 strategy/data_analyzer.py             |   46 +++++++-
 strategy/time_series_backtest.py      |    8 -
 strategy/strategy_manager.py          |   17 ++-
 trade/trade_manager.py                |  150 ++++++++++++++++++++++++++----
 strategy/strategy_variable_factory.py |    6 +
 utils/tool.py                         |    8 +
 strategy/strategy_script_v6.py        |    3 
 strategy/strategy_variable.py         |    1 
 9 files changed, 231 insertions(+), 47 deletions(-)

diff --git a/strategy/data_analyzer.py b/strategy/data_analyzer.py
index 7971c9d..cfdc57d 100644
--- a/strategy/data_analyzer.py
+++ b/strategy/data_analyzer.py
@@ -422,25 +422,57 @@
         return count
 
     @classmethod
-    def is_too_high_and_not_relase_volume(cls, code, k_data):
+    def is_too_high_and_not_relase_volume(cls, k_data):
         """
-        闀垮緱澶珮涓旀病鏀鹃噺锛�30涓氦鏄撴棩鍐咃紝鍑虹幇杩囨渶浣庝环锛堟渶楂樹环涔嬪墠鐨勪氦鏄撴棩锛夊埌鏈�楂樹环涔嬮棿鐨勬定骞呪墺35%鐨勭エ锛屼笖浠婃棩璺濈鏈�楂樹环閭f棩鏃犳定鍋�/鏃犵偢鏉�
+        闀垮緱澶珮涓旀病鏀鹃噺锛�30涓氦鏄撴棩鍐咃紝鍑虹幇杩囨渶浣庝环锛堟渶楂樹环涔嬪墠鐨勪氦鏄撴棩锛夊埌鏈�楂樹环涔嬮棿鐨勬定骞呪墺35%鐨勭エ锛屼笖浠婃棩璺濈鏈�楂樹环閭f棩鏃犳定鍋�/鏃犵偢鏉夸笖>=3鏉夸笖蹇呴』鏈�2杩炴澘
         @param k_data: K绾挎暟鎹垪琛�(杩�150涓氦鏄撴棩锛屼笉鍖呭惈褰撳墠浜ゆ槗鏃ワ紝鏃堕棿鍊掑簭)
         @return: 鍥涜穼鍋滃強浠ヤ笂澶╂暟
         """
         k_data = k_data[:30]
+        code = k_data[0]["sec_id"]
         # 鑾峰彇鏈�楂樹环淇℃伅
         max_high_price_data = max(k_data, key=lambda x: x["high"])
-        min_close_price_data = min([d for d in k_data if d['bob'] < max_high_price_data['bob']], key=lambda x: x["close"])
-        if (max_high_price_data['high'] - min_close_price_data['close'])/min_close_price_data['close'] < 0.35:
+        before_datas = [d for d in k_data if d['bob'] < max_high_price_data['bob']]
+        after_datas = [d for d in k_data if d['bob'] >= max_high_price_data['bob']]
+        if not before_datas:
+            return False
+        if len(before_datas) > 15:
+            # 浠庢渶楂樹环鏃ユ湡鍚戝墠鏈�澶氱湅15涓氦鏄撴棩
+            before_datas = before_datas[:15]
+        min_close_price_data = min(before_datas, key=lambda x: x["close"])
+        if (max_high_price_data['high'] - min_close_price_data['close']) / min_close_price_data['close'] < 0.35:
             # 娑ㄥ箙灏忎簬35%
             return False
+        before_k_datas = [d for d in k_data if min_close_price_data['bob'] <= d['bob'] <= max_high_price_data['bob']]
+        before_k_datas.sort(key=lambda x: x['bob'])
+
+        # [鏈�浣庝环-鏈�楂樹环]鏃ユ湡鍐呮湁3涓澘涓旀湁涓よ繛鎵�
+
+        continue_2_limit_up_date = None
+        for i in range(len(before_k_datas) - 1):
+            if cls.__is_limit_up(code, before_k_datas[i]["close"],
+                                 before_k_datas[i]["pre_close"]) and cls.__is_limit_up(code,
+                                                                                       before_k_datas[i + 1]["close"],
+                                                                                       before_k_datas[i + 1][
+                                                                                           "pre_close"]):
+                continue_2_limit_up_date = before_k_datas[i + 1]['bob'][:10]
+                break
+        if not continue_2_limit_up_date:
+            # 鏃犱袱杩炴澘
+            return False
+        # 涓よ繛鏉夸箣鍚庢槸鍚︽湁鐐告澘/娑ㄥ仠
+        # 鍙�2杩炴澘涔嬪悗鐨�3涓氦鏄撴棩
+        temp_k_datas = [d for d in before_k_datas if d['bob'][:10] > continue_2_limit_up_date][:3]
+        if len([d for d in temp_k_datas if cls.__is_limit_up(code, d["high"], d["pre_close"])]) < 1:
+            # 涓よ繛鏉夸箣鍚庢湁涓定鍋�/鐐告澘涓旀椂闂村湪2杩炴澘涔嬪悗鐨�3涓氦鏄撴棩鍐�
+            return False
+
         k_data = [d for d in k_data if d['bob'] > max_high_price_data['bob']]
         # 鍒ゆ柇鏄惁娑ㄥ仠杩�
-        if len([d for d in k_data if cls.__is_limit_up(code, d["high"], d["pre_close"])]) >0:
-            # 鏈�楂樹环涔嬪悗鏈夎繃娑ㄥ仠
+        if len([d for d in k_data if cls.__is_limit_up(code, d["high"], d["pre_close"])]) > 0 or len(after_datas) >= 10:
+            # 鏈�楂樹环涔嬪悗鏈夎繃娑ㄥ仠鎴栬�呮槸鏈�楂樹环鍚�10涓氦鏄撴棩
             return False
-        return True
+        return True, f"楂樹环鏃ユ湡锛歿max_high_price_data['bob'][:10]}锛屼綆浠锋棩鏈燂細{min_close_price_data['bob'][:10]}锛屼袱杩炴壋鏃ユ湡锛歿continue_2_limit_up_date}"
 
 
 class K60SLineAnalyzer:
diff --git a/strategy/strategy_manager.py b/strategy/strategy_manager.py
index d6acfb3..a9c585c 100644
--- a/strategy/strategy_manager.py
+++ b/strategy/strategy_manager.py
@@ -16,7 +16,7 @@
 from strategy.strategy_variable_factory import DataLoader, StrategyVariableFactory
 import constant
 from third_data import kpl_util
-from trade.trade_manager import DealCodesManager
+from trade.trade_manager import DealCodesManager, PlatePlaceOrderManager
 from utils import huaxin_util, tool
 
 
@@ -391,10 +391,14 @@
         # 娉ㄥ叆鏉垮潡娴佸叆淇℃伅
         if self.current_block_in_datas:
             sv.璧勯噾娴佸叆鏉垮潡 = self.current_block_in_datas
-        # 娉ㄥ叆宸叉垚浜や唬鐮�
-        place_order_plate_codes = DealCodesManager().get_place_order_plate_codes()
+        # 娉ㄥ叆宸叉垚浜や唬鐮�,鎴愪氦浠g爜浠ュ鎵樻暟鎹潵璁$畻
+        place_order_plate_codes = PlatePlaceOrderManager().get_plate_codes()
         sv.鏉垮潡鎴愪氦浠g爜 = place_order_plate_codes
-        sv.鎴愪氦浠g爜 = DealCodesManager().get_deal_codes()
+
+        code_sets = [set(lst) for lst in place_order_plate_codes.values()]
+        # 2. 浣跨敤 set.union() 姹傚苟闆�
+        union_code_sets = set().union(*code_sets)
+        sv.鎴愪氦浠g爜 = union_code_sets
         global_dict = {
             "sv": sv,
             "target_code": code,
@@ -407,8 +411,11 @@
                 return
             # 鍙互涓嬪崟
             # 鍒ゆ柇鏄惁鍙互涔�
+            order_ref = huaxin_util.create_order_ref()
+            price = tool.get_buy_max_price(sv.褰撳墠浠�)
+            volume = 100
+            DealCodesManager().place_order(set(compute_result[3]), code, order_ref, price, volume)
             for b in compute_result[3]:
-                DealCodesManager().place_order(b, code)
                 async_log_util.info(logger_trade, f"{code}涓嬪崟锛屾澘鍧楋細{compute_result[3]}")
 
 
diff --git a/strategy/strategy_script_v6.py b/strategy/strategy_script_v6.py
index 144d469..48bbf79 100644
--- a/strategy/strategy_script_v6.py
+++ b/strategy/strategy_script_v6.py
@@ -94,6 +94,9 @@
     if sv.鏃ヤ笁鏉夸釜鏁癬10 >= 1:
         return False, f"10涓氦鏄撴棩鏈�>=3杩炴澘"
 
+    if sv.娑ㄥ緱楂樻湭鏀鹃噺:
+        return False, f"娑ㄥ緱楂樻湭鏀鹃噺"
+
     # if sv.褰撳墠浠� > sv.鏄ㄦ棩鏈�浣庝环 * 1.1:
     #     return False, f"涔板叆鏃剁殑浠锋牸蹇呴』鈮ゆ槰鏃ユ渶浣庝环*110%"
 
diff --git a/strategy/strategy_variable.py b/strategy/strategy_variable.py
index e2b805f..74ca857 100644
--- a/strategy/strategy_variable.py
+++ b/strategy/strategy_variable.py
@@ -213,6 +213,7 @@
         self.杈ㄨ瘑搴︿唬鐮� = set()
         self.棰嗘定鏉垮潡淇℃伅 = set()
         self.杩炵画鑰侀鏉� = set()
+        self.娑ㄥ緱楂樻湭鏀鹃噺 = False
 
     def replace_variables(self, expression):
         """
diff --git a/strategy/strategy_variable_factory.py b/strategy/strategy_variable_factory.py
index 7a29a7e..2b3d3d1 100644
--- a/strategy/strategy_variable_factory.py
+++ b/strategy/strategy_variable_factory.py
@@ -602,6 +602,12 @@
             kline_data_60s = kline_data_60s_dict.get(trade_days[0])
             fdata = K60SLineAnalyzer.get_close_price_of_max_volume(kline_data_60s)
             instance.__setattr__(f"鏄ㄦ棩鍒嗘椂鏈�楂橀噺浠�", fdata)
+
+        if KTickLineAnalyzer.is_too_high_and_not_relase_volume(kline_data_1d):
+            instance.娑ㄥ緱楂樻湭鏀鹃噺 = True
+        else:
+            instance.娑ㄥ緱楂樻湭鏀鹃噺 = False
+
         return instance
 
 
diff --git a/strategy/test.py b/strategy/test.py
index 872dfb9..a916c50 100644
--- a/strategy/test.py
+++ b/strategy/test.py
@@ -1,13 +1,15 @@
 from huaxin_client import l1_subscript_codes_manager
-from strategy import strategy_manager
+from strategy import strategy_manager, data_analyzer
 from strategy.strategy_variable import StockVariables
 
 # 缁熻褰撴棩鐨勫钩鍧囨孩浠风巼
+from strategy.strategy_variable_factory import DataLoader
 from third_data.kpl_block_manager import KPLCodeJXBlocksManager
 
 
 def statistic_average(path):
     rate_list = []
+    yjl_list = []
     with open(path, mode='r', encoding='utf-8') as f:
         lines = f.readlines()
         for line in lines:
@@ -17,22 +19,39 @@
                 continue
             r = round(float(line.split("褰撴棩鐩堜簭锛�")[1].split("锛�")[0].replace("%", "")), 2)
             rate_list.append(r)
-    print("骞冲潎鍒╂鼎鐜囷細", round(sum(rate_list) / len(rate_list), 2))
-    print("鎬诲埄娑︾巼锛�", round(sum(rate_list), 2), "鎬讳拱绁ㄦ暟閲忥細", len(rate_list))
+            r = line.split("婧环鐜囷細")[1].split(",")[0].replace("%", "")
+            if r.find("鏈煡") < 0:
+                yjl_list.append(round(float(r), 2))
+    print("褰撴棩骞冲潎鍒╂鼎鐜囷細", round(sum(rate_list) / len(rate_list), 2))
+    print("褰撴棩鎬诲埄娑︾巼锛�", round(sum(rate_list), 2), "鎬讳拱绁ㄦ暟閲忥細", len(rate_list))
+    print("娆℃棩寮�鐩樺钩鍧囧埄娑︾巼锛�", round(sum(yjl_list) / len(yjl_list), 2))
+    print("娆℃棩寮�鐩樻�诲埄娑︾巼锛�", round(sum(yjl_list), 2), "鎬讳拱绁ㄦ暟閲忥細", len(yjl_list))
 
 
 if __name__ == "__main__":
+    print("======3涓エ娑ㄥ仠涔嬪悗涔癬涓嶄拱闀垮緱澶珮鏈斁閲�")
+    statistic_average(r"C:\Users\Administrator\Desktop\3涓エ娑ㄥ仠涔嬪悗涔癬涓嶄拱闀垮緱澶珮鏈斁閲�.txt")
     print("======3涓エ娑ㄥ仠涔嬪悗涔�")
     statistic_average(r"C:\Users\Administrator\Desktop\3涓エ娑ㄥ仠涔嬪悗涔�.txt")
-    # print("======3涓エ娑ㄥ仠涔嬪悗涔�+涓嶉檺寮�鐩樻定骞�+3涓定鍋滀箣鍚庡ぇ鍗曟墦鎶�")
-    # statistic_average(r"C:\Users\Administrator\Desktop\3涓エ娑ㄥ仠涔嬪悗涔癬涓嶉檺寮�鐩樻定骞�.txt")
-    codes = set()
-    codes_sh, codes_sz = l1_subscript_codes_manager.get_codes()
-    codes |= set([x.decode() for x in codes_sh])
-    codes |= set([x.decode() for x in codes_sz])
-    KPLCodeJXBlocksManager('2025-06-17', codes).start_download_blocks()
+    # codes = set()
+    # codes_sh, codes_sz = l1_subscript_codes_manager.get_codes()
+    # codes |= set([x.decode() for x in codes_sh])
+    # codes |= set([x.decode() for x in codes_sz])
+    # KPLCodeJXBlocksManager('2025-06-17', codes).start_download_blocks()
     # target_block = {"鐭虫补鐭冲寲", "澶╃劧姘�", "鍖栧伐"}
     # for code in code_blocks:
     #     blocks = code_blocks.get(code)
     #     if len(blocks & target_block) == len(target_block):
     #         print(code, blocks)
+
+    __DataLoader = DataLoader("2025-06-18")
+    kline_datas = __DataLoader.load_kline_data()
+    codes = []
+    for code in kline_datas:
+        # if code !='003010':
+        #     continue
+        result = data_analyzer.KTickLineAnalyzer.is_too_high_and_not_relase_volume(kline_datas[code])
+        if result:
+            print("鏈斁閲�", code, result[1])
+            codes.append(code)
+    print(len(codes))
diff --git a/strategy/time_series_backtest.py b/strategy/time_series_backtest.py
index b8902e4..aa984fb 100644
--- a/strategy/time_series_backtest.py
+++ b/strategy/time_series_backtest.py
@@ -807,13 +807,11 @@
 
 if __name__ == "__main__":
     back_test_dict = {}
-    days = ["2025-05-12", "2025-05-13", "2025-05-14", "2025-05-15", "2025-05-16", "2025-05-19", "2025-05-20",
-            "2025-05-21", "2025-05-22", "2025-05-23", "2025-05-26", "2025-05-27", "2025-05-28", "2025-05-29",
-            "2025-05-30", "2025-06-03"]
     # days = ["2025-05-12", "2025-05-13", "2025-05-14", "2025-05-15", "2025-05-16", "2025-05-19", "2025-05-20",
     #         "2025-05-21", "2025-05-22", "2025-05-23", "2025-05-26", "2025-05-27", "2025-05-28", "2025-05-29",
-    #         "2025-05-30", "2025-06-03", "2025-06-04", "2025-06-05", "2025-06-06", "2025-06-09", "2025-06-10",
-    #         "2025-06-11", "2025-06-12", "2025-06-13", "2025-06-16", "2025-06-17"]
+    #         "2025-05-30", "2025-06-03"]
+    days = ["2025-06-03", "2025-06-04", "2025-06-05", "2025-06-06", "2025-06-09", "2025-06-10",
+            "2025-06-11", "2025-06-12", "2025-06-13", "2025-06-16", "2025-06-17", "2025-06-18"]
 
     # days = ["2025-05-23"]
 
diff --git a/trade/trade_manager.py b/trade/trade_manager.py
index 56ab205..ad55349 100644
--- a/trade/trade_manager.py
+++ b/trade/trade_manager.py
@@ -176,14 +176,11 @@
 
     def __init__(self):
         self.musql = Mysqldb()
+        # 鎴愪氦寰楄鍗曚俊鎭�
         self.__deal_code_orders_info = {}
-        self.redis_manager = redis_manager.RedisManager(12)
-        # 涓嬭繃鍗曠殑鏉垮潡浠g爜
-        self.__place_order_plate_codes_info = {}
+        # 濮旀墭寰楄鍗曚俊鎭細{code#order_ref:}
+        self.__delegate_code_orders = {}
         self.__load_data()
-
-    def __get_redis(self):
-        return self.redis_manager.getRedis()
 
     def __load_data(self):
         # 涓嶇畻鎵撴澘鐨勬暟鎹�
@@ -193,13 +190,9 @@
             for r in results:
                 self.add_deal_order(r[1], r[4], round(float(r[3]), 2), r[0], r[2])
 
-        val = RedisUtils.get(self.__get_redis(), "place_order_plate_codes_info")
-        if val:
-            self.__place_order_plate_codes_info = json.loads(val)
-
     def add_deal_order(self, code, volume, price, trade_id, order_sys_id):
         """
-        娣诲姞鎴愪氦澶у崟
+        娣诲姞鎴愪氦璁㈠崟
         @param code:
         @param volume:
         @param price:
@@ -217,31 +210,148 @@
             return
         self.__deal_code_orders_info[code][trade_id] = (volume, price, order_sys_id)
 
+    def set_order_status(self, code, order_ref, order_sys_id, price, volume, status):
+        """
+        璁剧疆璁㈠崟鐘舵��
+        @param code:
+        @param order_ref:
+        @param order_sys_id:
+        @param price:
+        @param volume:
+        @param status:
+        @return:
+        """
+        k = f"{code}#{order_ref}"
+        if k not in self.__delegate_code_orders:
+            return
+        # [浠g爜锛岃鍗曠储寮曪紝璁㈠崟鍙凤紝浠锋牸锛岄噺锛岀姸鎬侊紝鏉垮潡闆嗗悎]
+        data = self.__delegate_code_orders[k]
+        data[2] = order_sys_id
+        data[5] = status
+        data[3] = price
+        data[4] = volume
+        # 濡傛灉璁㈠崟宸茬粡鍙栨秷灏遍渶瑕佸垹闄�
+        if status == huaxin_util.TORA_TSTP_OST_AllCanceled or status == huaxin_util.TORA_TSTP_OST_Rejected:
+            data = self.__delegate_code_orders.pop(k)
+            if data:
+                PlatePlaceOrderManager().remove_plates_code(data[6], code)
+
     def get_deal_codes(self):
         if not self.__deal_code_orders_info:
             return set()
         return set(self.__deal_code_orders_info.keys())
 
-    def place_order(self, plate, code):
+    def place_order(self, plates, code, order_ref, price, volume):
         """
         涓嬪崟
-        @param plate:
+        @param plates:
+        @param code:
+        @param order_ref:
+        @param price:
+        @param volume:
+        @return:
+        """
+        # 鍒濆鍖栧鎵樻暟鎹� [浠g爜锛岃鍗曠储寮曪紝璁㈠崟鍙凤紝浠锋牸锛岄噺锛岀姸鎬侊紝鏉垮潡闆嗗悎]
+        data = [code, order_ref, '', price, volume, huaxin_util.TORA_TSTP_OST_Unknown, plates]
+        k = f"{code}#{order_ref}"
+        if k not in self.__delegate_code_orders:
+            self.__delegate_code_orders[k] = data
+        PlatePlaceOrderManager().add_plates_code(plates, code)
+
+    def place_order_fail(self, code, order_ref):
+        """
+        涓嬪崟澶辫触浜�
+        @param code:
+        @param order_ref:
+        @return:
+        """
+        k = f"{code}#{order_ref}"
+        if k in self.__delegate_code_orders:
+            data = self.__delegate_code_orders.pop(k)
+            if data:
+                PlatePlaceOrderManager().remove_plates_code(data[6], code)
+
+    def get_deal_or_delegated_codes(self):
+        """
+        鑾峰彇宸茬粡鎴愪氦鎴栬�呭鎵樼殑浠g爜
+        @return:
+        """
+        codes = set()
+        if self.__delegate_code_orders:
+            for k in self.__delegate_code_orders:
+                codes.add(self.__delegate_code_orders[k][0])
+
+        if self.__deal_code_orders_info:
+            codes |= set(self.__deal_code_orders_info.keys())
+        return codes
+
+
+@tool.singleton
+class PlatePlaceOrderManager:
+    """
+    鏉垮潡涓嬪崟绠$悊
+    """
+
+    def __init__(self):
+        self.__db = 12
+        self.redis_manager = redis_manager.RedisManager(self.__db)
+        # 涓嬭繃鍗曠殑鏉垮潡浠g爜
+        self.__place_order_plate_codes_info = {}
+        self.__load_data()
+
+    def __get_redis(self):
+        return self.redis_manager.getRedis()
+
+    def __load_data(self):
+        val = RedisUtils.get(self.__get_redis(), "place_order_plate_codes_info")
+        if val:
+            self.__place_order_plate_codes_info = json.loads(val)
+
+    def add_plates_code(self, plates, code):
+        """
+        娣诲姞鏉垮潡涓嬪崟
+        @param plates:
         @param code:
         @return:
         """
-        if plate not in self.__place_order_plate_codes_info:
-            self.__place_order_plate_codes_info[plate] = []
-        if code not in self.__place_order_plate_codes_info[plate]:
-            self.__place_order_plate_codes_info[plate].append(code)
+        for plate in plates:
+            if plate not in self.__place_order_plate_codes_info:
+                self.__place_order_plate_codes_info[plate] = []
+            if code not in self.__place_order_plate_codes_info[plate]:
+                self.__place_order_plate_codes_info[plate].append(code)
+        self.__sync_plate_place_order_info()
+
+    def __sync_plate_place_order_info(self):
+        """
+        鍚屾鏉垮潡涓嬪崟淇℃伅
+        @return:
+        """
         RedisUtils.setex_async(self.__db, "place_order_plate_codes_info", tool.get_expire(),
                                json.dumps(self.__place_order_plate_codes_info))
 
-    def get_place_order_plate_codes(self):
+    def remove_plates_code(self, plates, code):
+        """
+        绉婚櫎鏉垮潡涓嬪崟
+        @param plates:
+        @param code:
+        @return:
+        """
+        for plate in plates:
+            if plate in self.__place_order_plate_codes_info:
+                if code in self.__place_order_plate_codes_info[plate]:
+                    self.__place_order_plate_codes_info[plate].remove(code)
+        self.__sync_plate_place_order_info()
+
+    def get_plate_codes(self):
         return self.__place_order_plate_codes_info
 
 
 __CodesTradeStateManager = CodesTradeStateManager()
 
 if __name__ == "__main__":
-    codes = DealCodesManager().get_codes()
-    print(codes)
+    PlatePlaceOrderManager().add_plates_code({"閫氫俊","璁$畻鏈�"}, "000333")
+    place_order_plate_codes = PlatePlaceOrderManager().get_plate_codes()
+    code_sets = [set(lst) for lst in place_order_plate_codes.values()]
+    # 2. 浣跨敤 set.union() 姹傚苟闆�
+    union_code_sets = set().union(*code_sets)
+    print(union_code_sets)
diff --git a/utils/tool.py b/utils/tool.py
index e191df7..9dff785 100644
--- a/utils/tool.py
+++ b/utils/tool.py
@@ -318,6 +318,14 @@
     return max(price1, price2)
 
 
+# 鑾峰彇涔板叆浠锋牸绗煎瓙鐨勬渶楂樹环
+def get_buy_max_price(price):
+    price1 = price * (1 + 0.02)
+    price1 = math.ceil(price1 * 100) / 100
+    price2 = price + 0.1
+    return max(price1, price2)
+
+
 # 鑾峰彇涔板叆浠锋牸绗煎瓙鐨勬渶浣庝环
 def get_shadow_price(price):
     # fprice = round((100 - random.randint(2, 10)) * price / 100, 2)

--
Gitblit v1.8.0