oqclib 0.1.37__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.
oqclib-0.1.37/PKG-INFO ADDED
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.1
2
+ Name: oqclib
3
+ Version: 0.1.37
4
+ Summary: OQC Python Libraries
5
+ License: MIT
6
+ Author: OQC
7
+ Author-email: oqc@hotmail.com
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Dist: mplfinance (>=0.12.10b0,<0.13.0)
15
+ Requires-Dist: pycryptodome (>=3.18.0,<4.0.0)
16
+ Requires-Dist: tomli (>=2.0.1,<3.0.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # oqclib
20
+
21
+
@@ -0,0 +1,2 @@
1
+ # oqclib
2
+
File without changes
@@ -0,0 +1,51 @@
1
+ import tomli
2
+ import socket
3
+ import os
4
+ import base64
5
+ from . import encrypt_util
6
+
7
+
8
+ class Config:
9
+ def __init__(self, config_path: str):
10
+ self.data = None
11
+ self.key_path = os.path.expanduser('~/.ssh/id_rsa')
12
+ self.config_path = config_path
13
+ self.load_config()
14
+
15
+ def load_config(self):
16
+ with open(self.config_path, 'rb') as f:
17
+ self.data = tomli.load(f)
18
+
19
+ try:
20
+ vault_path = self.get_vault_path()
21
+ with open(vault_path, 'rb') as f:
22
+ self.vault = tomli.load(f)
23
+ for k, v in self.vault.items():
24
+ encrypted_data = base64.b64decode(v.encode())
25
+ decrypted_data = encrypt_util.decryt(self.key_path, encrypted_data)
26
+ self.vault[k] = decrypted_data.decode('ascii').strip()
27
+ self.populate_password(self.data)
28
+ except Exception as e:
29
+ print(f"Error loading vault: {e}")
30
+
31
+ def populate_password(self, config_dict: dict[str, str]):
32
+ for k, v in config_dict.items():
33
+ if isinstance(v, dict):
34
+ self.populate_password(v)
35
+ continue
36
+ if not isinstance(v, str) or not v.startswith('map:'):
37
+ continue
38
+ vault_key = v.split(':')[1]
39
+ if vault_key in self.vault:
40
+ config_dict[k] = self.vault[vault_key]
41
+
42
+ def get_vault_path(self):
43
+ dirname = os.path.dirname(self.config_path)
44
+ return os.path.join(dirname, 'vault', socket.gethostname() + '.py')
45
+
46
+ def get_mysql_string(self, config_name: str):
47
+ if config_name not in self.data['mysql']:
48
+ return
49
+ config = self.data['mysql'][config_name]
50
+ return 'mysql+mysqldb://%s:%s@%s:%d/%s?charset=utf8' % (
51
+ config["user"], config["password"], config["host"], config["port"], config["db"])
@@ -0,0 +1,32 @@
1
+ from Crypto.PublicKey import RSA
2
+ from Crypto.Cipher import PKCS1_v1_5
3
+ from Crypto import Random
4
+
5
+ def read_private_key(path: str):
6
+ with open(path, 'rb') as f:
7
+ data = f.read()
8
+ key = RSA.importKey(data)
9
+ return key
10
+
11
+ def decode_msg_v1_5(cipherbytes, private_key):
12
+ """ Should consider using the more robust PKCS1 OAEP. """
13
+ sentinel = Random.new().read(256) # data length is 256
14
+ cipher = PKCS1_v1_5.new(private_key)
15
+ messagereceived = cipher.decrypt(cipherbytes, sentinel)
16
+ return messagereceived
17
+
18
+ def decryt(rsa_file: str, cipherbytes: bytes):
19
+ private_key = read_private_key(rsa_file)
20
+
21
+ # Decrypt the data
22
+ return decode_msg_v1_5(cipherbytes, private_key)
23
+
24
+ if __name__ == '__main__':
25
+ import os
26
+ import base64
27
+
28
+ encrypted_data = base64.b64decode(b"iICR6L42w6lqTNsOcZEs9fu7OmRDLSyfpW8v85u0hEkD4gObbwAM+SXO64ZCpKrzJPwoQPggmOaPybgOS4ZZtHsXeNq/CAznK+dz/fxp9FQher+yp6TBZ2BFe5B0NOj3NLDVImLBYPcXZ99IAQNrIylBkF7Ns6ZxPPBeqh3jlIZMTmoyO3PMBvPDmOyOP3zir2SG/CDU2quYKBRSNXNjo+Y2jeO/SPJqBjRWlsSW5xWz2+kqcxsXUhvQH56sQVT+MbYtvTfgQMkPS81Mk9JFJd6mlGgCKUrXKbW6wO9xfkdi92hOLSwGCQEbdaXqyQaYv62+Rb5KtM6Mw8/zoBJx7wNhkR/CWJt0jygEix57s+qKop1+KU9UZVbFdQDgyWCz7Un8v0kHEcMEF8TbgHSqajwqGumaXKQYy3bMkLAT816TeGWd4Yl1pD2SuTHoO1N76q1oLwmXhiZ9TQeDXuLOhwP0Fqc9SzbcB3ZUjUWHF/vCjF+UPbAtCBIotymNA+4Xc3YLHMJOCeRVhrorn4sG+Iu1tUjLtNqhYDgtuauzE7sB8cR0Q+qDajlTZw7iDLAkleQ8K+kDNUjaT37/AM37/m6Q2k9rT7EZMBDihkbXd3Y2u4z2yzxItArI1kPAd4HmBoc2R0JdnZnPJBr02SurGrYRwnJRrTW5UXWnmTjVBFA=")
29
+
30
+ decrypted_data = decryt(os.path.expanduser('~/.ssh/id_rsa'), encrypted_data)
31
+ # Print the decrypted data
32
+ print(decrypted_data)
File without changes
File without changes
@@ -0,0 +1,9 @@
1
+ # This is translated from Rust to Python.
2
+
3
+ # Constants for rate limit and API endpoints
4
+ RATE_LIMIT_SLEEP_20_PER_2 = 100 # Milliseconds
5
+ HOST = "https://www.okx.com"
6
+ REST_TICKER = "/api/v5/market/ticker?instId={}"
7
+ REST_INSTRUMENTS = "/api/v5/public/instruments?instType={}"
8
+ REST_FUNDING_RATE = "/api/v5/public/funding-rate?instId={}"
9
+ REST_CANDLE_STICK = "/api/v5/market/mark-price-candles?limit=100&instId={instId}&bar={bar}"
@@ -0,0 +1,58 @@
1
+
2
+
3
+ def format_time_period(minutes:int) -> str:
4
+ if minutes < 60:
5
+ return f"{minutes}m" # Minutes
6
+ elif minutes == 60:
7
+ return "1H" # 1 Hour
8
+ elif minutes < 1440:
9
+ hours = minutes // 60
10
+ return f"{hours}H" # Hours
11
+ elif minutes == 1440:
12
+ return "1D" # 1 Day
13
+ elif minutes < 10080:
14
+ days = minutes // 1440
15
+ return f"{days}D" # Days
16
+ elif minutes < 43200:
17
+ weeks = minutes // 10080
18
+ return f"{weeks}W" # Weeks
19
+ else:
20
+ months = minutes // 43200
21
+ return f"{months}M" # Months
22
+
23
+ import asyncio
24
+ import aiohttp
25
+ import time
26
+ from datetime import datetime
27
+
28
+ async def fetch_candlesticks(client, symbol, bar_period, look_back_minutes):
29
+ result = []
30
+ now_ms = int(time.time() * 1000)
31
+ from_ms = now_ms - look_back_minutes * 1000 * 60
32
+
33
+ while True:
34
+ to_ms = from_ms + 1000 * 60 * 100
35
+ # url = f"{HOST}{REST_CANDLE_STICK.replace('{instId}', symbol).replace('{bar}', bar_period)}&before={to_ms}&after={from_ms}"
36
+
37
+ print(f"Requesting {url} from {datetime.fromtimestamp(from_ms/1000)} to {datetime.fromtimestamp(to_ms/1000)}")
38
+ async with client.get(url) as response:
39
+ response_data = await response.json()
40
+ if response_data['code'] == '0': # Assuming '0' indicates success
41
+ response_data['data'].reverse()
42
+ result.extend(response_data['data'])
43
+
44
+ if to_ms >= now_ms:
45
+ break
46
+ from_ms = to_ms
47
+ await asyncio.sleep(RATE_LIMIT_SLEEP_20_PER_2 / 1000) # Convert milliseconds to seconds
48
+
49
+ return result
50
+
51
+ # Example usage
52
+ async def main():
53
+ async with aiohttp.ClientSession() as client:
54
+ candles = await fetch_candlesticks(client, 'BTC-USD', '1m', 60)
55
+ print(candles)
56
+
57
+ if __name__ == '__main__':
58
+ asyncio.run(main())
@@ -0,0 +1,142 @@
1
+ import pandas as pd
2
+ from .model import DivergenceType
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ FIELD = 'dif'
8
+ CROSS_THRESHOLD = 2
9
+
10
+ def detect_macd_divergence(df: pd.DataFrame, nth: int = 0) -> DivergenceType | None:
11
+ assert 'close' in df.columns, "Dataframe must contain 'close' column"
12
+ assert 'dif' in df.columns, "Dataframe must contain 'ema12' column"
13
+ assert 'dea' in df.columns, "Dataframe must contain 'ema26' column"
14
+
15
+ ret = is_bearish_divergence(df, nth)
16
+ if ret is not None:
17
+ logger.info(f"Bearish divergence detected {df.loc[ret[0]][['close', 'dif', 'dea']]} {df.loc[ret[1]][['close', 'dif', 'dea']]} last {df.tail(5)[['close', 'dif', 'dea']]}")
18
+ return DivergenceType.BEARISH
19
+ ret = is_bullish_divergence(df, nth)
20
+ if ret is not None:
21
+ logger.info(f"Bullish divergence detected {df.loc[ret[0]][['close', 'dif', 'dea']]} {df.loc[ret[1]][['close', 'dif', 'dea']]} last {df.tail(5)[['close', 'dif', 'dea']]}")
22
+ return DivergenceType.BULLISH
23
+ return None
24
+
25
+
26
+ def is_golden_cross(df: pd.DataFrame, nth: int = 0) -> bool:
27
+ # time_ms = df.iloc[-nth - 1]['timeMs']
28
+ # print(f"is_golden_cross, nth {nth}, time: {time_ms}")
29
+ dif_pre = df['dif'].iloc[-nth - 2]
30
+ dea_pre = df['dea'].iloc[-nth - 2]
31
+ return dif_pre < dea_pre and df['dif'].iloc[-nth - 1] > df['dea'].iloc[-nth - 1]
32
+
33
+
34
+ # Write is_death_cross function
35
+ def is_death_cross(df: pd.DataFrame, nth: int = 0) -> bool:
36
+ return df['dif'].iloc[-nth - 2] > df['dea'].iloc[-nth - 2] and df['dif'].iloc[-nth - 1] < df['dea'].iloc[-nth - 1]
37
+
38
+
39
+ def is_bearish_divergence(df: pd.DataFrame, nth: int = 0) -> ():
40
+ ret = None
41
+ # 如果不是死叉,直接退出
42
+ if not is_death_cross(df, nth):
43
+ return None
44
+ step = 1
45
+ size = len(df)
46
+ start1, start2, end1, end2 = 0, 0, 0, 0
47
+ for i in range(nth + 1, size - 2):
48
+ # 找到上一个金叉
49
+ if step == 1 and is_golden_cross(df, nth=i):
50
+ start1, end1 = nth + 1, i
51
+ if end1 - start1 < CROSS_THRESHOLD:
52
+ # print(f"First golden cross at {start1} {end1}")
53
+ return ret
54
+ step = 2
55
+ # 找到上一个死叉
56
+ if step == 2 and is_death_cross(df, nth=i):
57
+ start2 = i + 1
58
+ if start2 - end1 < CROSS_THRESHOLD:
59
+ # print(f"First death cross at {end1} {start2}")
60
+ return ret
61
+ step = 3
62
+ # 再找到上一个金叉
63
+ if step == 3 and is_golden_cross(df, nth=i):
64
+ end2 = i
65
+ step = 4
66
+ break
67
+ if step == 4:
68
+ # 求两个区间的最高价
69
+ max1 = -1e8
70
+ max2 = -1e8
71
+ id1 = 0
72
+ id2 = 0
73
+ for i in range(start1, end1 + 1):
74
+ if max1 < df[FIELD].iloc[-i - 1]:
75
+ max1 = df[FIELD].iloc[-i - 1]
76
+ id1 = i
77
+ for i in range(start2, end2 + 1):
78
+ if max2 < df[FIELD].iloc[-i - 1]:
79
+ max2 = df[FIELD].iloc[-i - 1]
80
+ id2 = i
81
+ close1 = df['close'].iloc[-id1 - 1]
82
+ close2 = df['close'].iloc[-id2 - 1]
83
+ dif1 = df['dif'].iloc[-id1 - 1]
84
+ dif2 = df['dif'].iloc[-id2 - 1]
85
+
86
+ if close1 > close2 and dif1 < dif2:
87
+ logger.info(f"Bearish divergence close2 {close2} dif2 {dif2} close1 {close1} dif1 {dif1}")
88
+ ret = (df.index[-id2 - 1], df.index[-id1 - 1])
89
+ return ret
90
+
91
+
92
+ def is_bullish_divergence(df: pd.DataFrame, nth: int = 0) -> ():
93
+ ret = None
94
+ # 如果不是金叉,直接退出
95
+ if not is_golden_cross(df, nth=nth):
96
+ return ret
97
+ step = 1
98
+ size = len(df)
99
+ start1, start2, end1, end2 = 0, 0, 0, 0
100
+ for i in range(nth + 1, size - 2):
101
+ # 找到上一个死叉
102
+ if step == 1 and is_death_cross(df, nth=i):
103
+ start1, end1 = nth + 1, i
104
+ if end1 - start1 < CROSS_THRESHOLD:
105
+ # print(f"First death cross at {start1} {end1}")
106
+ return ret
107
+ step = 2
108
+ # 找到上一个金叉
109
+ if step == 2 and is_golden_cross(df, nth=i):
110
+ start2 = i + 1
111
+ if start2 - end1 < CROSS_THRESHOLD:
112
+ # print(f"First Golden cross at {end1} {start2}")
113
+ return ret
114
+ step = 3
115
+ # 再找到上一个死叉
116
+ if step == 3 and is_death_cross(df, nth=i):
117
+ end2 = i
118
+ step = 4
119
+ break
120
+ if step == 4:
121
+ # 求两个区间的最低价
122
+ min1 = 1e8
123
+ min2 = 1e8
124
+ id1 = 0
125
+ id2 = 0
126
+ for i in range(start1, end1 + 1):
127
+ if min1 > df[FIELD].iloc[-i - 1]:
128
+ min1 = df[FIELD].iloc[-i - 1]
129
+ id1 = i
130
+ for i in range(start2, end2 + 1):
131
+ if min2 > df[FIELD].iloc[-i - 1]:
132
+ min2 = df[FIELD].iloc[-i - 1]
133
+ id2 = i
134
+ close1 = df['close'].iloc[-id1 - 1]
135
+ close2 = df['close'].iloc[-id2 - 1]
136
+ dif1 = df['dif'].iloc[-id1 - 1]
137
+ dif2 = df['dif'].iloc[-id2 - 1]
138
+
139
+ if close1 < close2 and dif1 > dif2:
140
+ logger.info(f"Bullish divergence close2 {close2} dif2 {dif2} close1 {close1} dif1 {dif1}")
141
+ ret = (df.index[-id2 - 1], df.index[-id1 - 1])
142
+ return ret
@@ -0,0 +1,167 @@
1
+ import pandas as pd
2
+ import talib as tl
3
+
4
+ class Divergence:
5
+ def __init__(self, quote: pd.DataFrame, fastperiod: int, slowperiod: int, signalperiod: int, name):
6
+ close = quote['close']
7
+ date = quote.index
8
+ self.dif, self.dea, self.macd = tl.MACDEXT(close, fastperiod=fastperiod, fastmatype=1, slowperiod=slowperiod,
9
+ slowmatype=1, signalperiod=signalperiod, signalmatype=1)
10
+ self.dif = pd.Series(self.dif).sort_index(ascending=False).dropna().reset_index(drop=True)
11
+ self.dea = pd.Series(self.dea).sort_index(ascending=False).dropna().reset_index(drop=True)
12
+ self.macd = pd.Series(self.macd).sort_index(ascending=False).dropna().reset_index(drop=True) * 2
13
+ self.sz = len(self.dif)
14
+ self.close = pd.Series(close).sort_index(ascending=False).reset_index(drop=True).head(self.sz)
15
+ self.date = pd.Series(date).sort_index(ascending=False).reset_index(drop=True).head(self.sz)
16
+ self.name = name
17
+
18
+ def get_crosses(self):
19
+ values = []
20
+ for i in range(self.sz - 2, 0, -1):
21
+ if self.is_gold_cross(i):
22
+ values.append("('%s',1,'%s')" % (self.get_date_by_index(i), self.name))
23
+ elif self.is_death_cross(i):
24
+ values.append("('%s',0,'%s')" % (self.get_date_by_index(i), self.name))
25
+
26
+ sql = 'insert ignore into `AMV` values %s' % (', '.join(values))
27
+ return sql
28
+
29
+ def get_divergence(self):
30
+ values = []
31
+ last_dir = -1
32
+ for i in range(self.sz - 2, 0, -1):
33
+ if last_dir != 0 and self.is_gold_cross(i):
34
+ last_dir = 0
35
+ values.append("('%s',1,'%s')" % (self.get_date_by_index(i), self.name + "-bearish-divergence"))
36
+ elif self.is_bearish_divergence(i):
37
+ last_dir = 1
38
+ values.append("('%s',0,'%s')" % (self.get_date_by_index(i), self.name + "-bearish-divergence"))
39
+
40
+ sql = 'insert ignore into `AMV` values %s' % (', '.join(values))
41
+ return sql
42
+
43
+ def get_date_by_index(self, N=0):
44
+ return self.date[N]
45
+
46
+ # 判断往前第N个时段是否为金叉,N取值范围为(0, self.sz),缺省N=0
47
+ def is_gold_cross(self, N=0):
48
+ return (self.dif[N + 1] < self.dea[N + 1] and self.dif[N] > self.dea[N])
49
+
50
+ # 判断往前第N个时段是否为死叉,N取值范围为(0, self.sz),缺省N=0
51
+ def is_death_cross(self, N=0):
52
+ return (self.dif[N + 1] > self.dea[N + 1] and self.dif[N] < self.dea[N])
53
+
54
+ # 判断往前第N个时段是否为0轴上,N取值范围为(0, self.sz),缺省N=0
55
+ def is_high_value(self, N=0):
56
+ return (self.dif[N] > 0 and self.dea[N] > 0)
57
+
58
+ # 判断往前第N个时段是否为0轴下,N取值范围为(0, self.sz),缺省N=0
59
+ def is_low_value(self, N=0):
60
+ return (self.dif[N] < 0 and self.dea[N] < 0)
61
+
62
+ # 判断往前第N个时段是否是低位二次金叉,缺省N=0
63
+ def is_second_low_gold_cross(self, N=0):
64
+ # 判断当前是否发生金叉,并且位置在0轴之下
65
+ if not self.is_gold_cross(N=N) or not self.is_low_value(N=N):
66
+ return False
67
+ flag = False
68
+ for i in range(N + 1, self.sz - 1):
69
+ if not self.is_low_value(N=i):
70
+ break
71
+ if self.is_gold_cross(N=i):
72
+ flag = True
73
+ break
74
+ return flag
75
+
76
+ # 判断往前第N个时段是否是高位二次死叉,缺省N=0
77
+ def is_second_high_death_cross(self, N=0):
78
+ # 判断当前是否发生死叉,并且位置在0轴之上
79
+ if not self.is_death_cross(N=N) or not self.is_high_value(N=N):
80
+ return False
81
+ flag = False
82
+ for i in range(N + 1, self.sz - 1):
83
+ if not self.is_high_value(N=i):
84
+ break
85
+ if self.is_death_cross(N=i):
86
+ flag = True
87
+ break
88
+ return flag
89
+
90
+ # 判断往前第N个时段是否是快慢线底背离(价格与DIF的背离),缺省N=0
91
+ # price创新低,但dif没有创新低
92
+ def is_bullish_divergence(self, N=0):
93
+ ret = None
94
+ # 如果不是金叉,直接退出
95
+ if not self.is_gold_cross(N=N):
96
+ return ret
97
+ step = 1
98
+ for i in range(N + 1, self.sz - 1):
99
+ # 找到上一个死叉
100
+ if step == 1 and self.is_death_cross(N=i):
101
+ start1, end1 = N + 1, i
102
+ step = 2
103
+ # 找到上一个金叉
104
+ if step == 2 and self.is_gold_cross(N=i):
105
+ start2 = i + 1
106
+ step = 3
107
+ # 再找到上一个死叉
108
+ if step == 3 and self.is_death_cross(N=i):
109
+ end2 = i
110
+ step = 4
111
+ break
112
+ if step == 4:
113
+ # 求两个区间的最低价
114
+ min1 = 1e8
115
+ min2 = 1e8
116
+ for i in range(start1, end1 + 1):
117
+ if min1 > self.close[i]:
118
+ min1 = self.close[i]
119
+ id1 = i
120
+ for i in range(start2, end2 + 1):
121
+ if min2 > self.close[i]:
122
+ min2 = self.close[i]
123
+ id2 = i
124
+ if self.close[id1] < self.close[id2] and self.dif[id1] > self.dif[id2]:
125
+ ret = (self.date[id2], self.date[id1])
126
+ return ret
127
+
128
+ # 判断往前第N个时段是否是快慢线顶背离(价格与DIF的背离),缺省N=0
129
+ def is_bearish_divergence(self, N=0):
130
+ ret = None
131
+ # 如果不是死叉,直接退出
132
+ if not self.is_death_cross(N=N):
133
+ return ret
134
+ step = 1
135
+ for i in range(N + 1, self.sz - 1):
136
+ # 找到上一个金叉
137
+ if step == 1 and self.is_gold_cross(N=i):
138
+ start1, end1 = N + 1, i
139
+ step = 2
140
+ # 找到上一个死叉
141
+ if step == 2 and self.is_death_cross(N=i):
142
+ start2 = i + 1
143
+ step = 3
144
+ # 再找到上一个金叉
145
+ if step == 3 and self.is_gold_cross(N=i):
146
+ end2 = i
147
+ step = 4
148
+ break
149
+ if step == 4:
150
+ # 求两个区间的最高价
151
+ max1 = -1e8
152
+ max2 = -1e8
153
+ id1 = 0
154
+ id2 = 0
155
+ for i in range(start1, end1 + 1):
156
+ if max1 < self.close[i]:
157
+ max1 = self.close[i]
158
+ id1 = i
159
+ for i in range(start2, end2 + 1):
160
+ if max2 < self.close[i]:
161
+ max2 = self.close[i]
162
+ id2 = i
163
+ if self.close[id1] > self.close[id2] and self.dif[id1] < self.dif[id2]:
164
+ ret = (self.date[id2], self.date[id1])
165
+ return ret
166
+
167
+
@@ -0,0 +1,6 @@
1
+ from enum import Enum
2
+
3
+
4
+ class DivergenceType(Enum):
5
+ BULLISH = 1,
6
+ BEARISH = 2
File without changes
@@ -0,0 +1,61 @@
1
+ import time
2
+ import hmac
3
+ import hashlib
4
+ import base64
5
+ import json
6
+ from urllib.parse import quote_plus
7
+ from oqclib.utils.http_util import send_post_request_with_retries
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class DingTalkRobot:
14
+ def __init__(self, keys_dict):
15
+ self.keys_dict = keys_dict
16
+ self.request_url = 'https://oapi.dingtalk.com/robot/send'
17
+
18
+ def generate_sign(self, timestamp, secret: str):
19
+ secret_enc = secret.encode('utf-8')
20
+ string_to_sign = '{}\n{}'.format(timestamp, secret)
21
+ string_to_sign_enc = string_to_sign.encode('utf-8')
22
+ hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
23
+ sign = quote_plus(base64.b64encode(hmac_code))
24
+ return sign
25
+
26
+ def get_request_url(self, robot):
27
+ timestamp = str(round(time.time() * 1000))
28
+ key = self.keys_dict[robot]
29
+
30
+ sign = self.generate_sign(timestamp, key['secret'])
31
+
32
+ url = self.request_url + '?access_token=' + key['token'] + '&timestamp=' + timestamp + '&sign=' + sign
33
+ return url
34
+
35
+ def send_msg(self, robot: str, message: str):
36
+ url = self.get_request_url(robot)
37
+
38
+ headers = {'Content-Type': 'application/json; charset=utf-8'}
39
+ data = {
40
+ "msgtype": "text",
41
+ "text": {
42
+ "content": message
43
+ }
44
+ }
45
+ logger.info(f"Sending message to DingTalk Robot: {robot}, message: {message}")
46
+
47
+ response = send_post_request_with_retries(url, headers=headers, data=json.dumps(data))
48
+ return response.json()
49
+
50
+ def send_card(self, robot: str, card: dict):
51
+ url = self.get_request_url(robot)
52
+
53
+ headers = {'Content-Type': 'application/json; charset=utf-8'}
54
+ data = {
55
+ "msgtype": "markdown",
56
+ "markdown": card
57
+ }
58
+ logger.info(f"Sending message to DingTalk Robot: {robot}, message: {card}")
59
+
60
+ response = send_post_request_with_retries(url, headers=headers, data=json.dumps(data))
61
+ return response.json()
@@ -0,0 +1,67 @@
1
+ import json
2
+ import logging
3
+ from oqclib.utils.http_util import send_post_request_with_retries
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class LarkMsg():
8
+ def __init__(self, keys_dict):
9
+ self.keys_dict = keys_dict
10
+ self.request_url = 'https://open.larksuite.com/open-apis/bot/v2/hook/'
11
+
12
+ def show_lark(self):
13
+ '''
14
+ show available lark config
15
+ :return: lark keys avaliable in config
16
+ '''
17
+ print(self.keys_dict.keys())
18
+
19
+ def send_msg(self, robot, msg):
20
+ '''
21
+ send lark message
22
+ :param msg: message to send
23
+ :return:
24
+ '''
25
+ key = self.keys_dict[robot]
26
+
27
+ mentioned_list = ['@all']
28
+
29
+ textJSON = [[{'tag': 'text', 'text': msg}]]
30
+
31
+ headers = {
32
+ 'Content-Type': 'application/json; charset=utf-8'
33
+ }
34
+
35
+ textMsg = {
36
+ 'msg_type': 'post',
37
+ 'content': {'post': {'en_us': {'content': textJSON}}}
38
+ }
39
+
40
+ se = json.dumps(textMsg)
41
+ #r = requests.post(self.request_url + key, data=se, headers=headers)
42
+ url = self.request_url + key
43
+ r = send_post_request_with_retries(url, se, 3, headers=headers)
44
+ logger.info("Sending lark resp: {}".format(r.text))
45
+
46
+
47
+ def send_card(self, robot, card_body: dict):
48
+ '''
49
+ send lark message card
50
+ :param title: lark card title
51
+ :param msg: message to send
52
+ :param robot: lark key name as defined in lark_config
53
+ :return:
54
+ '''
55
+ key = self.keys_dict[robot]
56
+ headers = {
57
+ 'Content-Type': 'application/json; charset=utf-8'
58
+ }
59
+ textMsg = {
60
+ "msg_type": "interactive",
61
+ "card": card_body
62
+ }
63
+ se = json.dumps(textMsg)
64
+ #r = requests.post(self.request_url + key, data=se, headers=headers)
65
+ url = self.request_url + key
66
+ r = send_post_request_with_retries(url, se, 3, headers=headers)
67
+ logger.info("Sending lark resp: {}".format(r.text))
@@ -0,0 +1,4 @@
1
+ import pandas as pd
2
+
3
+ def to_tushare_date_str(date: pd.Timestamp) -> str:
4
+ return str(date.date()).replace("-","")
File without changes
@@ -0,0 +1,10 @@
1
+ import os
2
+
3
+ def has_gui():
4
+ # Check if DISPLAY environment variable is set (common in Unix-like systems)
5
+ if os.name == 'posix' and 'DISPLAY' not in os.environ:
6
+ return False
7
+ # Windows and macOS are generally running under a GUI
8
+ elif os.name == 'nt' or sys.platform == 'darwin':
9
+ return True
10
+ return False
@@ -0,0 +1,32 @@
1
+ from datetime import datetime, timedelta
2
+
3
+
4
+ def seconds_to_next_interval(dt: datetime, interval_seconds: int) -> float:
5
+ # Calculate total minutes since midnight
6
+ seconds_since_midnight = (dt.hour * 60 + dt.minute) * 60 + dt.second
7
+
8
+ # Find the next multiple of the interval
9
+ next_interval_second = ((seconds_since_midnight // interval_seconds) + 1) * interval_seconds
10
+
11
+ # Calculate the time of the next interval
12
+ next_interval_time = datetime(dt.year, dt.month, dt.day, 0, 0) + timedelta(seconds=next_interval_second)
13
+
14
+ # Calculate the number of seconds until the next interval
15
+ seconds_to_next = (next_interval_time - dt).total_seconds()
16
+
17
+ return seconds_to_next
18
+
19
+
20
+ def is_midnight() -> bool:
21
+ # Get the current local time
22
+ now = datetime.now()
23
+
24
+ # Get the hour component of the current time
25
+ hour = now.hour
26
+
27
+ return hour < 7
28
+
29
+
30
+ if __name__ == '__main__':
31
+ print(seconds_to_next_interval(datetime.now(), 3600 * 12))
32
+ print(is_midnight())
@@ -0,0 +1,18 @@
1
+ import requests
2
+ import logging
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+
7
+ def send_post_request_with_retries(url, data, headers=None, retries=3, **kwargs):
8
+ for i in range(retries + 1):
9
+ try:
10
+ response = requests.post(url, data=data, headers=headers, **kwargs)
11
+ logger.info("POST Response: {} {}".format(response.status_code, response.content.decode("utf-8")))
12
+ response.raise_for_status()
13
+ if response.status_code // 100 == 2:
14
+ return response
15
+ except (requests.exceptions.RequestException, requests.exceptions.HTTPError) as e:
16
+ logger.info(f"Retry {i + 1}: {e}")
17
+
18
+ return None # No successful response after retries
@@ -0,0 +1,37 @@
1
+ [tool.poetry]
2
+ name = "oqclib"
3
+ version = "0.1.37"
4
+ description = "OQC Python Libraries"
5
+ authors = ["OQC <oqc@hotmail.com>"]
6
+ readme = "README.md"
7
+ packages = [
8
+ {include = "oqclib/*"},
9
+ {include = "oqclib/robot/*"},
10
+ {include = "oqclib/utils*"},
11
+ {include = "oqclib/indicators*"},
12
+ {include = "oqclib/exch*"},
13
+ ]
14
+ license = "MIT"
15
+
16
+ [tool.poetry.dependencies]
17
+ python = "^3.10"
18
+ pycryptodome = "^3.18.0"
19
+ tomli = "^2.0.1"
20
+ mplfinance = "^0.12.10b0"
21
+
22
+ [tool.poetry.group.dev.dependencies]
23
+ pytest = "^7.3.1"
24
+
25
+ # douban https://pypi.doubanio.com/simple/
26
+ # netease https://mirrors.163.com/pypi/simple/
27
+ # aliyun https://mirrors.aliyun.com/pypi/simple/
28
+ # tsinghua https://pypi.tuna.tsinghua.edu.cn/simple/
29
+
30
+ [[tool.poetry.source]]
31
+ name = "aliyun"
32
+ url = "https://mirrors.aliyun.com/pypi/simple/"
33
+ priority = "primary"
34
+
35
+ [build-system]
36
+ requires = ["poetry-core"]
37
+ build-backend = "poetry.core.masonry.api"