openfund-core 0.0.3__py3-none-any.whl → 1.0.1__py3-none-any.whl

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.
Files changed (43) hide show
  1. core/Exchange.py +276 -0
  2. core/main.py +23 -0
  3. core/smc/SMCBase.py +130 -0
  4. core/smc/SMCFVG.py +86 -0
  5. core/smc/SMCLiquidity.py +7 -0
  6. core/smc/SMCOrderBlock.py +288 -0
  7. core/smc/SMCPDArray.py +77 -0
  8. core/smc/SMCStruct.py +290 -0
  9. core/smc/__init__.py +0 -0
  10. core/utils/OPTools.py +30 -0
  11. openfund_core-1.0.1.dist-info/METADATA +48 -0
  12. openfund_core-1.0.1.dist-info/RECORD +15 -0
  13. {openfund_core-0.0.3.dist-info → openfund_core-1.0.1.dist-info}/WHEEL +1 -1
  14. openfund_core-1.0.1.dist-info/entry_points.txt +3 -0
  15. openfund/core/__init__.py +0 -14
  16. openfund/core/api_tools/__init__.py +0 -16
  17. openfund/core/api_tools/binance_futures_tools.py +0 -23
  18. openfund/core/api_tools/binance_tools.py +0 -26
  19. openfund/core/api_tools/enums.py +0 -539
  20. openfund/core/base_collector.py +0 -72
  21. openfund/core/base_tool.py +0 -58
  22. openfund/core/factory.py +0 -97
  23. openfund/core/openfund_old/continuous_klines.py +0 -153
  24. openfund/core/openfund_old/depth.py +0 -92
  25. openfund/core/openfund_old/historical_trades.py +0 -123
  26. openfund/core/openfund_old/index_info.py +0 -67
  27. openfund/core/openfund_old/index_price_kline.py +0 -118
  28. openfund/core/openfund_old/klines.py +0 -95
  29. openfund/core/openfund_old/klines_qrr.py +0 -103
  30. openfund/core/openfund_old/mark_price.py +0 -121
  31. openfund/core/openfund_old/mark_price_klines.py +0 -122
  32. openfund/core/openfund_old/ticker_24hr_price_change.py +0 -99
  33. openfund/core/pyopenfund.py +0 -85
  34. openfund/core/services/um_futures_collector.py +0 -142
  35. openfund/core/sycu_exam/__init__.py +0 -1
  36. openfund/core/sycu_exam/exam.py +0 -19
  37. openfund/core/sycu_exam/random_grade_cplus.py +0 -440
  38. openfund/core/sycu_exam/random_grade_web.py +0 -404
  39. openfund/core/utils/time_tools.py +0 -25
  40. openfund_core-0.0.3.dist-info/LICENSE +0 -201
  41. openfund_core-0.0.3.dist-info/METADATA +0 -67
  42. openfund_core-0.0.3.dist-info/RECORD +0 -30
  43. {openfund/core/openfund_old → core}/__init__.py +0 -0
@@ -0,0 +1,288 @@
1
+ import logging
2
+ import pandas as pd
3
+
4
+ from core.smc.SMCStruct import SMCStruct
5
+
6
+
7
+ class SMCOrderBlock(SMCStruct):
8
+ OB_HIGH_COL = "ob_high"
9
+ OB_LOW_COL = "ob_low"
10
+ OB_MID_COL = "ob_mid"
11
+ OB_VOLUME_COL = "ob_volume"
12
+ OB_DIRECTION_COL = "ob_direction" # 1: 向上突破, 2: 向下突破
13
+ # OB_START_INDEX_COL = "ob_start_index"
14
+ # OB_START_TS_COL = "ob_start_ts"
15
+ OB_ATR = "ob_atr"
16
+ OB_IS_COMBINED = "ob_is_combined"
17
+ OB_WAS_CROSSED = "ob_was_crossed"
18
+
19
+ def __init__(self):
20
+ super().__init__()
21
+ self.logger = logging.getLogger(__name__)
22
+
23
+ def find_OBs(
24
+ self,
25
+ struct: pd.DataFrame,
26
+ side=None,
27
+ start_index: int = -1,
28
+ is_valid: bool = True,
29
+ if_combine: bool = True,
30
+ ) -> pd.DataFrame:
31
+ """_summary_
32
+
33
+ Args:
34
+ symbol (_type_): _description_
35
+ data (pd.DataFrame): _description_
36
+ side (_type_): _description_ 如果是None, 则返回所有OB boxes(包括bullish和bearish)
37
+ pivot_index (int): _description_ 开始的位置
38
+ is_valid (bool): _description_ 找到有效的OB,没有被crossed
39
+ if_combine (bool): _description_ 是否合并OB
40
+ Returns:
41
+ list: _description_
42
+ """
43
+
44
+ df = struct.copy() if start_index == -1 else struct.copy().iloc[start_index:]
45
+ if self.OB_DIRECTION_COL not in df.columns:
46
+ df = self.build_struct_for_ob(df)
47
+
48
+ # 获取有效的OB数据
49
+ ob_df = df[df[self.OB_DIRECTION_COL].notna()]
50
+
51
+ # 根据side过滤并生成OB
52
+ if side is not None:
53
+ direction = "Bullish" if side == self.BUY_SIDE else "Bearish"
54
+ ob_df = ob_df[ob_df[self.OB_DIRECTION_COL] == direction]
55
+
56
+ # 检查OB是否被平衡过
57
+
58
+ ob_df.loc[:, self.OB_WAS_CROSSED] = ob_df.apply(
59
+ lambda row: any(
60
+ df.loc[row.name + 1 :, self.LOW_COL] <= row[self.OB_LOW_COL]
61
+ )
62
+ if row[self.OB_DIRECTION_COL] == "Bullish"
63
+ else any(df.loc[row.name + 1 :, self.HIGH_COL] >= row[self.OB_HIGH_COL]),
64
+ axis=1,
65
+ )
66
+
67
+ ob_df = ob_df[~ob_df[self.OB_WAS_CROSSED]]
68
+
69
+ # # 过滤出有效的OB
70
+ # if is_valid is not None and not ob_df.empty:
71
+ # valid_mask = []
72
+ # for _, row in ob_df.iterrows():
73
+ # start_idx = row[self.OB_START_INDEX_COL] + 1
74
+ # if start_idx >= len(df):
75
+ # valid = False
76
+ # else:
77
+ # future_data = df.loc[start_idx:]
78
+ # if row[self.OB_DIRECTION_COL] == "Bullish":
79
+ # valid = row[self.OB_LOW_COL] < future_data[self.LOW_COL].min()
80
+ # else: # Bearish
81
+ # valid = row[self.OB_HIGH_COL] > future_data[self.HIGH_COL].max()
82
+
83
+ # valid_mask.append(valid if is_valid else not valid)
84
+
85
+ # ob_df = ob_df[pd.Series(valid_mask, index=ob_df.index)]
86
+
87
+ if if_combine:
88
+ # 合并OB
89
+ ob_df = self._combineOB(ob_df)
90
+
91
+ return ob_df
92
+
93
+ def build_struct_for_ob(
94
+ self, df, window=20, is_struct_body_break=True, atr_multiplier=0.6
95
+ ):
96
+ """
97
+ 构建结构并检测Order Block
98
+
99
+ Args:
100
+ df: 数据框
101
+ window: 寻找结构极值的窗口大小
102
+ is_struct_body_break: 是否使用收盘价判断突破
103
+ ob_length: 搜索Order Block的回溯长度
104
+ atr_multiplier: ATR倍数阈值
105
+
106
+ Returns:
107
+ 处理后的数据框,包含结构和Order Block相关列
108
+ """
109
+ # 首先构建基础结构
110
+ df = self.build_struct(df, window, is_struct_body_break)
111
+
112
+ check_columns = [self.HIGH_COL, self.LOW_COL, self.CLOSE_COL]
113
+ self.check_columns(df, check_columns)
114
+
115
+ # 初始化OB相关列
116
+ ob_columns = [
117
+ self.OB_HIGH_COL,
118
+ self.OB_LOW_COL,
119
+ self.OB_MID_COL,
120
+ self.OB_VOLUME_COL,
121
+ self.OB_DIRECTION_COL,
122
+ # self.OB_START_INDEX_COL,
123
+ # self.OB_START_TS_COL,
124
+ self.OB_ATR,
125
+ ]
126
+ for col in ob_columns:
127
+ df[col] = None
128
+
129
+ # 计算ATR用于阈值判断
130
+ df[self.ATR_COL] = self._calculate_atr(df)
131
+
132
+ # 检测Order Block
133
+ for i in range(1, len(df)):
134
+ # 检查是否为结构高点突破
135
+ if df.at[i, self.STRUCT_COL] and "Bullish" in df.at[i, self.STRUCT_COL]:
136
+ self._find_ob(df, i, atr_multiplier)
137
+
138
+ # 检查是否为结构低点突破
139
+ elif df.at[i, self.STRUCT_COL] and "Bearish" in df.at[i, self.STRUCT_COL]:
140
+ self._find_ob(df, i, atr_multiplier, is_bullish=False)
141
+
142
+ return df
143
+
144
+ def _combineOB(self, df_OBs, combine_atr_muiltiplier=0.2):
145
+ """
146
+ 合并OB
147
+ """
148
+
149
+ df_ob = df_OBs.copy()
150
+ # 初始化 OB_IS_COMBINED 列为 0
151
+ df_ob[self.OB_IS_COMBINED] = 0
152
+
153
+ combine_atr_muiltiplier = self.toDecimal(combine_atr_muiltiplier)
154
+ # 遍历所有OB,检查是否需要合并
155
+ for i in range(len(df_ob)):
156
+ # 如果当前OB已被合并,跳过
157
+ if df_ob.iloc[i][self.OB_IS_COMBINED] == 1:
158
+ continue
159
+
160
+ current_direction = df_ob.iloc[i][self.OB_DIRECTION_COL]
161
+ current_mid = df_ob.iloc[i][self.OB_MID_COL]
162
+ current_atr = df_ob.iloc[i][self.OB_ATR]
163
+
164
+ # 检查后续的OB
165
+ for j in range(i + 1, len(df_ob)):
166
+ # 如果后续OB已被合并,跳过
167
+ if df_ob.iloc[j][self.OB_IS_COMBINED] == 1:
168
+ continue
169
+
170
+ # 如果方向相同且中间价差值小于阈值,标记为已合并
171
+ if (
172
+ df_ob.iloc[j][self.OB_DIRECTION_COL] == current_direction
173
+ and abs(df_ob.iloc[j][self.OB_MID_COL] - current_mid)
174
+ < current_atr * combine_atr_muiltiplier
175
+ ):
176
+ df_ob.iloc[i, df_ob.columns.get_loc(self.OB_IS_COMBINED)] = 1
177
+ break
178
+ # 遍历所有的OB
179
+
180
+ return df_ob
181
+
182
+ def _calculate_atr(self, df, period=200, multiplier=1):
183
+ return super().calculate_atr(df, period, multiplier)
184
+
185
+ def _find_ob(self, df, i, atr_multiplier, is_bullish=True):
186
+ """寻找Order Block
187
+ Args:
188
+ df: 数据框
189
+ i: 当前索引
190
+ ob_length: OB长度
191
+ atr_multiplier: ATR乘数
192
+ is_bullish: 是否为看涨OB,True为看涨,False为看跌
193
+ """
194
+ # 根据方向获取相应的结构索引和价格,OB是取结构开始的最高价或最低价的K线
195
+ if is_bullish:
196
+ index = df.loc[i, self.STRUCT_LOW_INDEX_COL]
197
+ extreme_price = df.loc[i, self.STRUCT_LOW_COL]
198
+
199
+ # Oper_func = min
200
+ src = self.toDecimal(df.loc[index, self.HIGH_COL])
201
+ direction = "Bullish"
202
+
203
+ else:
204
+ index = df.loc[i, self.STRUCT_HIGH_INDEX_COL]
205
+ extreme_price = df.loc[i, self.STRUCT_HIGH_COL]
206
+ # Oper_func = max
207
+ src = self.toDecimal(df.loc[index, self.LOW_COL])
208
+ direction = "Bearish"
209
+
210
+ # 计算累积成交量
211
+ vol = df.loc[index:i, self.VOLUME_COL].sum()
212
+
213
+ # 应用ATR阈值,如果OB的范围小于ATR(=20)的60%(20*0.6=12),则把OB可扩展到ATR(=20)范围。
214
+ precision = self.get_precision_length(extreme_price)
215
+ atr = self.toDecimal(df.loc[i, self.ATR_COL])
216
+ atr_multiplier = self.toDecimal(atr_multiplier)
217
+ if is_bullish:
218
+ # 计算当前区间大小
219
+ current_range = src - extreme_price
220
+ target_range = atr
221
+ if current_range < atr * atr_multiplier:
222
+ # 如果区间过小,将区间扩展到目标大小,并在中心点两侧平均分配
223
+ extend_amount = (target_range - current_range) / 2
224
+ src += extend_amount
225
+ extreme_price -= extend_amount
226
+
227
+ high, low = (
228
+ self.toDecimal(src, precision),
229
+ self.toDecimal(extreme_price, precision),
230
+ )
231
+ else:
232
+ # 计算当前区间大小
233
+ current_range = extreme_price - src
234
+ target_range = atr
235
+ if current_range < atr * atr_multiplier:
236
+ # 如果区间过小,将区间扩展到目标大小,并在中心点两侧平均分配
237
+ extend_amount = (target_range - current_range) / 2
238
+ src -= extend_amount
239
+ extreme_price += extend_amount
240
+
241
+ high, low = (
242
+ self.toDecimal(extreme_price, precision),
243
+ self.toDecimal(src, precision),
244
+ )
245
+
246
+ # 计算中间值
247
+ mid = (high + low) / 2
248
+
249
+ # 更新OB信息到DataFrame
250
+ df.at[index, self.OB_HIGH_COL] = high
251
+ df.at[index, self.OB_LOW_COL] = low
252
+ df.at[index, self.OB_MID_COL] = mid
253
+ df.at[index, self.OB_VOLUME_COL] = vol
254
+ df.at[index, self.OB_DIRECTION_COL] = direction
255
+ # df.at[i, self.OB_START_INDEX_COL] = index
256
+ # df.at[i, self.OB_START_TS_COL] = df.loc[index, self.TIMESTAMP_COL]
257
+ df.at[index, self.OB_ATR] = atr
258
+
259
+ def get_last_ob(self, df, prd=-1):
260
+ """
261
+ 获取最新的Order Block
262
+
263
+ Args:
264
+ df: 包含OB信息的数据框
265
+ prd: 回溯周期,-1表示全部
266
+
267
+ Returns:
268
+ 最新的Order Block信息或None
269
+ """
270
+ # 获取prd范围内的数据
271
+ start_idx = max(0, len(df) - 1 - prd) if prd > 0 else 0
272
+
273
+ # 筛选有效OB且在prd范围内的数据
274
+ mask = df[self.OB_DIRECTION_COL].notna() & (df.index >= start_idx)
275
+ valid_obs = df[mask]
276
+
277
+ if not valid_obs.empty:
278
+ # 获取最近的OB
279
+ last_ob = valid_obs.iloc[-1]
280
+ return {
281
+ self.OB_HIGH_COL: last_ob[self.OB_HIGH_COL],
282
+ self.OB_LOW_COL: last_ob[self.OB_LOW_COL],
283
+ self.OB_MID_COL: last_ob[self.OB_MID_COL],
284
+ self.OB_VOLUME_COL: last_ob[self.OB_VOLUME_COL],
285
+ self.OB_DIRECTION_COL: last_ob[self.OB_DIRECTION_COL],
286
+ }
287
+
288
+ return None
core/smc/SMCPDArray.py ADDED
@@ -0,0 +1,77 @@
1
+ import logging
2
+ import pandas as pd
3
+
4
+ from core.smc.SMCFVG import SMCFVG
5
+ from core.smc.SMCOrderBlock import SMCOrderBlock
6
+
7
+ class SMCPDArray(SMCFVG,SMCOrderBlock):
8
+ PD_HIGH_COL = "pd_high"
9
+ PD_LOW_COL = "pd_low"
10
+ PD_MID_COL = "pd_mid"
11
+ PD_TYPE_COL = "pd_type"
12
+
13
+ def __init__(self):
14
+ super().__init__()
15
+ self.logger = logging.getLogger(__name__)
16
+
17
+ def find_PDArrays(
18
+ self, struct: pd.DataFrame, side, start_index=-1
19
+ ) -> pd.DataFrame:
20
+ """_summary_
21
+ 寻找PDArrays,包括Fair Value Gap (FVG)|Order Block (OB)|Breaker Block(BB)|Mitigation Block(BB)
22
+ Args:
23
+ data (pd.DataFrame): K线数据
24
+ side (_type_): 交易方向 'buy'|'sell'
25
+ threshold (_type_): 阈值价格,通常为溢价和折价区的CE
26
+ check_balanced (bool): 是否检查FVG是否被平衡过,默认为True
27
+ start_index (int): 开始查找索引的起点,默认为-1
28
+
29
+ Returns:
30
+ pd.DataFrame: _description_
31
+
32
+ """
33
+
34
+ df = (
35
+ struct.copy()
36
+ if start_index == -1
37
+ else struct.copy().iloc[max(0, start_index - 1) :]
38
+ )
39
+ # check_columns = [self.HIGH_COL, self.LOW_COL, self.CLOSE_COL]
40
+ # self.check_columns(df, check_columns)
41
+
42
+ df_FVGs = self.find_FVGs(df, side)
43
+ # self.logger.info(f"fvgs:\n{df_FVGs[['timestamp', self.FVG_SIDE, self.FVG_TOP, self.FVG_BOT, self.FVG_WAS_BALANCED]]}")
44
+
45
+
46
+ df_OBs = self.find_OBs(df, side)
47
+ # self.logger.info("find_OBs:\n %s", df_OBs)
48
+
49
+ # 使用更简洁的方式重命名和合并时间戳列
50
+ timestamp_mapping = {self.TIMESTAMP_COL: ['ts_OBs', 'ts_FVGs']}
51
+ df_OBs = df_OBs.rename(columns={self.TIMESTAMP_COL: timestamp_mapping[self.TIMESTAMP_COL][0]})
52
+ df_FVGs = df_FVGs.rename(columns={self.TIMESTAMP_COL: timestamp_mapping[self.TIMESTAMP_COL][1]})
53
+
54
+ # 使用更高效的方式合并数据框
55
+ df_PDArrays = pd.concat(
56
+ [df_OBs, df_FVGs],
57
+ axis=1,
58
+ join='outer'
59
+ ).sort_index()
60
+
61
+ # 使用更清晰的方式合并时间戳列
62
+ df_PDArrays[self.TIMESTAMP_COL] = df_PDArrays[timestamp_mapping[self.TIMESTAMP_COL][0]].fillna(
63
+ df_PDArrays[timestamp_mapping[self.TIMESTAMP_COL][1]]
64
+ )
65
+ df_PDArrays[self.PD_TYPE_COL] = df_PDArrays[[self.FVG_SIDE, self.OB_DIRECTION_COL]].apply(
66
+ lambda x: 'FVG-OB' if pd.notna(x.iloc[0]) and pd.notna(x.iloc[1]) else 'FVG' if pd.notna(x.iloc[0]) else 'OB', axis=1
67
+ )
68
+
69
+ df_PDArrays.loc[:, self.PD_HIGH_COL] = df_PDArrays[[self.FVG_TOP, self.OB_HIGH_COL]].max(axis=1)
70
+ df_PDArrays.loc[:, self.PD_LOW_COL] = df_PDArrays[[self.FVG_BOT, self.OB_LOW_COL]].min(axis=1)
71
+ df_PDArrays.loc[:, self.PD_MID_COL] = (df_PDArrays[self.PD_HIGH_COL] + df_PDArrays[self.PD_LOW_COL]) / 2
72
+
73
+
74
+
75
+
76
+ return df_PDArrays
77
+
core/smc/SMCStruct.py ADDED
@@ -0,0 +1,290 @@
1
+ import logging
2
+ import operator
3
+ from core.utils.OPTools import OPTools
4
+ from core.smc.SMCBase import SMCBase
5
+
6
+ class SMCStruct(SMCBase):
7
+ STRUCT_COL = "struct"
8
+ STRUCT_HIGH_COL = "struct_high"
9
+ STRUCT_LOW_COL = "struct_low"
10
+ STRUCT_HIGH_INDEX_COL = "struct_high_index"
11
+ STRUCT_LOW_INDEX_COL = "struct_low_index"
12
+ STRUCT_DIRECTION_COL = "struct_direction"
13
+ HIGH_START_COL = "high_start"
14
+ LOW_START_COL = "low_start"
15
+
16
+ def __init__(self):
17
+ super().__init__()
18
+ self.logger = logging.getLogger(__name__)
19
+
20
+
21
+
22
+ def build_struct(self, data, window=10, is_struct_body_break=True):
23
+ """处理价格结构,识别高低点突破和结构方向
24
+
25
+ Args:
26
+ df: 数据框
27
+ window: 寻找结构极值的窗口大小
28
+ is_struct_body_break: 是否使用收盘价判断突破
29
+
30
+ Returns:
31
+ 处理后的数据框,包含结构相关列
32
+ """
33
+ df = data.copy()
34
+ check_columns = [self.HIGH_COL, self.LOW_COL, self.CLOSE_COL]
35
+ self.check_columns(df, check_columns)
36
+
37
+ # 初始化结构相关列
38
+ # 定义结构相关的列名
39
+ struct_columns = [self.STRUCT_COL, self.STRUCT_HIGH_COL, self.STRUCT_LOW_COL,
40
+ self.STRUCT_HIGH_INDEX_COL, self.STRUCT_LOW_INDEX_COL, self.STRUCT_DIRECTION_COL]
41
+
42
+ # 初始化结构相关列的默认值
43
+ default_values = {
44
+ self.STRUCT_COL: None, # 结构类型列初始化为None
45
+ self.STRUCT_HIGH_COL: self.toDecimal('0.0'), # 结构高点价格初始化为0
46
+ self.STRUCT_LOW_COL: self.toDecimal('0.0'), # 结构低点价格初始化为0
47
+ self.STRUCT_HIGH_INDEX_COL: 0, # 结构高点索引初始化为0
48
+ self.STRUCT_LOW_INDEX_COL: 0, # 结构低点索引初始化为0
49
+ self.STRUCT_DIRECTION_COL: 0 # 结构方向初始化为0
50
+ }
51
+
52
+ # 为每个结构列赋默认值
53
+ for col in struct_columns:
54
+ df[col] = default_values[col]
55
+
56
+ # 初始化结构变量
57
+ structure = {
58
+ self.HIGH_COL: df[self.HIGH_COL].iloc[0],
59
+ self.LOW_COL: df[self.LOW_COL].iloc[0],
60
+ self.HIGH_START_COL: -1,
61
+ self.LOW_START_COL: -1,
62
+ 'direction': 0
63
+ }
64
+
65
+ # 确定突破判断列
66
+ break_price_col = self.CLOSE_COL if is_struct_body_break else self.HIGH_COL
67
+ break_price_col_low = self.CLOSE_COL if is_struct_body_break else self.LOW_COL
68
+
69
+ for i in range(1, len(df)):
70
+ curr_prices = {
71
+ self.HIGH_COL: df[break_price_col].iloc[i],
72
+ self.LOW_COL: df[break_price_col_low].iloc[i]
73
+ }
74
+
75
+ # 获取前3根K线价格
76
+ prev_prices = {
77
+ self.HIGH_COL: df[break_price_col].iloc[i-3:i].values,
78
+ self.LOW_COL: df[break_price_col_low].iloc[i-3:i].values
79
+ }
80
+
81
+ # 判断结构突破
82
+ is_high_broken = self._check_structure_break(
83
+ curr_price=curr_prices[self.HIGH_COL],
84
+ struct_price=structure[self.HIGH_COL],
85
+ prev_prices=prev_prices[self.HIGH_COL],
86
+ struct_start=structure[self.HIGH_START_COL],
87
+ i=i,
88
+ direction=structure['direction'],
89
+ target_direction=1
90
+ )
91
+
92
+ is_low_broken = self._check_structure_break(
93
+ curr_price=curr_prices[self.LOW_COL],
94
+ struct_price=structure[self.LOW_COL],
95
+ prev_prices=prev_prices[self.LOW_COL],
96
+ struct_start=structure[self.LOW_START_COL],
97
+ i=i,
98
+ direction=structure['direction'],
99
+ target_direction=2,
100
+ mode=self.LOW_COL
101
+ )
102
+
103
+ if is_low_broken:
104
+ # 处理低点突破
105
+ structure = self._handle_structure_break(
106
+ df, i, window, structure,
107
+ break_type=self.LOW_COL,
108
+ struct_type='BOS' if structure['direction'] == 1 else 'CHOCH'
109
+ )
110
+
111
+ elif is_high_broken:
112
+ # 处理高点突破
113
+ structure = self._handle_structure_break(
114
+ df, i, window, structure,
115
+ break_type=self.HIGH_COL,
116
+ struct_type='BOS' if structure['direction'] == 2 else 'CHOCH'
117
+ )
118
+
119
+ else:
120
+ # 更新当前结构
121
+ structure = self._update_current_structure(
122
+ df, i, structure,
123
+ is_struct_body_break=is_struct_body_break
124
+ )
125
+
126
+ # 更新数据框结构列
127
+ self._update_structure_columns(df, i, structure)
128
+
129
+ return df
130
+
131
+ def _get_structure_extreme_bar(self, df, bar_index, struct_index, lookback=10, mode='high'):
132
+ """
133
+ 获取结构最高点或最低点
134
+ :param df: DataFrame数据
135
+ :param bar_index: 当前K线索引
136
+ :param lookback: 回溯周期
137
+ :param mode: self.HIGH_COL寻找最高点,self.LOW_COL寻找最低点
138
+ :return: 结构极值点的索引
139
+ """
140
+ df = df.copy()
141
+ # window_start = max(0, bar_index - lookback + 1)
142
+ window_start = max(0, struct_index)
143
+
144
+ window = df.iloc[window_start : bar_index + 1]
145
+
146
+ # 获取窗口内的极值点索引
147
+ if mode == self.HIGH_COL:
148
+ extremeBar = window[self.HIGH_COL].argmax()
149
+ price_col = self.HIGH_COL
150
+ comp_func = lambda x, y: x > y
151
+ else:
152
+ extremeBar = window[self.LOW_COL].argmin()
153
+ price_col = self.LOW_COL
154
+ comp_func = lambda x, y: x < y
155
+
156
+ # 初始化记录点
157
+ pivot = 0
158
+ # 从后向前遍历寻找结构极值点
159
+ # for i in range(lookback - 1, -1, -1):
160
+ for idx in range(bar_index - 1, struct_index - 1, -1):
161
+ # 计算当前位置的索引
162
+ # idx = bar_index - i
163
+ if idx - 2 < 0 or idx + 1 >= len(df):
164
+ continue
165
+
166
+ price_prev = df[price_col].iloc[idx - 1]
167
+ price_prev2 = df[price_col].iloc[idx - 2]
168
+ price_curr = df[price_col].iloc[idx]
169
+
170
+ # 记录满足条件的点位
171
+ if (comp_func(price_prev, price_prev2) and
172
+ not comp_func(price_curr, price_prev) \
173
+ # and idx - 1 >= extremeBar
174
+ ):
175
+ if pivot == 0:
176
+ pivot = idx - 1
177
+ continue
178
+ else:
179
+ # 比较当前点位与之前记录的极值点的价格,因为在区间内有多个极致点,需要比较
180
+ if comp_func(df[price_col].iloc[idx-1], df[price_col].iloc[pivot]):
181
+ pivot = idx - 1
182
+ if pivot != 0:
183
+ extremeBar = pivot
184
+
185
+ return extremeBar
186
+
187
+ def _check_structure_break(self, curr_price, struct_price, prev_prices, struct_start, i, direction, target_direction, mode='high'):
188
+ """检查结构是否突破"""
189
+ comp = operator.gt if mode == self.HIGH_COL else operator.lt
190
+ reverse_comp = operator.le if mode == self.HIGH_COL else operator.ge
191
+
192
+ basic_break = (
193
+ comp(curr_price, struct_price) and
194
+ all(reverse_comp(p, struct_price) for p in prev_prices) and
195
+ all(i-j > struct_start for j in range(1,4))
196
+ )
197
+
198
+ direction_break = direction == target_direction and comp(curr_price, struct_price)
199
+
200
+ return basic_break or direction_break
201
+
202
+ def _handle_structure_break(self, df, i, window, structure, break_type, struct_type):
203
+ """处理结构突破"""
204
+ is_high_break = break_type == self.HIGH_COL
205
+
206
+ struct_start = structure[self.HIGH_START_COL] if is_high_break else structure[self.LOW_START_COL]
207
+
208
+ # 获取新的极值点
209
+ extreme_idx = self._get_structure_extreme_bar(
210
+ df, i, struct_start, window,
211
+ mode=self.LOW_COL if is_high_break else self.HIGH_COL
212
+ )
213
+
214
+ # 更新结构信息
215
+ new_structure = structure.copy()
216
+ new_structure['direction'] = 2 if is_high_break else 1
217
+
218
+ if is_high_break:
219
+ new_structure.update({
220
+ self.LOW_COL: self.toDecimal(df[self.LOW_COL].iloc[extreme_idx]),
221
+ self.HIGH_COL: self.toDecimal(df[self.HIGH_COL].iloc[i]),
222
+ self.LOW_START_COL: extreme_idx,
223
+ self.HIGH_START_COL: i
224
+ })
225
+ else:
226
+ new_structure.update({
227
+ self.HIGH_COL: self.toDecimal(df[self.HIGH_COL].iloc[extreme_idx]),
228
+ self.LOW_COL: self.toDecimal(df[self.LOW_COL].iloc[i]),
229
+ self.HIGH_START_COL: extreme_idx,
230
+ self.LOW_START_COL: i
231
+ })
232
+
233
+ # 更新DataFrame结构信息
234
+ df.at[i, 'struct_direction'] = new_structure['direction']
235
+ df.at[i, 'struct'] = f"{'Bullish' if is_high_break else 'Bearish'}_{struct_type}"
236
+
237
+ return new_structure
238
+
239
+ def _update_current_structure(self, df, i, structure, is_struct_body_break):
240
+ """更新当前结构"""
241
+ new_structure = structure.copy()
242
+
243
+ # 检查是否需要更新高点
244
+ if (structure['direction'] in (2,0)) and df.at[i,self.HIGH_COL] > structure[self.HIGH_COL]:
245
+ if not (is_struct_body_break and all(i-j > structure[self.HIGH_START_COL] for j in range(1,4))):
246
+ new_structure[self.HIGH_COL] = df.at[i,self.HIGH_COL]
247
+ new_structure[self.HIGH_START_COL] = i
248
+
249
+ # 检查是否需要更新低点
250
+ elif (structure['direction'] in (1,0)) and df.at[i,self.LOW_COL] < structure[self.LOW_COL]:
251
+ if not (is_struct_body_break and all(i-j > structure[self.LOW_START_COL] for j in range(1,4))):
252
+ new_structure[self.LOW_COL] = df.at[i,self.LOW_COL]
253
+ new_structure[self.LOW_START_COL] = i
254
+
255
+ return new_structure
256
+
257
+ def _update_structure_columns(self, df, i, structure):
258
+ """更新数据框中的结构列"""
259
+ df.at[i, self.STRUCT_HIGH_COL] = self.toDecimal(structure[self.HIGH_COL])
260
+ df.at[i, self.STRUCT_LOW_COL] = self.toDecimal(structure[self.LOW_COL] )
261
+ df.at[i, self.STRUCT_HIGH_INDEX_COL] = structure[self.HIGH_START_COL]
262
+ df.at[i, self.STRUCT_LOW_INDEX_COL] = structure[self.LOW_START_COL]
263
+
264
+ def get_last_struct(self, df, prd=-1):
265
+ """
266
+ 获取最新的结构
267
+ """
268
+ data = self.build_struct(df=df, window=10)
269
+ # 筛选出有效的结构
270
+
271
+ # 获取prd范围内的数据
272
+ start_idx = max(0, len(data) - 1 - prd)
273
+ # 筛选有效结构且在prd范围内的数据
274
+ last_struct = None
275
+ mask = data[self.STRUCT_COL].notna() & (data.index >= start_idx) if prd > 0 else data[self.STRUCT_COL].notna()
276
+ valid_structs = data[ mask ]
277
+ if not valid_structs.empty:
278
+ # 获取最近的结构
279
+ last_struct = valid_structs.iloc[-1]
280
+ return {
281
+ self.STRUCT_COL: last_struct[self.STRUCT_COL],
282
+ self.STRUCT_HIGH_COL: last_struct[self.STRUCT_HIGH_COL],
283
+ self.STRUCT_LOW_COL: last_struct[self.STRUCT_LOW_COL],
284
+ self.STRUCT_HIGH_INDEX_COL: last_struct[self.STRUCT_HIGH_INDEX_COL],
285
+ self.STRUCT_LOW_INDEX_COL: last_struct[self.STRUCT_LOW_INDEX_COL],
286
+ self.STRUCT_DIRECTION_COL: last_struct[self.STRUCT_DIRECTION_COL]
287
+ }
288
+
289
+ return last_struct
290
+
core/smc/__init__.py ADDED
File without changes
core/utils/OPTools.py ADDED
@@ -0,0 +1,30 @@
1
+ import requests
2
+ from decimal import Decimal
3
+
4
+ class OPTools:
5
+
6
+ @staticmethod
7
+ def toDecimal(value, precision:int=None):
8
+ """将数值转换为Decimal类型
9
+
10
+ Args:
11
+ value: 需要转换的数值
12
+ precision: 精度,如果不指定则保持原始精度
13
+
14
+ Returns:
15
+ Decimal: 转换后的Decimal对象
16
+ """
17
+ if precision is None:
18
+ return Decimal(str(value))
19
+ return Decimal(f"{value:.{precision}f}")
20
+
21
+ @staticmethod
22
+ def send_feishu_notification(webhook, message):
23
+ if webhook:
24
+ headers = {'Content-Type': 'application/json'}
25
+ data = {"msg_type": "text", "content": {"text": message}}
26
+ response = requests.post(webhook, headers=headers, json=data)
27
+ if response.status_code != 200:
28
+ # self.logger.debug("飞书通知发送成功")
29
+ raise Exception(f"飞书通知发送失败: {response.text} {webhook}")
30
+