openfund-maker 1.3.18__tar.gz → 2.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openfund-maker
3
- Version: 1.3.18
3
+ Version: 2.0.2
4
4
  Summary: Openfund-maker.
5
5
  Requires-Python: >=3.9,<4.0
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "openfund-maker"
3
- version = "1.3.18"
3
+ version = "2.0.2"
4
4
  description = "Openfund-maker."
5
5
  authors = []
6
6
  readme = "README.md"
@@ -0,0 +1,220 @@
1
+ # -*- coding: utf-8 -*-
2
+ import traceback
3
+ import pandas as pd
4
+
5
+ from maker.SMCStrategyMaker import SMCStrategyMaker
6
+
7
+
8
+ class BestFVGStrategyMaker(SMCStrategyMaker):
9
+ def __init__(self, config, platform_config, feishu_webhook=None,logger=None):
10
+ super().__init__(config, platform_config, feishu_webhook, logger)
11
+ self.place_order_prices = {} # 记录每个symbol的挂单价格
12
+ self.htf_last_CHoCH = {} #记录HTF的CHoCH struct
13
+
14
+ def check_price_in_fvg(self, df, side, fvg):
15
+ """
16
+ 检查最大或最小价格是否在FVG范围内
17
+ Args:
18
+ htf_last_side: str, 方向 'buy' or 'sell'
19
+ fvg_top: float, FVG上边界
20
+ fvg_bot: float, FVG下边界
21
+ Returns:
22
+ bool: 是否在FVG范围内
23
+ """
24
+ if fvg is None:
25
+ return False
26
+
27
+ fvg_top = fvg["top"]
28
+ fvg_bot = fvg["bot"]
29
+ fvg_index = fvg["index"]
30
+
31
+ # 检查价格是否在FVG范围内,bar_index 是从fvg_index+2开始
32
+ if side == 'buy':
33
+ # 多头趋势检查最低价是否进入FVG区域
34
+ min_price = min(df['low'].iloc[fvg_index+2:])
35
+ return min_price <= fvg_top
36
+ else:
37
+ # 空头趋势检查最高价是否进入FVG区域
38
+ max_price = max(df['high'].iloc[fvg_index+2:])
39
+ return fvg_bot <= max_price
40
+
41
+
42
+ def process_pair(self,symbol,pair_config):
43
+ self.logger.info("=" * 60)
44
+ """_summary_
45
+ 1. HTF 判断struct趋势
46
+ 2. HTF 获取最新的两个极值点,设置折价区和溢价区
47
+ 3. HTF 在折价区找FVG,监控价格是否进入FVG
48
+
49
+ 4. LTF 判断struct趋势是否有CHoCH
50
+ 5. LTF 寻找FVG,下单
51
+ """
52
+ try:
53
+ # 检查是否有持仓,有持仓不进行下单
54
+ if self.check_position(symbol=symbol) :
55
+ self.logger.info(f"{symbol} : 有持仓合约,不进行下单。")
56
+ if symbol in self.place_order_prices:
57
+ self.place_order_prices[symbol] = {}
58
+ return
59
+
60
+
61
+ smc_strategy = pair_config.get('smc_strategy',{})
62
+
63
+ # 获取历史K线,HTF和CTF
64
+ htf = str(smc_strategy.get('HTF','15m'))
65
+ ltf = str(smc_strategy.get('LTF', '1m'))
66
+ htf_Klines = self.get_historical_klines(symbol=symbol, bar=htf)
67
+ htf_df = self.format_klines(htf_Klines)
68
+
69
+ enable_FVG = smc_strategy.get('enable_FVG',True) # 是否启用FVG
70
+ enable_OB = smc_strategy.get('enable_OB',True) # 是否启用OB
71
+ self.logger.debug(f"{symbol} : BestFVGSMC策略 {ltf}|{htf} enable_FVG={enable_FVG} enable_OB={enable_OB} ...")
72
+
73
+ # 初始化HTF趋势相关变量
74
+ htf_last_side, htf_last_CHoCH_label = None, None
75
+
76
+ # 检查是否有上一个CHoCH结构
77
+ htf_last_CHoCH = self.htf_last_CHoCH.get(symbol,None)
78
+
79
+ # 如果存在上一个CHoCH结构,更新趋势标签和方向
80
+ if htf_last_CHoCH:
81
+ htf_last_CHoCH_label = htf_last_CHoCH["struct"]
82
+ htf_last_side = htf_last_CHoCH["side"]
83
+
84
+
85
+ # 1. HTF 判断struct趋势(CHoCH\SMS\BMS) ,HTF struct 看趋势,CTF 看FVG和OB的位置
86
+ swing_points_length = smc_strategy.get('swing_points_length',10)
87
+ htf_struct = self.detect_struct(htf_df,prd=swing_points_length,struct_key="CHoCH")
88
+ htf_struct_label = htf_struct["struct"]
89
+ htf_last_pivot_high = htf_struct["pivot_high"]
90
+ htf_last_pivot_low = htf_struct["pivot_low"]
91
+ htf_last_mid_line = self.calculate_ce(symbol,htf_last_pivot_high,htf_last_pivot_low)
92
+
93
+
94
+ # 检查是否已形成CHoCH结构
95
+ if not (htf_last_CHoCH or 'CHoCH' in htf_struct_label):
96
+ self.logger.debug(f"{symbol} : {htf} 未形成 CHoCH struct,不下单。{htf_struct}。")
97
+ return
98
+
99
+ # 更新最新的CHoCH结构信息
100
+ if 'CHoCH' in htf_struct_label and htf_struct_label != htf_last_CHoCH_label:
101
+ self.htf_last_CHoCH[symbol] = htf_struct
102
+ htf_last_CHoCH = htf_struct
103
+ htf_last_CHoCH_label = htf_struct_label
104
+ htf_last_side = htf_struct["side"]
105
+
106
+
107
+ # 2. HTF 获取最新的两个极值点,设置折价(discount)区和溢价(premium)区
108
+
109
+
110
+ # 计算溢价和折价区
111
+ premium_box = {
112
+ 'top': htf_last_pivot_high,
113
+ 'bot': htf_last_mid_line,
114
+ 'ce': self.calculate_ce(symbol,htf_last_pivot_high,htf_last_mid_line)
115
+ }
116
+ discount_box = {
117
+ 'top': htf_last_mid_line,
118
+ 'bot': htf_last_pivot_low,
119
+ 'ce': self.calculate_ce(symbol,htf_last_mid_line,htf_last_pivot_low)
120
+ }
121
+
122
+ self.logger.info(f"{symbol} : {htf} 趋势={htf_last_CHoCH_label}")
123
+ self.logger.debug(f"{symbol} : \npivot_high={htf_last_pivot_high} pivot_low={htf_last_pivot_low} mid_line={htf_last_mid_line}\n溢价区={premium_box}\n折价区={discount_box}")
124
+
125
+ # 3. find HTF FVG
126
+ pivot_index = htf_struct["pivot_low_index"] if htf_last_side == "buy" else htf_struct["pivot_high_index"]
127
+ htf_fvg_boxes = self.find_fvg_boxes(htf_df,side=htf_last_side,threshold=htf_last_mid_line,check_balanced=False,pivot_index=pivot_index)
128
+ if len(htf_fvg_boxes) == 0:
129
+ self.logger.debug(f"{symbol} : HTF={htf} 方向={htf_last_side}, 未找到 FVG")
130
+ return
131
+ self.logger.debug(f"{symbol} : HTF_fvg_box={htf_fvg_boxes[-1]}")
132
+
133
+ # 判断是否进入最近的FVG
134
+ if_tap_into_fvg = self.check_price_in_fvg(htf_df,htf_last_side,htf_fvg_boxes[-1])
135
+ if not if_tap_into_fvg:
136
+ self.logger.debug(f"{symbol} : 价格[未进入]HTF_FVG区域,不进行下单")
137
+ return
138
+ else:
139
+ self.logger.debug(f"{symbol} : 价格[进入]HTF_FVG区域,开始下单。fvgbox={htf_fvg_boxes[-1]}")
140
+
141
+ # 4. LTF 判断struct趋势是否有CHoCH
142
+
143
+
144
+ ltf_kLines = self.get_historical_klines(symbol=symbol, bar=ltf)
145
+ ltf_df = self.format_klines(ltf_kLines)
146
+
147
+ ltf_struct = self.detect_struct(ltf_df,prd=swing_points_length)
148
+ ltf_struct_label = ltf_struct["struct"]
149
+ ltf_struct_side = ltf_struct["side"]
150
+ ltf_last_pivot_high = ltf_struct["pivot_high"]
151
+ ltf_last_pivot_low = ltf_struct["pivot_low"]
152
+ ltf_last_mid_line = self.calculate_ce(symbol,ltf_last_pivot_high,ltf_last_pivot_low)
153
+
154
+ # 计算溢价和折价区
155
+ ltf_premium_box = {
156
+ 'top': ltf_last_pivot_high,
157
+ 'bot': ltf_last_mid_line,
158
+ 'ce': self.calculate_ce(symbol,ltf_last_pivot_high,ltf_last_mid_line)
159
+ }
160
+ ltf_discount_box = {
161
+ 'top': ltf_last_mid_line,
162
+ 'bot': ltf_last_pivot_low,
163
+ 'ce': self.calculate_ce(symbol,ltf_last_mid_line,ltf_last_pivot_low)
164
+ }
165
+
166
+ self.logger.info(f"{symbol} : {ltf} 趋势={ltf_struct_label} struct={ltf_struct}")
167
+ self.logger.debug(f"{symbol} : \npivot_high={ltf_last_pivot_high} pivot_low={ltf_last_pivot_low} mid_line={ltf_last_mid_line}\n溢价区={ltf_premium_box}\n折价区={ltf_discount_box}")
168
+
169
+
170
+ # 5. LTF 寻找FVG,下单
171
+ # if htf_last_CHoCH_label != ltf_struct_label :
172
+ if htf_last_side != ltf_struct_side :
173
+ self.logger.debug(f"{symbol} : {htf} {htf_last_CHoCH_label} VS {ltf} {ltf_struct_label} 趋势不一致,不进行下单")
174
+ return
175
+
176
+ threshold = 0.0
177
+ # 如果LTF结构是BMS,趋势强,FVG的范围不要求,只要能接上就行,如果结构是CHoCH或SMS,趋势弱,则取折价区或溢价区的FVG
178
+ if 'BMS' in ltf_struct_label:
179
+ threshold = ltf_last_pivot_high if ltf_struct_side == "buy" else ltf_last_pivot_low
180
+ else:
181
+ threshold = self.calculate_ce(symbol,ltf_last_pivot_high,ltf_last_pivot_low)
182
+
183
+ pivot_index = ltf_struct["pivot_low_index"] if ltf_struct_side == "buy" else ltf_struct["pivot_high_index"]
184
+
185
+ ltf_fvg_boxes = self.find_fvg_boxes(ltf_df,side=ltf_struct_side,threshold=threshold,pivot_index=pivot_index)
186
+
187
+ if len(ltf_fvg_boxes) == 0:
188
+ self.logger.debug(f"{symbol} : LTF={ltf} 趋势={ltf_struct_label}, 未找到 FVG")
189
+ return
190
+
191
+ self.logger.debug(f"{symbol} : LTF_fvg_box={ltf_fvg_boxes[-1]}")
192
+
193
+
194
+ # 4. LTF 寻找FVG,下单
195
+ order_price = ltf_fvg_boxes[-1]["top"] if ltf_struct_side == "buy" else ltf_fvg_boxes[-1]["bot"]
196
+
197
+ latest_order_price = self.place_order_prices.get(symbol,0.0)
198
+ if order_price == latest_order_price:
199
+ self.logger.debug(f"{symbol} : 下单价格 {order_price} 未变化,不进行下单。")
200
+ return
201
+
202
+
203
+ # 下单
204
+ self.cancel_all_orders(symbol=symbol)
205
+ self.place_order(symbol=symbol, price=order_price, side=ltf_struct_side, pair_config=pair_config)
206
+ self.place_order_prices[symbol] = order_price # 记录下单价格,过滤重复下单
207
+ self.logger.debug(f"{symbol} : {ltf_struct_side}, 下单价格 {order_price}")
208
+
209
+
210
+ except KeyboardInterrupt:
211
+ self.logger.info("程序收到中断信号,开始退出...")
212
+ except Exception as e:
213
+ error_message = f"程序异常退出: {str(e)}"
214
+ self.logger.error(error_message,exc_info=True)
215
+ traceback.print_exc()
216
+ self.send_feishu_notification(error_message)
217
+ finally:
218
+ self.logger.info("-" * 60)
219
+
220
+
@@ -0,0 +1,580 @@
1
+ # -*- coding: utf-8 -*-
2
+ import traceback
3
+ import pandas as pd
4
+ import talib as ta
5
+
6
+ from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
7
+
8
+
9
+ class SMCStrategyMaker(ThreeLineStrategyMaker):
10
+ def __init__(self, config, platform_config, feishu_webhook=None,logger=None):
11
+ super().__init__(config, platform_config, feishu_webhook, logger)
12
+ self.place_order_prices = {} # 记录每个symbol的挂单价格
13
+
14
+ def place_order(self,symbol, price, side,pair_config):
15
+ """_summary_
16
+ 下单
17
+ Args:
18
+ symbol (_type_): _description_
19
+ price (_type_): _description_
20
+ amount_usdt (_type_): _description_
21
+ side (_type_): _description_
22
+ order_type (_type_): _description_
23
+ """
24
+ long_amount_usdt = pair_config.get('long_amount_usdt', 5)
25
+ short_amount_usdt = pair_config.get('short_amount_usdt', 5)
26
+ order_amount_usdt = 5
27
+ # order_type='optimal_limit_ioc'
28
+
29
+ if side == 'sell' :
30
+ self.logger.debug(f"{symbol} : 触发做空下单条件。")
31
+ order_amount_usdt = short_amount_usdt
32
+ elif side == 'buy' :
33
+ self.logger.debug(f"{symbol} : 触发做多下单条件。")
34
+ order_amount_usdt = long_amount_usdt
35
+ super().place_order(symbol=symbol, price=price, amount_usdt=order_amount_usdt, side=side)
36
+
37
+ def format_klines(self,klines) -> pd.DataFrame:
38
+
39
+ """_summary_
40
+ 格式化K线数据
41
+ Args:
42
+ klines (_type_): _description_
43
+ """
44
+ klines_df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
45
+ # 转换时间戳为日期时间
46
+ klines_df['timestamp'] = pd.to_datetime(klines_df['timestamp'], unit='ms')
47
+ klines_df['timestamp'] = klines_df['timestamp'].dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')
48
+
49
+ return klines_df
50
+
51
+ def find_OB_boxes(self, data, side, threshold, pivot_index, symbol=None, pair_config=None) -> list:
52
+ """_summary_
53
+ 识别OB
54
+ Args:
55
+ data (_type_): _description_
56
+ symbol (_type_): _description_
57
+
58
+ """
59
+ df = data.copy().iloc[pivot_index:]
60
+ # 首先计算实体的高点和低点,即开盘价和收盘价中的较大值和较小值
61
+ df['body_high'] = df[['open', 'close']].max(axis=1)
62
+ df['body_low'] = df[['open', 'close']].min(axis=1)
63
+
64
+ # 初始化OB的高点和低点列为空
65
+ df['OB_high'] = None
66
+ df['OB_low'] = None
67
+
68
+ # 使用布尔索引一次性更新OB_high
69
+ # df.loc[df['iUp'] == df.index, 'OB_high'] = df.loc[df['iUp'] == df.index, 'high']
70
+ # df.loc[df['iUp'] == df.index, 'OB_low'] = df['body_low'].shift(1).fillna(df['body_low'])
71
+
72
+ # df.loc[df['iDn'] == df.index, 'OB_low'] = df.loc[df['iDn'] == df.index, 'low']
73
+ # df.loc[df['iDn'] == df.index, 'OB_high'] = df['body_high'].shift(1).fillna(df['body_high'])
74
+
75
+ # print(df[['timestamp', 'pattern','high','low','Dn','iDn','Up','iUp','body_high','body_low']])
76
+
77
+ OB_boxes = []
78
+ # 根据交易方向构建OB盒子,OB区规则孤立高点+实体低点 孤立低点+实体高点
79
+ if side == 'buy':
80
+ # 买入方向的OB盒子构建
81
+ OB_boxes = [
82
+ {
83
+ 'index': idx,
84
+ 'top': float(df.loc[idx, 'low']), # OB低点为当前K线的最低点
85
+ 'bot': float(df.loc[idx - 1 if idx > df.index[0] else idx, 'body_high']) # OB高点为前一根K线的实体高点
86
+ }
87
+ for idx in df.index
88
+ # 判断条件:是第一根K线(极值点)或当前下降趋势大于前一个,且前一根K线实体高点小于阈值
89
+ if (idx == df.index[0] or (df.loc[idx, 'Dn'] > df.loc[idx - 1, 'Dn']))
90
+ and df.loc[idx - 1 if idx > df.index[0] else idx, 'body_high'] <= threshold
91
+ ]
92
+ else:
93
+ # 卖出方向的OB盒子构建
94
+ OB_boxes = [
95
+ {
96
+ 'index': idx,
97
+ 'top': float(df.loc[idx, 'high']), # OB高点为当前K线的最高点
98
+ 'bot': float(df.loc[idx - 1 if idx > df.index[0] else idx, 'body_low']) # OB低点为前一根K线的实体低点
99
+ }
100
+ for idx in df.index
101
+ # 判断条件:是第一根K线(极值点)或当前上升趋势小于前一个,且前一根K线实体低点大于阈值
102
+ if (idx == df.index[0] or (df.loc[idx, 'Up'] < df.loc[idx - 1, 'Up']))
103
+ and df.loc[idx - 1 if idx > df.index[0] else idx, 'body_low'] >= threshold
104
+ ]
105
+
106
+ return OB_boxes
107
+
108
+
109
+ def find_fvg_boxes(self, data, side, threshold, check_balanced=True, pivot_index=0, symbol=None, pair_config=None) -> list:
110
+ """_summary_
111
+ 寻找公允价值缺口
112
+ Args:
113
+ data (_type_): K线数据
114
+ side (_type_): 交易方向 'buy'|'sell'
115
+ threshold (_type_): 阈值价格,通常为溢价和折价区的CE
116
+ check_balanced (bool): 是否检查FVG是否被平衡过,默认为True
117
+ pivot_index (int): 枢轴点索引,默认为0
118
+ symbol (_type_): 交易对名称
119
+ pair_config (_type_): 交易对配置
120
+ Returns:
121
+ list: FVG盒子列表,每个盒子包含以下字段:
122
+ # index: FVG出现的K线位置索引
123
+ # top: FVG的上边界价格,对应K线的最高价或最低价
124
+ # bot: FVG的下边界价格,对应K线的最高价或最低价
125
+ """
126
+
127
+ df = data.copy().iloc[pivot_index:]
128
+
129
+ fvg_boxes = []
130
+ if side == 'buy' :
131
+
132
+ # 处理看涨公允价值缺口
133
+ df.loc[:, 'is_bullish_fvg'] = df['high'].shift(3) < df['low'].shift(1)
134
+ bullish_df = df[df['is_bullish_fvg']].copy()
135
+ valid_indices = bullish_df.index[
136
+ (bullish_df.index - 1).isin(df.index) &
137
+ (bullish_df.index - 2).isin(df.index) &
138
+ (bullish_df.index - 3).isin(df.index)
139
+ ]
140
+
141
+ fvg_boxes = [
142
+ {
143
+ 'index': idx - 2, # FVG的索引
144
+ 'top': min(float(df.loc[idx - 1, 'low']),threshold), # FVG高点为右1K线的最低点
145
+ 'bot': float(df.loc[idx - 3, 'high']) # FVG低点为左1K线的最高点
146
+ }
147
+ # [df.loc[idx - 1, 'low'], df.loc[idx - 3, 'high'], idx - 2]
148
+ for idx in valid_indices
149
+ if df.loc[idx - 3, 'high'] <= threshold and
150
+ (not check_balanced or all((df.loc[idx:, 'low'] > df.loc[idx - 3, 'high']))) # check_balanced = true 检查FVG是否被平衡过
151
+ ]
152
+
153
+
154
+ else :
155
+ # 处理看跌公允价值缺口
156
+ df.loc[:, 'is_bearish_fvg'] = df['low'].shift(3) > df['high'].shift(1)
157
+
158
+ bearish_df = df[df['is_bearish_fvg']].copy()
159
+ valid_indices = bearish_df.index[
160
+ (bearish_df.index - 1).isin(df.index) &
161
+ (bearish_df.index - 2).isin(df.index) &
162
+ (bearish_df.index - 3).isin(df.index)
163
+ ]
164
+
165
+
166
+ fvg_boxes = [
167
+ {
168
+ 'index': idx - 2, # FVG的索引
169
+ 'top': float(df.loc[idx - 3, 'low']), # FVG高点为右1K线的最高点
170
+ 'bot': max(float(df.loc[idx - 1, 'high']),threshold) # FVG低点为左1K线的最低点
171
+ }
172
+
173
+ for idx in valid_indices
174
+ if df.loc[idx - 3, 'low'] >= threshold and
175
+ (not check_balanced or all((df.loc[idx:, 'high'] < df.loc[idx - 3, 'low']))) # check_balanced = true 检查FVG是否被平衡过
176
+ ]
177
+
178
+
179
+ return fvg_boxes
180
+
181
+
182
+ def detect_struct(self, data, prd=10, struct_key=None, check_bounds=True, global_extremum=False, s1=True, resp=7) -> dict:
183
+ """_summary_
184
+ 识别SMC结构,参考 Tradingview Smart Money Concepts Probability (Expo)@Openfund
185
+
186
+ Args:
187
+ data (df): df格式的K线数据
188
+ prd (int): 计算Swing Points的bar数量
189
+ struct_key (str): 结构类型,如 'CHoCH'|'SMS'|'BMS'
190
+ check_bounds (bool): 计算Swing Points是否检查边界,默认为True
191
+ global_extremum (bool): 是否使用全局极值点,默认为False
192
+ s1 (bool): 结构响应布尔值
193
+ resp (int): 响应周期
194
+ Returns:
195
+ dict: 包含结构识别结果的字典,包含以下字段:
196
+ "struct": 结构类型,如 'Bullish_CHoCH'|'Bullish_SMS'|'Bullish_BMS'|'Bearish_CHoCH'|'Bearish_SMS'|'Bearish_BMS'
197
+ "index": 结构出现的位置索引
198
+ "pivot_high": 枢轴高点价格
199
+ "pivot_high_index": 枢轴高点索引
200
+ "pivot_low": 枢轴低点价格
201
+ "pivot_low_index": 枢轴低点索引
202
+ "side": 交易方向,'buy'或'sell'
203
+
204
+
205
+ """
206
+
207
+ # data = data.copy()
208
+ data['Up'] = None
209
+ data['Dn'] = None
210
+ data['iUp'] = None
211
+ data['iDn'] = None
212
+ data['pos'] = 0
213
+ data['pattern'] = None
214
+
215
+ # 初始化 Up 和 Dn 的第一个值
216
+ data.at[0, 'Up'] = data.at[0, 'high']
217
+ data.at[0, 'Dn'] = data.at[0, 'low']
218
+
219
+
220
+ for index in range(1, len(data)):
221
+
222
+ data.at[index, 'Up'] = max(data.at[index - 1, 'Up'], data.at[index, 'high'])
223
+ data.at[index, 'Dn'] = min(data.at[index - 1, 'Dn'], data.at[index, 'low'])
224
+ data.at[index, 'pos'] = data.at[index - 1, 'pos']
225
+ data.at[index, 'iUp'] = data.at[max(0,index - 1), 'iUp'] if data.at[max(0,index - 1), 'iUp'] is not None else index
226
+ data.at[index, 'iDn'] = data.at[max(0,index - 1), 'iDn'] if data.at[max(0,index - 1), 'iDn'] is not None else index
227
+
228
+ # 寻找枢轴高点和低点
229
+ pvtHi = self.is_pivot_high(data, index, prd, check_bounds)
230
+ pvtLo = self.is_pivot_low(data, index, prd, check_bounds)
231
+
232
+ if pvtHi:
233
+ data.at[index, 'Up'] = data.at[index, 'high']
234
+ data.at[index, 'iUp'] = index
235
+ if pvtLo:
236
+ data.at[index, 'Dn'] = data.at[index, 'low']
237
+ data.at[index, 'iDn'] = index
238
+ # 寻找Bullish结构
239
+ if data.at[index, 'Up'] > data.at[index - 1, 'Up']:
240
+
241
+ data.at[index, 'iUp'] = index
242
+ if data.at[index - 1, 'pos'] <= 0:
243
+ # data.at[index, 'pattern'] = 'CHoCH (Bullish)'
244
+ data.at[index, 'pattern'] = 'Bullish_CHoCH'
245
+ data.at[index, 'pos'] = 1
246
+ elif data.at[index - 1, 'pos'] == 1 \
247
+ and data.at[index - 1, 'Up'] == data.at[max(0,index - (resp if s1 else prd)), 'Up']:
248
+ data.at[index, 'pattern'] = 'Bullish_SMS'
249
+ data.at[index, 'pos'] = 2
250
+
251
+ elif data.at[index - 1, 'pos'] > 1 \
252
+ and data.at[index - 1, 'Up'] == data.at[max(0,index - (resp if s1 else prd)), 'Up']:
253
+ data.at[index, 'pattern'] = 'Bullish_BMS'
254
+ data.at[index, 'pos'] = data.at[index - 1, 'pos'] + 1
255
+
256
+ elif global_extremum and data.at[index, 'Up'] < data.at[index - 1, 'Up']:
257
+ data.at[index, 'iUp'] = data.at[index - 1, 'iUp']
258
+
259
+ # # 寻找Bearish结构
260
+ if data.at[index, 'Dn'] < data.at[index - 1, 'Dn']:
261
+ data.at[index, 'iDn'] = index
262
+ if data.at[index - 1, 'pos'] >= 0:
263
+
264
+ data.at[index, 'pattern'] = 'Bearish_CHoCH'
265
+ data.at[index, 'pos'] = -1
266
+ elif data.at[index - 1, 'pos'] == -1 \
267
+ and data.at[index - 1, 'Dn'] == data.at[max(0,index - (resp if s1 else prd)), 'Dn']:
268
+ data.at[index, 'pattern'] = 'Bearish_SMS'
269
+ data.at[index, 'pos'] = -2
270
+ elif data.at[index - 1, 'pos'] < -1 \
271
+ and data.at[index - 1, 'Dn'] == data.at[max(0,index - (resp if s1 else prd)), 'Dn']:
272
+ data.at[index, 'pattern'] = 'Bearish_BMS'
273
+ data.at[index, 'pos'] = data.at[index - 1, 'pos'] - 1
274
+
275
+ elif global_extremum and data.at[index, 'Dn'] > data.at[index - 1, 'Dn']:
276
+ data.at[index, 'iDn'] = data.at[index - 1, 'iDn']
277
+
278
+ # 获取最后一个结构和位置
279
+ last_struct = {
280
+ "struct": None,
281
+ "index": -1,
282
+ "pivot_high": None,
283
+ "pivot_high_index": -1,
284
+ "pivot_low": None,
285
+ "pivot_low_index": -1,
286
+ "side": None
287
+
288
+ }
289
+
290
+
291
+ for i in range(len(data)-1, -1, -1):
292
+ if data.at[i, 'pattern'] is not None:
293
+ if struct_key is not None and struct_key not in data.at[i, 'pattern']:
294
+ continue
295
+ last_struct["struct"] = data.at[i, 'pattern']
296
+ last_struct["index"] = i
297
+
298
+ break
299
+
300
+ if last_struct['struct'] is not None :
301
+ # 找到最后一个结构的枢轴高点和低点,如果当前是孤立点,则取前一个孤立点
302
+ # 判断交易方向
303
+ if 'Bearish' in last_struct["struct"]:
304
+ last_struct["side"] = 'sell'
305
+ else :
306
+ last_struct["side"] = 'buy'
307
+
308
+ last_struct["pivot_high_index"] = int(data["iUp"].iloc[-1])
309
+ last_struct["pivot_low_index"] = int(data["iDn"].iloc[-1])
310
+
311
+ last_struct["pivot_high"] = float(data.loc[last_struct["pivot_high_index"], 'high'])
312
+ last_struct["pivot_low"] = float(data.loc[last_struct["pivot_low_index"], 'low'])
313
+
314
+
315
+
316
+ # last_struct["pivot_high_index"] = int(data.loc[data.index > last_struct["index"], 'high'].idxmax())
317
+ # last_struct["pivot_high"] = float(data.loc[data.index > last_struct["index"], 'high'].max())
318
+ # last_struct["pivot_low_index"] = int(data.loc[data.index > last_struct["index"], 'low'].idxmin())
319
+ # last_struct["pivot_low"] = float(data.loc[data.index > last_struct["index"], 'low'].min())
320
+
321
+ return last_struct
322
+
323
+
324
+
325
+
326
+ def is_pivot_high(self, data, index, period, check_bounds=False):
327
+ """
328
+ 判断当前索引处是否为枢轴高点
329
+ :param data: 包含 'high' 列的 DataFrame
330
+ :param index: 当前索引
331
+ :param period: 前后比较的周期数
332
+ :return: 是否为枢轴高点
333
+ """
334
+ if check_bounds and (index < period or index >= len(data) - period):
335
+ return False
336
+ current_high = data.at[index, 'high']
337
+ prev_highs = data['high'].iloc[max(0,index - period):index]
338
+ next_highs = data['high'].iloc[index :min(len(data),index + period + 1)]
339
+ return all(current_high >= prev_highs) and all(current_high >= next_highs)
340
+
341
+
342
+ def is_pivot_low(self, data, index, period, check_bounds=False):
343
+ """
344
+ 判断当前索引处是否为枢轴低点
345
+ :param data: 包含 'low' 列的 DataFrame
346
+ :param index: 当前索引
347
+ :param period: 前后比较的周期数
348
+ :return: 是否为枢轴低点
349
+ """
350
+ if check_bounds and (index < period or index >= len(data) - period):
351
+ return False
352
+ current_low = data.at[index, 'low']
353
+ prev_lows = data['low'].iloc[max(0,index - period):index]
354
+ next_lows = data['low'].iloc[index :min(len(data),index + period + 1)]
355
+ return all(current_low <= prev_lows) and all(current_low <= next_lows)
356
+
357
+ def round_price(self,symbol, price):
358
+ tick_size = self.get_tick_size(symbol)
359
+ return super().round_price_to_tick(price, tick_size)
360
+
361
+ def calculate_ce(self,symbol,pivot_high , pivot_low) -> float:
362
+ ce = (pivot_high + pivot_low) / 2
363
+ return float(self.round_price(symbol, ce))
364
+
365
+
366
+ def process_pair(self,symbol,pair_config):
367
+ self.logger.info("=" * 60)
368
+ """_summary_
369
+ 1. HTF 判断struct趋势(SMS和BMS)
370
+ 2. HTF 获取最新的两个极值点,设置折价区和溢价区
371
+ 3. CTF 在折价区获取FVG和OB的位置
372
+ 4. CTF 下单
373
+ 5.
374
+ """
375
+ try:
376
+ # 检查是否有持仓,有持仓不进行下单
377
+ if self.check_position(symbol=symbol) :
378
+ self.logger.info(f"{symbol} : 有持仓合约,不进行下单。")
379
+ if symbol in self.place_order_prices:
380
+ self.place_order_prices[symbol] = {}
381
+ return
382
+
383
+
384
+ smc_strategy = pair_config.get('smc_strategy',{})
385
+
386
+ # 获取历史K线,HTF和CTF
387
+ htf = str(smc_strategy.get('HTF','15m'))
388
+ htf_Klines = self.get_historical_klines(symbol=symbol, bar=htf)
389
+ htf_df = self.format_klines(htf_Klines)
390
+
391
+ ctf = str(pair_config.get('CHF', '5m'))
392
+ ctf_kLines = self.get_historical_klines(symbol=symbol, bar=ctf)
393
+ ctf_df = self.format_klines(ctf_kLines)
394
+
395
+ enable_FVG = smc_strategy.get('enable_FVG',True) # 是否启用FVG
396
+ enable_OB = smc_strategy.get('enable_OB',True) # 是否启用OB
397
+ self.logger.debug(f"{symbol} : SMC策略 {ctf}|{htf} enable_FVG={enable_FVG} enable_OB={enable_OB} ...")
398
+
399
+ side = 'none'
400
+ # 1. HTF 判断struct趋势(CHoCH\SMS\BMS) ,HTF struct 看趋势,CTF 看FVG和OB的位置
401
+ swing_points_length = smc_strategy.get('swing_points_length',10)
402
+ htf_last_struct = self.detect_struct(htf_df,prd=swing_points_length)
403
+ htf_last_struct_label = htf_last_struct["struct"]
404
+
405
+
406
+ if htf_last_struct_label is None:
407
+ self.logger.debug(f"{symbol} : {htf} 未形成 struct,不下单。{htf_last_struct}。")
408
+ return
409
+
410
+ # ctf_last_struct = self.detect_struct(ctf_df)
411
+ # ctf_last_struct_label = ctf_last_struct["struct"]
412
+
413
+ # if ctf_last_struct_label is None:
414
+ # self.logger.debug(f"{symbol} :{ctf} 未形成 struct,不下单。{ctf_last_struct}。")
415
+ # return
416
+
417
+ side = htf_last_struct["side"]
418
+ # self.logger.debug(f"{symbol} : {htf} 趋势={htf_last_struct_label}-{side}: \n{htf_last_struct}")
419
+
420
+
421
+ # 2. HTF 获取最新的两个极值点,设置折价(discount)区和溢价(premium)区
422
+ pivot_high = htf_last_struct["pivot_high"]
423
+ pivot_low = htf_last_struct["pivot_low"]
424
+ mid_line = self.calculate_ce(symbol,pivot_high,pivot_low)
425
+
426
+ # 计算溢价和折价区
427
+ premium_box = {
428
+ 'top': pivot_high,
429
+ 'bot': mid_line,
430
+ 'ce': self.calculate_ce(symbol,pivot_high,mid_line)
431
+ }
432
+ discount_box = {
433
+ 'top': mid_line,
434
+ 'bot': pivot_low,
435
+ 'ce': self.calculate_ce(symbol,mid_line,pivot_low)
436
+ }
437
+
438
+ self.logger.debug(f"{symbol} : {htf} 趋势={htf_last_struct_label}: \npivot_high={pivot_high} pivot_low={pivot_low} mid_line={mid_line}\n溢价区={premium_box}\n折价区={discount_box}")
439
+
440
+ # 3. 根据HTF结构来分析下单位置和止盈位置
441
+ threshold = 0.0
442
+ order_side = side
443
+ # 获取当前市场价格
444
+ market_price = ctf_df['close'].iloc[-1]
445
+
446
+ if 'CHoCH' in htf_last_struct_label:
447
+ """
448
+ ChoCh 结构。
449
+ Bearish趋势 如果价格,
450
+ 1.在溢价区上半区,可以考虑顺当前趋势,做空。
451
+ 2.在折价区下半区,则考虑回收流动性,做多。
452
+ 3.溢价区下半区和折价区上半区,不做单。
453
+
454
+ Bullish趋势 如果价格,
455
+ 1.在折价区下半区,可以考虑顺当前趋势,做多。
456
+ 2.在溢价区上半区,则考虑回收流动性的,做空。
457
+ 3.溢价区下半区和折价区上半区,不做单。
458
+
459
+ """
460
+ # 溢价区上半区做空
461
+ if market_price >= premium_box['ce'] and side == 'sell':
462
+ threshold = premium_box['ce']
463
+ # 折价区下半区做多
464
+ elif market_price <= discount_box['ce'] and side == 'buy':
465
+ threshold = discount_box['ce']
466
+ # 折价区下半区回收流动性做空 # TODO 要考虑是否有孤立点
467
+ # elif market_price <= discount_box['ce'] and side == 'sell':
468
+ # threshold = discount_box['ce']
469
+ # order_side = 'buy'
470
+ # # 溢价区上半区回收流动性做多
471
+ # elif market_price >= premium_box['ce'] and side == 'buy':
472
+ # threshold = premium_box['ce']
473
+ # order_side = 'sell'
474
+
475
+
476
+ elif 'SMS' in htf_last_struct_label or 'BMS' in htf_last_struct_label:
477
+ """
478
+ SMS/BMS 结构。
479
+ Bullish趋势 如果价格,
480
+ 1.在折价区可以下单,不区分上下半区
481
+
482
+ Bearish趋势 如果价格,
483
+ 1.在溢价区可以下单,不区分上下半区
484
+
485
+ """
486
+ # Bearish趋势 如果价格在溢价区可以下单
487
+ # if market_price >= mid_line and side == 'sell':
488
+ # threshold = mid_line
489
+ # # Bullish趋势 如果价格在折价区可以下单
490
+ # elif market_price <= mid_line and side == 'buy':
491
+ # threshold = mid_line
492
+ threshold = mid_line
493
+
494
+
495
+
496
+ if threshold == 0.0:
497
+ self.logger.debug(f"{symbol} : 价格{market_price}不在目标区域,不下单。")
498
+ # 取消所有未成交订单
499
+ self.cancel_all_orders(symbol=symbol)
500
+ return
501
+
502
+
503
+ # 4. 在CTF折价区获取FVG的位置
504
+ order_price = 0.0
505
+
506
+ if enable_FVG and order_price == 0.0:
507
+
508
+ all_tf = ['1m', '3m', '5m', '15m', '30m', '1H', '2H', '4H']
509
+ # 获取当前时间周期之前的所有时间周期
510
+ ctf_index = all_tf.index(ctf)
511
+ ltf_tfs = all_tf[:ctf_index + 1]
512
+
513
+ # 遍历所有LTF时间周期,获取FVG
514
+ for tf in ltf_tfs[::-1]:
515
+ tf_Klines = self.get_historical_klines(symbol=symbol, bar=tf)
516
+ tf_df = self.format_klines(tf_Klines)
517
+
518
+ fvg_boxes = self.find_fvg_boxes(tf_df,side=order_side,threshold=threshold)
519
+ if len(fvg_boxes) > 0:
520
+ self.logger.debug(f"{symbol} : 方向={order_side}, {tf} FVG={fvg_boxes}")
521
+ break
522
+ else:
523
+ self.logger.debug(f"{symbol} : 方向={order_side}, {tf} 未找到 FVG")
524
+
525
+
526
+
527
+ if len(fvg_boxes) != 0 and order_price == 0.0:
528
+ last_fvg_box = fvg_boxes[-1]
529
+ ce_price = self.calculate_ce(symbol,last_fvg_box['top'],last_fvg_box['bot'])
530
+ self.logger.info(f"{symbol} : 方向={order_side}, FVG_ce={ce_price} FVG={last_fvg_box} ")
531
+ order_price = ce_price
532
+
533
+ # 4. 找OB位置,OB规则孤立高点+实体低点 孤立低点+实体高点
534
+
535
+ if enable_OB and order_price == 0.0: # OB 优先级低于 FVG, order_price有价格时,不再计算OB
536
+
537
+ ctf_last_struct = self.detect_struct(ctf_df,prd=swing_points_length)
538
+ # 找到最近的一个极值点的位置
539
+ if order_side == 'buy':
540
+ pivot_index = ctf_last_struct["pivot_low_index"]
541
+ else:
542
+ pivot_index = ctf_last_struct["pivot_high_index"]
543
+ # TODO 不同级别的pivot_index 需要优化计算
544
+ OB_boxes = self.find_OB_boxes(ctf_df,side=side,threshold=threshold,pivot_index=pivot_index)
545
+
546
+ if len(OB_boxes) != 0 :
547
+ last_OB_box = OB_boxes[-1]
548
+ ce_price = self.calculate_ce(symbol,last_OB_box['top'],last_OB_box['bot'])
549
+ self.logger.info(f"{symbol} : 方向={order_side}, OB_ce={ce_price} , OB={last_OB_box} ")
550
+ order_price = ce_price
551
+
552
+ if order_price == 0.0:
553
+ self.logger.warning(f"!!!{symbol} : 未找到 FVG和OB")
554
+ self.cancel_all_orders(symbol=symbol)
555
+ return
556
+
557
+ latest_order_price = self.place_order_prices.get(symbol,0.0)
558
+ if order_price == latest_order_price:
559
+ self.logger.debug(f"{symbol} : 下单价格 {order_price} 未变化,不进行下单。")
560
+ return
561
+
562
+
563
+ # 下单
564
+ self.cancel_all_orders(symbol=symbol)
565
+ self.place_order(symbol=symbol, price=order_price, side=order_side, pair_config=pair_config)
566
+ self.place_order_prices[symbol] = order_price # 记录下单价格,过滤重复下单
567
+ self.logger.debug(f"{symbol} : {side}, 下单价格 {order_price}")
568
+
569
+
570
+ except KeyboardInterrupt:
571
+ self.logger.info("程序收到中断信号,开始退出...")
572
+ except Exception as e:
573
+ error_message = f"程序异常退出: {str(e)}"
574
+ self.logger.error(error_message,exc_info=True)
575
+ traceback.print_exc()
576
+ self.send_feishu_notification(error_message)
577
+ finally:
578
+ self.logger.info("-" * 60)
579
+
580
+
@@ -42,7 +42,9 @@ class ThreeLineStrategyMaker:
42
42
  return self.exchange.market(symbol)
43
43
 
44
44
  def get_tick_size(self,symbol):
45
- return float(self.getMarket(symbol)['precision']['price'])
45
+ market = self.getMarket(symbol)
46
+ marketPrecision = self.exchange.safe_dict(market, 'precision')
47
+ return self.exchange.safe_float(marketPrecision, 'price')
46
48
 
47
49
  def convert_contract(self, symbol, amount, price:float, direction='cost_to_contract'):
48
50
  """
@@ -123,7 +125,7 @@ class ThreeLineStrategyMaker:
123
125
  else:
124
126
  raise ValueError("Unexpected response structure or missing 'last' key")
125
127
 
126
- def round_price_to_tick(self,price, tick_size):
128
+ def round_price_to_tick(self, price, tick_size):
127
129
  # 计算 tick_size 的小数位数
128
130
  tick_decimals = len(f"{tick_size:.10f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.10f}" else 0
129
131
 
@@ -424,14 +426,15 @@ class ThreeLineStrategyMaker:
424
426
  def place_order(self,symbol, price, amount_usdt, side,order_type='limit'):
425
427
 
426
428
 
427
- markets = self.exchange.load_markets()
428
- if symbol not in markets:
429
- self.logger.error(f"{symbol}: Instrument {symbol} not found in markets")
430
- return
431
- market = markets[symbol]
432
- # 获取价格精度
433
- price_precision = market['precision']['price']
434
- adjusted_price = self.round_price_to_tick(price, price_precision)
429
+ # markets = self.exchange.load_markets()
430
+ # if symbol not in markets:
431
+ # self.logger.error(f"{symbol}: Instrument {symbol} not found in markets")
432
+ # return
433
+ # market = markets[symbol]
434
+ # # 获取价格精度
435
+ # price_precision = market['precision']['price']
436
+ tick_size = self.get_tick_size(symbol)
437
+ adjusted_price = self.round_price_to_tick(price, tick_size)
435
438
 
436
439
  if amount_usdt > 0:
437
440
  if side == 'buy':
@@ -501,15 +504,15 @@ class ThreeLineStrategyMaker:
501
504
 
502
505
  self.logger.info(f"{symbol}: {order_ids} 挂单取消成功.")
503
506
  else:
504
- self.logger.info(f"{symbol}: 没有未完成订单.")
507
+ self.logger.info(f"{symbol}: 无挂单.")
505
508
  return True
506
509
 
507
510
  except Exception as e:
508
511
  retry_count += 1
509
512
  if retry_count == max_retries:
510
- self.logger.warning(f"{symbol} 取消订单失败(重试{retry_count}次): {str(e)}")
513
+ self.logger.warning(f"{symbol} 取消挂单失败(重试{retry_count}次): {str(e)}")
511
514
  return False
512
- self.logger.warning(f"{symbol} 取消订单失败,正在进行第{retry_count}次重试: {str(e)}")
515
+ self.logger.warning(f"{symbol} 取消挂单失败,正在进行第{retry_count}次重试: {str(e)}")
513
516
  time.sleep(0.1) # 重试前等待0.1秒
514
517
 
515
518
  def process_pair(self,symbol,pair_config):
@@ -666,13 +669,13 @@ class ThreeLineStrategyMaker:
666
669
  def monitor_klines(self):
667
670
  symbols = list(self.trading_pairs_config.keys()) # 获取所有币对的ID
668
671
  batch_size = 5 # 每批处理的数量
669
- while True:
670
-
671
- for i in range(0, len(symbols), batch_size):
672
- batch = symbols[i:i + batch_size]
673
- with ThreadPoolExecutor(max_workers=batch_size) as executor:
674
- futures = [executor.submit(self.process_pair, symbol,self.trading_pairs_config[symbol]) for symbol in batch]
675
- for future in as_completed(futures):
676
- future.result() # Raise any exceptions caught during execution
672
+ # while True:
673
+
674
+ for i in range(0, len(symbols), batch_size):
675
+ batch = symbols[i:i + batch_size]
676
+ with ThreadPoolExecutor(max_workers=batch_size) as executor:
677
+ futures = [executor.submit(self.process_pair, symbol,self.trading_pairs_config[symbol]) for symbol in batch]
678
+ for future in as_completed(futures):
679
+ future.result() # Raise any exceptions caught during execution
677
680
 
678
681
  # time.sleep(self.monitor_interval)
@@ -8,6 +8,8 @@ from datetime import datetime
8
8
  from maker.WickReversalStrategyMaker import WickReversalStrategyMaker
9
9
  from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
10
10
  from maker.MACDStrategyMaker import MACDStrategyMaker
11
+ from maker.SMCStrategyMaker import SMCStrategyMaker
12
+ from maker.BestFVGStrategyMaker import BestFVGStrategyMaker
11
13
 
12
14
  def build_logger(log_config) -> logging.Logger:
13
15
  # 配置日志
@@ -58,9 +60,16 @@ def main():
58
60
  feishu_webhook_url = config_data['feishu_webhook']
59
61
  logger = build_logger(config_data["Logger"])
60
62
  package_name = __package__ or "maker"
61
- logger.info(f" ++ {package_name}:{version} is doing...")
62
- logger.info("开始执行交易任务...")
63
- bot = MACDStrategyMaker(config_data, platform_config, feishu_webhook=feishu_webhook_url, logger=logger)
63
+
64
+
65
+ maker = config_data.get('actived_maker', 'MACDStrategyMaker')
66
+
67
+
68
+ # 根据配置动态创建策略实例
69
+ strategy_class = globals()[maker]
70
+ bot = strategy_class(config_data, platform_config, feishu_webhook=feishu_webhook_url, logger=logger)
71
+
72
+ logger.info(f" ++ {package_name}.{maker}:{version} is doing...")
64
73
 
65
74
  # 获取计划配置
66
75
  schedule_config = config_data.get('schedule', {})
@@ -92,7 +101,7 @@ def main():
92
101
  )
93
102
 
94
103
  try:
95
- logger.info(f"启动定时任务调度器,从 {next_run} 开始每5分钟执行一次...")
104
+ logger.info(f"启动定时任务调度器,从 {next_run} 开始每{monitor_interval}分钟执行一次...")
96
105
  scheduler.start()
97
106
  except (KeyboardInterrupt, SystemExit):
98
107
  logger.info("程序收到中断信号,正在退出...")