diff --git a/gridtrader/connect_futures.json b/gridtrader/connect_futures.json index 1f4fcbf..054020d 100644 --- a/gridtrader/connect_futures.json +++ b/gridtrader/connect_futures.json @@ -1,6 +1,6 @@ { - "key": "", - "secret": "", + "key": "qBFimviRucbMNt9bcPKWBIrhrAqJpcGlPjDMkiIFj04GzhT0YNsLq9A1XU9IFC2Y", + "secret": "UvkbgNauNOGMwYxnGbz81hFYbhGaEdNdFfirwLUIE72McPhc4eJOkSelS65Stbpu", "futures_type": "USDT", "proxy_host": "", "proxy_port": 0 diff --git a/gridtrader/grid_strategy_data.json b/gridtrader/grid_strategy_data.json index 7a73a41..79f1f90 100644 --- a/gridtrader/grid_strategy_data.json +++ b/gridtrader/grid_strategy_data.json @@ -1,2 +1,335 @@ { + "BTC": { + "pos": 0.027000000000000003, + "avg_price": 103608.8148148148, + "step_price": 451.6, + "trade_times": 11, + "price_volume_dict": { + "96000.0": 0.01, + "96451.6": 0.01, + "96903.1": 0.01, + "97354.8": 0.01, + "97806.3": 0.01, + "98258.0": 0.01, + "98709.6": 0.01, + "99161.1": 0.01, + "99612.8": 0.01, + "100064.3": 0.01, + "100516.0": 0.01, + "100967.6": 0.01, + "101419.1": 0.009, + "101870.8": 0.009, + "102322.3": 0.009, + "102774.0": 0.008, + "103225.6": 0.008, + "103677.1": 0.008, + "104128.8": 0.008, + "104580.3": 0.008, + "105032.0": 0.008, + "105483.6": 0.008, + "105935.1": 0.008, + "106386.8": 0.008, + "106838.3": 0.008, + "107290.0": 0.008, + "107741.6": 0.008, + "108193.1": 0.008, + "108644.8": 0.008, + "109096.3": 0.008, + "109548.0": 0.008 + }, + "start_price_triggered": true, + "lower_grid_total_volume": 0.15318422750216226, + "upper_grid_total_volume": 0.13874767719868758 + }, + "AVAAI": { + "pos": 1299.0, + "avg_price": 0.04939, + "step_price": 0.0007, + "trade_times": 11, + "price_volume_dict": { + "0.048": 1474.0, + "0.0487": 1453.0, + "0.04939": 1432.0, + "0.05009": 1412.0, + "0.05079": 1393.0, + "0.0515": 1374.0, + "0.0522": 1356.0, + "0.0529": 1338.0, + "0.0536": 1320.0, + "0.0543": 1303.0, + "0.055": 1287.0, + "0.05569": 1270.0, + "0.05639": 1255.0, + "0.05709": 1239.0, + "0.0578": 1224.0, + "0.0585": 1210.0, + "0.0592": 1195.0, + "0.0599": 1181.0, + "0.0606": 1168.0, + "0.0613": 1154.0, + "0.06199": 1141.0, + "0.0627": 1128.0, + "0.06339": 1116.0, + "0.0641": 1104.0, + "0.06479": 1092.0, + "0.0655": 1080.0, + "0.0662": 1069.0, + "0.0669": 1058.0, + "0.06759": 1047.0, + "0.06829": 1036.0, + "0.069": 1025.0, + "0.06969": 1015.0, + "0.0704": 1005.0, + "0.07109": 995.0, + "0.0718": 985.0, + "0.0725": 976.0, + "0.0732": 967.0, + "0.07389": 870.0, + "0.07459": 862.0, + "0.0753": 854.0, + "0.07599": 846.0, + "0.0767": 838.0, + "0.07739": 831.0, + "0.0781": 823.0, + "0.0788": 816.0, + "0.0795": 809.0, + "0.08019": 802.0, + "0.08089": 795.0, + "0.0816": 788.0, + "0.08229": 781.0, + "0.08299": 775.0, + "0.08369": 768.0, + "0.0844": 762.0, + "0.0851": 756.0, + "0.0858": 750.0, + "0.08649": 743.0, + "0.08719": 737.0, + "0.0879": 732.0, + "0.08859": 726.0, + "0.08929": 720.0, + "0.08999": 715.0, + "0.0907": 709.0, + "0.0914": 704.0, + "0.0921": 698.0, + "0.09279": 693.0, + "0.09349": 688.0, + "0.0942": 683.0, + "0.09489": 678.0, + "0.09559": 673.0, + "0.09629": 668.0, + "0.097": 663.0, + "0.0977": 658.0, + "0.0984": 653.0, + "0.09909": 649.0 + }, + "start_price_triggered": true, + "lower_grid_total_volume": 43894.590849018256, + "upper_grid_total_volume": 27733.76065942861 + }, + "PENGU": { + "pos": 32159.0, + "avg_price": 0.01813325168693056, + "step_price": 0.0001964285714285714, + "trade_times": 7, + "price_volume_dict": { + "0.017": 13205.0, + "0.017196": 13054.0, + "0.017392": 12907.0, + "0.017589": 12762.0, + "0.017785": 12621.0, + "0.017982": 12484.0, + "0.018178": 12349.0, + "0.018375": 12217.0, + "0.018571": 12087.0, + "0.018767": 11961.0, + "0.018964": 11837.0, + "0.01916": 11716.0, + "0.019357": 11597.0, + "0.019553": 11480.0, + "0.01975": 11366.0, + "0.019946": 11254.0, + "0.020142": 11144.0, + "0.020339": 11037.0, + "0.020535": 10931.0, + "0.020732": 10828.0, + "0.020928": 10726.0, + "0.021125": 10626.0, + "0.021321": 10528.0, + "0.021517": 10432.0, + "0.021714": 10338.0, + "0.02191": 10245.0, + "0.022107": 10154.0, + "0.022303": 10065.0, + "0.022499": 9070.0, + "0.022696": 8991.0, + "0.022892": 8914.0, + "0.023089": 8838.0, + "0.023285": 8764.0, + "0.023482": 8690.0, + "0.023678": 8618.0, + "0.023875": 8547.0, + "0.024071": 8478.0, + "0.024267": 8409.0, + "0.024464": 8342.0, + "0.02466": 8275.0, + "0.024857": 8210.0, + "0.025053": 8145.0, + "0.02525": 8082.0, + "0.025446": 8020.0, + "0.025642": 7958.0, + "0.025839": 7898.0, + "0.026035": 7838.0, + "0.026232": 7779.0, + "0.026428": 7722.0, + "0.026625": 7665.0, + "0.026821": 7608.0, + "0.027017": 7553.0, + "0.027214": 7499.0, + "0.02741": 7445.0, + "0.027607": 7392.0, + "0.027803": 7340.0 + }, + "start_price_triggered": true, + "lower_grid_total_volume": 321964.5383785239, + "upper_grid_total_volume": 228102.72970017433 + }, + "FARTCOIN": { + "pos": 0, + "avg_price": 0.0, + "step_price": 0.010655737704918034, + "trade_times": 0, + "price_volume_dict": { + "0.8499": 50.4, + "0.8606": 49.8, + "0.8713": 49.2, + "0.8819": 48.6, + "0.8926": 48.0, + "0.9032": 47.4, + "0.9139": 46.9, + "0.9245": 46.4, + "0.9352": 45.8, + "0.9459": 45.3, + "0.9565": 44.8, + "0.9672": 44.3, + "0.9778": 43.8, + "0.9885": 43.3, + "0.9991": 42.9, + "1.0098": 42.4, + "1.0204": 42.0, + "1.0311": 41.6, + "1.0418": 41.1, + "1.0524": 40.7, + "1.0631": 40.3, + "1.0737": 39.9, + "1.0844": 39.5, + "1.095": 39.1, + "1.1057": 38.7, + "1.1163": 38.4, + "1.127": 38.0, + "1.1377": 37.7, + "1.1483": 37.3, + "1.159": 37.0, + "1.1696": 33.3, + "1.1803": 33.0, + "1.1909": 32.7, + "1.2016": 32.4, + "1.2122": 32.1, + "1.2229": 31.8, + "1.2336": 31.6, + "1.2442": 31.3, + "1.2549": 31.0, + "1.2655": 30.8, + "1.2762": 30.5, + "1.2868": 30.3, + "1.2975": 30.0, + "1.3081": 29.8, + "1.3188": 29.5, + "1.3295": 29.3, + "1.3401": 29.1, + "1.3508": 28.8, + "1.3614": 28.6, + "1.3721": 28.4, + "1.3827": 28.2, + "1.3934": 27.9, + "1.404": 27.7, + "1.4147": 27.5, + "1.4254": 27.3, + "1.436": 27.1, + "1.4467": 26.9, + "1.4573": 26.7, + "1.468": 26.5, + "1.4786": 26.3, + "1.4893": 26.1 + }, + "start_price_triggered": true, + "lower_grid_total_volume": 1292.2433322080694, + "upper_grid_total_volume": 914.1121837060554 + }, + "AI16Z": { + "pos": 0, + "avg_price": 0.0, + "step_price": 0.0, + "trade_times": 0, + "price_volume_dict": {}, + "start_price_triggered": false, + "lower_grid_total_volume": 0.0, + "upper_grid_total_volume": 0.0 + }, + "RUNE": { + "pos": -248.0, + "avg_price": 1.4024347826086956, + "step_price": 0.014, + "trade_times": 4, + "price_volume_dict": { + "1.35": 84.0, + "1.364": 83.0, + "1.378": 82.0, + "1.392": 81.0, + "1.406": 80.0, + "1.42": 80.0, + "1.434": 79.0, + "1.448": 78.0, + "1.462": 77.0, + "1.475": 77.0, + "1.49": 76.0, + "1.504": 75.0, + "1.518": 75.0, + "1.532": 74.0, + "1.546": 73.0, + "1.56": 72.0, + "1.574": 72.0, + "1.588": 71.0, + "1.602": 71.0, + "1.616": 70.0, + "1.63": 69.0, + "1.644": 69.0, + "1.658": 68.0, + "1.672": 61.0, + "1.686": 61.0, + "1.7": 60.0, + "1.713": 60.0, + "1.728": 59.0, + "1.741": 59.0, + "1.756": 58.0, + "1.77": 58.0, + "1.784": 58.0, + "1.798": 57.0, + "1.812": 57.0, + "1.826": 56.0, + "1.84": 56.0, + "1.854": 55.0, + "1.868": 55.0, + "1.882": 55.0, + "1.896": 54.0, + "1.91": 54.0, + "1.924": 53.0, + "1.938": 53.0, + "1.951": 53.0, + "1.966": 52.0, + "1.979": 52.0 + }, + "start_price_triggered": true, + "lower_grid_total_volume": 1748.072954517176, + "upper_grid_total_volume": 1307.3052553704113 + } } \ No newline at end of file diff --git a/gridtrader/grid_strategy_setting.json b/gridtrader/grid_strategy_setting.json index b94316f..33be7a4 100644 --- a/gridtrader/grid_strategy_setting.json +++ b/gridtrader/grid_strategy_setting.json @@ -1,26 +1,76 @@ { - "BTCBUSD": { + "BTC": { "class_name": "FutureGridStrategy", - "vt_symbol": "BTCBUSD.BINANCE", + "vt_symbol": "BTCUSDT.BINANCE", "setting": { - "upper_price": 42000, - "bottom_price": 39000, - "grid_number": 100, - "order_volume": 0.001, - "max_open_orders": 5 + "upper_price": 110000.0, + "bottom_price": 96000.0, + "max_open_orders": 5, + "order_amount": 30000, + "start_price": 0.0, + "direction_int": 1 } }, - "btcbusd": { - "class_name": "SpotGridStrategy", - "vt_symbol": "btcbusd.BINANCE", + "AVAAI": { + "class_name": "FutureGridStrategy", + "vt_symbol": "AVAAIUSDT.BINANCE", + "setting": { + "upper_price": 0.1, + "bottom_price": 0.048, + "max_open_orders": 5, + "order_amount": 5000, + "start_price": 0.0, + "direction_int": 1 + } + }, + "PENGU": { + "class_name": "FutureGridStrategy", + "vt_symbol": "PENGUUSDT.BINANCE", + "setting": { + "upper_price": 0.028, + "bottom_price": 0.017, + "max_open_orders": 5, + "order_amount": 12000, + "start_price": 0.0, + "direction_int": 1 + } + }, + "FARTCOIN": { + "class_name": "FutureGridStrategy", + "vt_symbol": "FARTCOINUSDT.BINANCE", + "setting": { + "class_name": "FutureGridStrategy", + "upper_price": 1.5, + "bottom_price": 0.85, + "max_open_orders": 5, + "order_amount": 2500, + "start_price": 0.0, + "direction_int": 1 + } + }, + "AI16Z": { + "class_name": "FutureGridStrategy", + "vt_symbol": "AI16ZUSDT.BINANCE", + "setting": { + "upper_price": 1.2, + "bottom_price": 0.6, + "max_open_orders": 5, + "order_amount": 6000, + "start_price": 0.0, + "direction_int": 1 + } + }, + "RUNE": { + "class_name": "FutureGridStrategy", + "vt_symbol": "RUNEUSDT.BINANCE", "setting": { - "class_name": "SpotGridStrategy", - "upper_price": 42000.0, - "bottom_price": 32000.0, - "grid_number": 50, - "order_volume": 0.001, - "invest_coin": "BUSD", - "max_open_orders": 5 + "class_name": "FutureGridStrategy", + "upper_price": 2.0, + "bottom_price": 1.35, + "max_open_orders": 5, + "order_amount": 5000, + "start_price": 0.0, + "direction_int": 1 } } } \ No newline at end of file diff --git a/gridtrader/tools/__init__.py b/gridtrader/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gridtrader/tools/common/__init__.py b/gridtrader/tools/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gridtrader/tools/common/contract_handler.py b/gridtrader/tools/common/contract_handler.py new file mode 100644 index 0000000..54f95eb --- /dev/null +++ b/gridtrader/tools/common/contract_handler.py @@ -0,0 +1,20 @@ +from decimal import Decimal, ROUND_DOWN + + +class ContractHandler: + def __init__(self, price_tick_str:str=None,price_tick:Decimal=None): + # 假设 price_tick 是 Decimal 类型 + if price_tick_str is not None: + self.price_tick = Decimal(price_tick_str) + elif price_tick is not None: + self.price_tick = price_tick + + def process_price(self, price): + # 确保 price 也转换为 Decimal 类型 + price = Decimal(price) + + # 将 price 按照 price_tick 进行舍入 + processed_price = (price // self.price_tick) * self.price_tick + processed_price = float(processed_price) + # 返回处理后的价格(保留原始精度) + return processed_price diff --git a/gridtrader/trader/strategies/future_grid_strategy.py b/gridtrader/trader/strategies/future_grid_strategy.py index 10a8ac1..deae0cb 100644 --- a/gridtrader/trader/strategies/future_grid_strategy.py +++ b/gridtrader/trader/strategies/future_grid_strategy.py @@ -1,74 +1,209 @@ -from ..engine import CtaEngine, EVENT_TIMER - -from gridtrader.trader.object import Status +from decimal import Decimal, ROUND_DOWN from typing import Union, Optional -from gridtrader.trader.utility import floor_to + +import ccxt + from gridtrader.trader.object import OrderData, TickData, TradeData, ContractData -from .template import CtaTemplate +from gridtrader.trader.object import Status from gridtrader.trader.utility import GridPositionCalculator +from gridtrader.tools.common.contract_handler import ContractHandler +from .template import CtaTemplate +from ..engine import CtaEngine +from ...event import EVENT_TIMER class FutureGridStrategy(CtaTemplate): """ - 币安合约中性网格 - 策略在震荡行情下表现很好,但是如果发生趋势行情,单次止损会比较大,导致亏损过多。 - - 免责声明: 本策略仅供测试参考,本人不负有任何责任。使用前请熟悉代码。测试其中的bugs, 请清楚里面的功能后再使用。 - 币安邀请链接: https://www.binancezh.pro/cn/futures/ref/51bitquant - 合约邀请码:51bitquant - - - Disclaimer: - Invest in Crypto currency is high risk. Take care of yourself. I am not responsible for your investment. - Binance Referral Link: https://www.binancezh.pro/cn/futures/ref/51bitquant - + 优化后的币安合约网格策略,支持多头和空头模式。 + 多头模式下,下方网格的单格金额是上方网格的1.1倍。 + 空头模式下,上方网格的单格金额是下方网格的1.1倍。 + 总金额保持不变,且满足最小下单数量。 """ author = "51bitquant" - # parameters - upper_price = 0.0 # The grid strategy high/upper price 执行策略的最高价. - bottom_price = 0.0 # The grid strategy low/bottom price 执行策略的最低价. - grid_number = 100 # grid number 网格的数量. - order_volume = 0.05 # order volume 每次下单的数量. - max_open_orders = 5 # max open price 一边订单的数量. - - # the strategy will stop when the price break the upper/bottom price, if you set the close_position when stop True, - # it will automatically close your position. - - # variables - avg_price = 0.0 # current average price for the position 持仓的均价 - step_price = 0.0 # price step between two grid 网格的间隔 - trade_times = 0 # trade times - - parameters = ["upper_price", "bottom_price", "grid_number", "order_volume", "max_open_orders"] - - variables = ["avg_price", "step_price", "trade_times"] + # 参数 + upper_price = 0.0 # 策略最高价 + bottom_price = 0.0 # 策略最低价 + grid_number = 100 # 网格数量 + max_open_orders = 5 # 最大同时挂单数 + order_amount = 5000 # 下单总金额 + start_price = 0.0 # 启动价格 + stop_loss_price = 0.0 # 止损价格 + direction_int = 1 # 方向:1为多头,-1为空头 + + # 变量 + avg_price = 0.0 # 持仓均价 + step_price = 0.0 # 网格间距 + trade_times = 0 # 成交次数 + price_volume_dict = {} # 保存每个价格对应的下单数量 + start_price_triggered = False # 启动价格是否已触发 + lower_grid_total_volume = 0.0 # 下方网格的总下单量 + upper_grid_total_volume = 0.0 # 上方网格的总下单量 + first_order = True # 标记是否是第一次下单 + orders_dict = {} # 存储所有订单的字典 + + parameters = ["bottom_price", "upper_price", "max_open_orders", "order_amount", "start_price", "direction_int","stop_loss_price"] + variables = ["avg_price", "step_price", "trade_times", "price_volume_dict", "start_price_triggered", + "lower_grid_total_volume", "upper_grid_total_volume"] def __init__(self, cta_engine: CtaEngine, strategy_name, vt_symbol, setting): - """""" super().__init__(cta_engine, strategy_name, vt_symbol, setting) - - self.long_orders_dict = {} # long orders dict {'orderid': price} - self.short_orders_dict = {} # short orders dict {'orderid': price} - + self.is_sell_outed = False + self.exchange = self.create_exchange() # 创建 ccxt 交易所实例 + self.long_orders_dict = {} # 多单挂单字典 + self.short_orders_dict = {} # 空单挂单字典 self.tick: Union[TickData, None] = None self.contract_data: Optional[ContractData] = None - self.pos_calculator = GridPositionCalculator() self.timer_count = 0 - - def on_init(self): + self._ContractHandler = None + self.fake_active_orders_price_list = None + + def create_exchange(self): + """创建 ccxt 交易所实例""" + # 获取历史订单 + fbinance = ccxt.binance() + binance_key4 = 'qBFimviRucbMNt9bcPKWBIrhrAqJpcGlPjDMkiIFj04GzhT0YNsLq9A1XU9IFC2Y' + binance_secret4 = 'UvkbgNauNOGMwYxnGbz81hFYbhGaEdNdFfirwLUIE72McPhc4eJOkSelS65Stbpu' + fbinance.apiKey = binance_key4 + fbinance.secret = binance_secret4 + fbinance.enableRateLimit = True + fbinance.password = '5601564a' + fbinance.options['defaultType'] = 'future' + fbinance.options["warnOnFetchOpenOrdersWithoutSymbol"] = False + fbinance.name = 'future-binance' + fbinance.enableRateLimit = True + + exchange = fbinance + + return exchange + + def calculate_grid_parameters(self): """ - Callback when strategy is inited. + 根据下单金额或手动设置的价格范围和网格数量计算网格参数。 + 确保每个网格的单格数量满足最小下单数量,同时保持总下单金额不变。 """ + if not self.order_amount: + return + + ### + fake_active_orders = self.cta_engine.main_engine.future_gateway.active_orders + self.fake_active_orders_price_list = [float(order.price) for order in fake_active_orders.values()] + + self._ContractHandler = ContractHandler(self.contract_data.price_tick) + + # 获取最小下单数量 + min_volume = self.contract_data.min_volume + + # 根据 min_volume 确定需要保留的小数位数 + decimal_places = abs(Decimal(str(min_volume)).as_tuple().exponent) + quantize_str = f"0.{'0' * decimal_places}" # 例如 min_volume=0.001 -> "0.000" + + # 计算价格范围 + price_range = self.upper_price - self.bottom_price + if price_range <= 0: + raise ValueError("价格区间设置错误,上限价格必须大于下限价格。") + + if self.vt_symbol.split(".")[0] == "BTCUSDT": + rate = 0.003 + else: + rate = 0.005 + + # 初始网格数量 + grid_spacing = self.upper_price * rate # 网格间距为下限价格的 0.6% + self.grid_number = max(1, int(price_range / grid_spacing)) # 初始网格数量 + + # 调整网格数量,确保每个网格的币数量满足最小下单数量 + while True: + # 计算网格间距 + self.step_price = self._ContractHandler.process_price(price_range / self.grid_number) + + # 计算下方网格和上方网格的数量 + lower_grid_number = self.grid_number // 2 + upper_grid_number = self.grid_number - lower_grid_number + + # 计算下方网格和上方网格的单格金额 + if self.direction_int == 1: # 多头模式 + upper_grid_amount = self.order_amount / (upper_grid_number * 1.1 + lower_grid_number) + lower_grid_amount = upper_grid_amount * 1.1 + else: # 空头模式 + lower_grid_amount = self.order_amount / (lower_grid_number * 1.1 + upper_grid_number) + upper_grid_amount = lower_grid_amount * 1.1 + + # 计算每个网格的币数量 + self.price_volume_dict = {} + total_amount = 0 + self.lower_grid_total_volume = 0.0 # 重置下方网格的总下单量 + self.upper_grid_total_volume = 0.0 # 重置上方网格的总下单量 + for i in range(self.grid_number): + price = self.bottom_price + i * self.step_price + if i < lower_grid_number: + volume = lower_grid_amount / price # 金额转换为币数量 + self.lower_grid_total_volume += volume # 累加下方网格的总下单量 + else: + volume = upper_grid_amount / price # 金额转换为币数量 + self.upper_grid_total_volume += volume # 累加上方网格的总下单量 + + # 根据 min_volume 保留小数位数 + volume = float(Decimal(volume).quantize(Decimal(quantize_str), rounding=ROUND_DOWN)) + + # 如果币数量小于最小下单数量,减少网格数量并重新计算 + if volume < min_volume: + self.grid_number -= 1 + if self.grid_number < 1: + raise ValueError("无法满足最小下单数量要求,请调整参数。") + break + else: + price_dc = self._ContractHandler.process_price(price) + # 将 Decimal 键转换为 float + price_float = float(price_dc) + self.price_volume_dict[price_float] = volume + total_amount += volume * price + else: + # 所有网格的币数量都满足最小下单数量要求,退出循环 + break + + # 检查总金额是否超过设定值 + if total_amount > self.order_amount: + scale_factor = self.order_amount / total_amount + for price in self.price_volume_dict: + self.price_volume_dict[price] = float( + Decimal(self.price_volume_dict[price] * scale_factor).quantize(Decimal(quantize_str), + rounding=ROUND_DOWN)) + # 按比例调整下方和上方网格的总下单量 + self.lower_grid_total_volume *= scale_factor + self.upper_grid_total_volume *= scale_factor + + self.write_log( + f"Calculated Parameters: Upper Price: {self.upper_price}, Bottom Price: {self.bottom_price}, " + f"Grid Number: {self.grid_number}, Step Price: {self.step_price}, " + f"Price Volume Dict: {self.price_volume_dict}, " + f"Lower Grid Total Volume: {self.lower_grid_total_volume}, " + f"Upper Grid Total Volume: {self.upper_grid_total_volume}, " + f"Mode: {'Long' if self.direction_int == 1 else 'Short'}") + + # 计算价格变化率 + self.calculate_price_change_rate() + # def get_min_volume(self, vt_symbol): + # """获取最小下单数量""" + # symbol = vt_symbol.split(".")[0] + # data = settings_exchange.load_markets() + # symbol = symbol.replace("USDT", "/USDT") + # precision = data[symbol]["precision"]["amount"] + # return 10 ** -precision # 最小下单数量 + + def on_init(self): + """策略初始化回调""" self.write_log("Init Strategy") def on_start(self): - """ - Callback when strategy is started. - """ - self.write_log("Start Strategy") self.contract_data = self.cta_engine.main_engine.get_contract(self.vt_symbol) + """策略启动回调""" + self.calculate_grid_parameters() + # self.avoid_finished_orders() + self.write_log(f"Calculated Parameters: Upper Price: {self.upper_price}, Bottom Price: {self.bottom_price}, " + f"Grid Number: {self.grid_number}, Step Price: {self.step_price}, " + f"Price Volume Dict: {self.price_volume_dict}") if not self.contract_data: self.write_log(f"Could Not Find The Symbol:{self.vt_symbol}, Please Connect the Api First.") @@ -82,154 +217,280 @@ def on_start(self): self.cta_engine.event_engine.register(EVENT_TIMER, self.process_timer) def on_stop(self): - """ - Callback when strategy is stopped. - """ + """策略停止回调""" self.write_log("Stop Strategy") self.cta_engine.event_engine.unregister(EVENT_TIMER, self.process_timer) def process_timer(self, event): + """定时器回调""" self.timer_count += 1 if self.timer_count >= 10: self.timer_count = 0 - # remove the order(highest price order for short, lowest price to for long) - # to keep the max open order meet requirements + # 移除超出最大挂单数的订单 if len(self.long_orders_dict.keys()) > self.max_open_orders: - - vt = list(self.long_orders_dict.keys())[0] - lowest_price = self.long_orders_dict[vt] - cancel_order_id = None - - for orderid in self.long_orders_dict.keys(): - order_price = self.long_orders_dict[orderid] - if lowest_price >= order_price: - cancel_order_id = orderid - lowest_price = order_price - - if cancel_order_id: - self.cancel_order(cancel_order_id) + cancel_order_id = min(self.long_orders_dict.keys(), key=lambda k: self.long_orders_dict[k]) + self.cancel_order(cancel_order_id) if len(self.short_orders_dict.keys()) > self.max_open_orders: - - vt = list(self.short_orders_dict.keys())[0] - highest_price = self.short_orders_dict[vt] - cancel_order_id = None - - for orderid in self.short_orders_dict.keys(): - order_price = self.short_orders_dict[orderid] - if highest_price <= order_price: - cancel_order_id = orderid - highest_price = order_price - - if cancel_order_id: - self.cancel_order(cancel_order_id) + cancel_order_id = max(self.short_orders_dict.keys(), key=lambda k: self.short_orders_dict[k]) + self.cancel_order(cancel_order_id) self.put_event() def on_tick(self, tick: TickData): - """ - Callback of new tick data update. - """ - + """Tick 数据回调""" if tick and tick.bid_price_1 > 0 and self.contract_data: self.tick = tick + # 检查是否达到止损价格 + if self.stop_loss_price > 0 and tick.bid_price_1 <= self.stop_loss_price and not self.is_sell_outed: + self.write_log(f"触发止损,当前价格: {tick.bid_price_1},止损价格: {self.stop_loss_price}") + self.on_stop() # 触发停止功能 + self.sell_market() # 执行市价卖出 + return + if self.upper_price - self.bottom_price <= 0: return - step_price = (self.upper_price - self.bottom_price) / self.grid_number + # 获取当前价格 + current_price = float(self.tick.bid_price_1) - self.step_price = float(floor_to(step_price, self.contract_data.price_tick)) + # 检查是否达到启动价格并执行相应操作 + self.check_start_price_and_execute(current_price) - mid_count = round((float(self.tick.bid_price_1) - self.bottom_price) / self.step_price) + # 如果启动价格未触发,则不执行网格交易 + if not self.start_price_triggered: + return - if len(self.long_orders_dict.keys()) == 0: + # 从 price_volume_dict 中取出当前价格上下方的网格价格 + sorted_prices = sorted(self.price_volume_dict.keys()) + lower_prices = [price for price in sorted_prices if price < current_price] + upper_prices = [price for price in sorted_prices if price > current_price] - for i in range(self.max_open_orders): - price = self.bottom_price + (mid_count - i - 1) * self.step_price - if price < self.bottom_price: - return + # 处理多头挂单(下方网格) + if len(self.long_orders_dict.keys()) == 0: + for price in lower_prices[-self.max_open_orders:]: # 取最接近当前价格的 max_open_orders 个 + volume = self.price_volume_dict.get(price) + if volume is None: + continue # 如果价格不在字典中,跳过 - orders_ids = self.buy(price, self.order_volume) + # 下单 + orders_ids = self.buy(price, volume) for orderid in orders_ids: self.long_orders_dict[orderid] = price + # 处理空头挂单(上方网格) if len(self.short_orders_dict.keys()) == 0: - for i in range(self.max_open_orders): - price = self.bottom_price + (mid_count + i + 1) * self.step_price - if price > self.upper_price: - return - - orders_ids = self.short(price, self.order_volume) + for price in upper_prices[:self.max_open_orders]: # 取最接近当前价格的 max_open_orders 个 + volume = self.price_volume_dict.get(price) + if volume is None: + continue # 如果价格不在字典中,跳过 + # 下单 + orders_ids = self.short(price, volume) for orderid in orders_ids: self.short_orders_dict[orderid] = price + # 第一次下单后,将标志设置为 False + if self.first_order: + self.first_order = False + def on_order(self, order: OrderData): - """ - Callback of new order data update. - """ + """订单状态回调""" if order.vt_orderid not in (list(self.short_orders_dict.keys()) + list(self.long_orders_dict.keys())): return self.pos_calculator.update_position(order) self.avg_price = self.pos_calculator.avg_price + _ContractHandler = ContractHandler(self.contract_data.price_tick) if order.status == Status.ALLTRADED: + short_price = float(order.price) + float(self.step_price) + long_price = float(order.price) - float(self.step_price) + + if not(long_price >= self.bottom_price and short_price <= self.upper_price): + return if order.vt_orderid in self.long_orders_dict.keys(): del self.long_orders_dict[order.vt_orderid] - self.trade_times += 1 - short_price = float(order.price) + float(self.step_price) - + # 使用 min() 找最接近的价格 + short_price, volume = self.getVolume(short_price) if short_price <= self.upper_price: - orders_ids = self.short(short_price, self.order_volume) + if short_price in self.short_orders_dict.values(): # 检查是否已有该价格的订单 + self.write_log(f" short_price 跳过 {short_price}。") + return + orders_ids = self.short(short_price, volume) for orderid in orders_ids: - self.short_orders_dict[orderid] = short_price + self.short_orders_dict[orderid] = short_price # 存储在总字典中 + ## 补充买单 if len(self.long_orders_dict.keys()) < self.max_open_orders: count = len(self.long_orders_dict.keys()) + 1 long_price = float(order.price) - float(self.step_price) * count if long_price >= self.bottom_price: - orders_ids = self.buy(long_price, self.order_volume) + ## 方式3:使用 min() 找最接近的价格 + closest_price, volume = self.getVolume(long_price) + + if closest_price in self.long_orders_dict.values(): # 检查是否已有该价格的订单 + self.write_log(f" long_price 跳过 {closest_price}。") + return + + orders_ids = self.buy(closest_price, volume) for orderid in orders_ids: self.long_orders_dict[orderid] = long_price if order.vt_orderid in self.short_orders_dict.keys(): - del self.short_orders_dict[order.vt_orderid] - + del self.short_orders_dict[order.vt_orderid] # 从总字典中删除已成交的订单 self.trade_times += 1 - long_price = float(order.price) - float(self.step_price) + + # 使用 min() 找最接近的价格 + long_price, volume = self.getVolume(long_price) if long_price >= self.bottom_price: - orders_ids = self.buy(long_price, self.order_volume) + if long_price in self.long_orders_dict.values(): # 检查是否已有该价格的订单 + self.write_log(f" long_price 跳过 {long_price}。") + return + + orders_ids = self.buy(long_price, volume) for orderid in orders_ids: - self.long_orders_dict[orderid] = long_price + self.long_orders_dict[orderid] = long_price # 存储在总字典中 + ## 补充卖单 if len(self.short_orders_dict.keys()) < self.max_open_orders: count = len(self.short_orders_dict.keys()) + 1 short_price = float(order.price) + float(self.step_price) * count - if short_price <= self.upper_price: - orders_ids = self.short(short_price, self.order_volume) + ## 方式3:使用 min() 找最接近的价格 + closest_price, volume = self.getVolume(short_price) + + if closest_price in self.short_orders_dict.values(): # 检查是否已有该价格的订单 + self.write_log(f" short_price 跳过 {closest_price}。") + return + + orders_ids = self.short(closest_price, volume) for orderid in orders_ids: self.short_orders_dict[orderid] = short_price if not order.is_active(): if order.vt_orderid in self.long_orders_dict.keys(): del self.long_orders_dict[order.vt_orderid] - elif order.vt_orderid in self.short_orders_dict.keys(): - del self.short_orders_dict[order.vt_orderid] self.put_event() def on_trade(self, trade: TradeData): + """成交回调""" + self.put_event() + + def check_start_price_and_execute(self, current_price: float): + if self.start_price == 0: + self.start_price_triggered = True # 标记启动价格已触发 + + # if self.position != 0: + # self.start_price_triggered = True # 存在的话,就是启动了 + """ - Callback of new trade data update. + 检查当前价格是否达到启动价格,如果是,则立即执行相应的操作。 """ - self.put_event() + if self.direction_int == 1: # 多头模式 + if current_price <= self.start_price and not self.start_price_triggered: + # 获取所有空头订单的价格和数量 + for price, volume in self.price_volume_dict.items(): + if price > current_price: # 空头订单的价格高于当前价格 + # 立即买入 + orders_ids = self.buy(price, volume) + for orderid in orders_ids: + self.long_orders_dict[orderid] = price + self.start_price_triggered = True # 标记启动价格已触发 + elif self.direction_int == -1: # 空头模式 + if current_price >= self.start_price and not self.start_price_triggered: + # 获取所有多头订单的价格和数量 + for price, volume in self.price_volume_dict.items(): + if price < current_price: # 多头订单的价格低于当前价格 + # 立即卖出 + orders_ids = self.sell(price, volume) + for orderid in orders_ids: + self.short_orders_dict[orderid] = price + self.start_price_triggered = True # 标记启动价格已触发 + + def calculate_price_change_rate(self): + """ + 计算 price_volume_dict 中各个价格之间的变化率。 + 返回一个字典,键为价格对 (prev_price, current_price),值为变化率。 + """ + if not self.price_volume_dict: + return {} + + # 将价格排序 + sorted_prices = sorted(self.price_volume_dict.keys()) + + # 计算变化率 + change_rate_dict = {} + for i in range(1, len(sorted_prices)): + prev_price = sorted_prices[i - 1] + current_price = sorted_prices[i] + change_rate = (current_price - prev_price) / prev_price * 100 # 变化率(百分比) + change_rate_dict[(prev_price, current_price)] = change_rate + print(current_price,change_rate) + + return change_rate_dict + + def avoid_finished_orders(self): + symbol = self.vt_symbol.split(".")[0] + symbol = symbol.replace("USDT", "/USDT") + orders = self.exchange.fetch_my_trades(symbol=symbol, limit=10) + + self.set_avoid_finished_orders = set() + # 打印订单信息 + for i, order in enumerate(orders): + print(f"订单 {i + 1}:") + print(f" 时间: {self.exchange.iso8601(order['timestamp'])}") + print(f" 交易对: {order['symbol']}") + print(f" 类型: {order['side']}") # 买入(buy)或卖出(sell) + print(f" 数量: {order['amount']}") + print(f" 成交价格: {order['price']}") + print(f" 成交金额: {order['cost']}") + print(f" 手续费: {order['fee']['cost']} {order['fee']['currency']}") + print("-" * 30) + + self.set_avoid_finished_orders.add(order['price']) + + ## 使用 min() 找最接近的价格 + def getVolume(self, price): + closest_price = min(self.price_volume_dict.keys(), key=lambda x: abs(x - price)) + volume = self.price_volume_dict.get(closest_price, None) + + return closest_price, volume + + def sell_market(self): + """市价卖出功能""" + symbol2 = self.vt_symbol.split(".")[0] # 获取交易对 + symbol = symbol2.replace("USDT", "/USDT") + + # 获取合约账户余额信息 + balance = self.exchange.fetch_balance({"type": "future"}) + btc_positions = balance.get('info', {}).get('positions', []) + + # 过滤出目标交易对的持仓 + btc_position = next((pos for pos in btc_positions if pos["symbol"] == symbol2), None) + + if not btc_position: + self.write_log(f"未持有 {symbol} 的仓位,无法执行市价卖出。") + return + + position_amt = float(btc_position.get("positionAmt", 0)) # 获取仓位数量 + + if position_amt == 0: + self.write_log(f"{symbol} 持仓数量为 0,无法执行市价卖出。") + return + + self.write_log(f"持有 {position_amt} 个 {symbol},准备市价卖出。") + + # 使用 ccxt 执行市价卖出 + self.exchange.create_market_sell_order(symbol, abs(position_amt)) + self.write_log(f"已市价卖出 {btc_position} 个 {symbol}。") + self.is_sell_outed = True \ No newline at end of file diff --git a/gridtrader/trader/strategies/template.py b/gridtrader/trader/strategies/template.py index 5e4f5c6..4a94289 100644 --- a/gridtrader/trader/strategies/template.py +++ b/gridtrader/trader/strategies/template.py @@ -157,20 +157,14 @@ def cover(self, price: float, volume: float): """ return self.send_order(Direction.LONG, Offset.CLOSE, price, volume) - def send_order( - self, - direction: Direction, - offset: Offset, - price: float, - volume: float - ): + def send_order(self, direction: Direction, offset: Offset, price: float, volume: float): """ - Send a new order. + Send a new order with logging. """ if self.trading: - vt_orderids = self.cta_engine.send_order( - self, direction, offset, price, volume - ) + vt_orderids = self.cta_engine.send_order(self, direction, offset, price, volume) + print( + f"Order Sent | Direction: {direction}, Offset: {offset}, Price: {price}, Volume: {volume}, Order IDs: {vt_orderids}") return vt_orderids else: return [] diff --git a/main_spot_script.py b/main_spot_script.py index e4f0bc4..3e82712 100644 --- a/main_spot_script.py +++ b/main_spot_script.py @@ -2,8 +2,8 @@ from logging import INFO from gridtrader.event import EventEngine -from gridtrader.trader.setting import SETTINGS from gridtrader.trader.engine import MainEngine, CtaEngine +from gridtrader.trader.setting import SETTINGS SETTINGS["log.active"] = True SETTINGS["log.level"] = INFO diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d3c8880 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +__version__ = "2.2" diff --git a/tests/test_future_grid_strategy.py b/tests/test_future_grid_strategy.py new file mode 100644 index 0000000..9e5cfec --- /dev/null +++ b/tests/test_future_grid_strategy.py @@ -0,0 +1,154 @@ +import unittest +from unittest.mock import Mock, patch +from decimal import Decimal + +from gridtrader.trader.constant import Product, Exchange +from gridtrader.trader.strategies.future_grid_strategy import FutureGridStrategy +from gridtrader.trader.object import TickData, OrderData, ContractData, Status +from gridtrader.trader.utility import GridPositionCalculator + +class TestFutureGridStrategy(unittest.TestCase): + def setUp(self): + """测试前的设置""" + self.cta_engine = Mock() + self.cta_engine.main_engine = Mock() + + # 模拟合约数据 + self.contract = ContractData( + symbol="BTC-USDT", + exchange=Exchange.BINANCE, + name="BTC永续合约", + price_tick=0.01, + product=Product.FUTURES, + gateway_name="BTCUSDT", + min_volume=0.001 + ) + self.cta_engine.main_engine.get_contract = Mock(return_value=self.contract) + + # 创建策略实例 + self.strategy = FutureGridStrategy( + cta_engine=self.cta_engine, + strategy_name="test_strategy", + vt_symbol="BTC-USDT.BINANCE", + setting={ + "upper_price": 50000.0, + "bottom_price": 40000.0, + "grid_number": 100, + "max_open_orders": 5, + "order_amount": 5000.0, + "start_price": 45000.0, + "direction_int": 1 + } + ) + + # 模拟gateway + self.fake_gateway = Mock() + self.fake_gateway.active_orders = {} + self.cta_engine.main_engine.future_gateway = self.fake_gateway + self.contract_data = self.cta_engine.main_engine.get_contract(self.strategy.vt_symbol) + + def test_calculate_grid_parameters(self): + """测试网格参数计算""" + self.strategy.calculate_grid_parameters() + + # 验证基本参数是否正确计算 + self.assertGreater(len(self.strategy.price_volume_dict), 0) + self.assertGreater(self.strategy.step_price, 0) + + # 验证网格价格范围 + prices = list(self.strategy.price_volume_dict.keys()) + self.assertGreaterEqual(min(prices), self.strategy.bottom_price) + self.assertLessEqual(max(prices), self.strategy.upper_price) + + # 验证下单量是否满足最小要求 + for volume in self.strategy.price_volume_dict.values(): + self.assertGreaterEqual(volume, self.contract.min_volume) + + def test_check_start_price_and_execute(self): + """测试启动价格检查和执行""" + self.strategy.calculate_grid_parameters() + + # 测试多头模式 + self.strategy.direction_int = 1 + self.strategy.start_price_triggered = False + + # 模拟买入订单 + self.strategy.buy = Mock(return_value=["test_order_id"]) + + # 测试价格低于启动价格 + self.strategy.check_start_price_and_execute(44000.0) + self.assertTrue(self.strategy.start_price_triggered) + self.strategy.buy.assert_called() + + def test_on_tick(self): + """测试Tick数据处理""" + self.strategy.calculate_grid_parameters() + self.strategy.start_price_triggered = True + + # 创建模拟Tick数据 + tick = TickData( + symbol="BTC-USDT", + exchange="BINANCE", + datetime=None, + bid_price_1=45000.0, + bid_volume_1=1.0, + ask_price_1=45001.0, + ask_volume_1=1.0 + ) + + # 模拟下单方法 + self.strategy.buy = Mock(return_value=["test_order_id"]) + self.strategy.short = Mock(return_value=["test_order_id"]) + + # 测试tick处理 + self.strategy.on_tick(tick) + + # 验证是否创建了正确数量的订单 + total_orders = len(self.strategy.long_orders_dict) + len(self.strategy.short_orders_dict) + self.assertLessEqual(total_orders, self.strategy.max_open_orders * 2) + + def test_on_order(self): + """测试订单状态更新处理""" + self.strategy.calculate_grid_parameters() + + # 创建模拟订单 + order = OrderData( + symbol="BTC-USDT", + exchange="BINANCE", + orderid="test_order_id", + direction=1, # 买入 + price=44000.0, + volume=0.1, + status=Status.ALLTRADED + ) + + # 添加到订单字典 + self.strategy.long_orders_dict["test_order_id"] = order.price + + # 模拟下单方法 + self.strategy.buy = Mock(return_value=["new_test_order_id"]) + self.strategy.short = Mock(return_value=["new_test_order_id"]) + + # 测试订单完全成交 + self.strategy.on_order(order) + + # 验证订单是否被正确处理 + self.assertNotIn("test_order_id", self.strategy.long_orders_dict) + self.assertEqual(self.strategy.trade_times, 1) + + def test_price_consistency(self): + """测试价格类型一致性""" + self.strategy.calculate_grid_parameters() + + # 验证价格字典中的键是否都是float类型 + for price in self.strategy.price_volume_dict.keys(): + self.assertIsInstance(price, float) + + # 验证订单字典中的价格是否都是float类型 + test_order_id = "test_order_id" + test_price = 44000.0 + self.strategy.long_orders_dict[test_order_id] = test_price + self.assertIsInstance(self.strategy.long_orders_dict[test_order_id], float) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/utils/contract_handler.py b/utils/contract_handler.py new file mode 100644 index 0000000..734600a --- /dev/null +++ b/utils/contract_handler.py @@ -0,0 +1,20 @@ +from decimal import Decimal, ROUND_DOWN + + +class ContractHandler: + def __init__(self, price_tick_str:str=None,price_tick:Decimal=None): + # 假设 price_tick 是 Decimal 类型 + if price_tick_str is not None: + self.price_tick = Decimal(price_tick_str) + elif price_tick is not None: + self.price_tick = price_tick + + def process_price(self, price): + # 确保 price 也转换为 Decimal 类型 + price = Decimal(price) + + # 将 price 按照 price_tick 进行舍入 + processed_price = (price // self.price_tick) * self.price_tick + + # 返回处理后的价格(保留原始精度) + return processed_price