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 +21 -0
- oqclib-0.1.37/README.md +2 -0
- oqclib-0.1.37/oqclib/__init__.py +0 -0
- oqclib-0.1.37/oqclib/config.py +51 -0
- oqclib-0.1.37/oqclib/encrypt_util.py +32 -0
- oqclib-0.1.37/oqclib/exch/__init__.py +0 -0
- oqclib-0.1.37/oqclib/exch/okv5/__init__.py +0 -0
- oqclib-0.1.37/oqclib/exch/okv5/consts.py +9 -0
- oqclib-0.1.37/oqclib/exch/okv5/utils.py +58 -0
- oqclib-0.1.37/oqclib/indicators/macd_dvgc.py +142 -0
- oqclib-0.1.37/oqclib/indicators/macd_ref.py +167 -0
- oqclib-0.1.37/oqclib/indicators/model.py +6 -0
- oqclib-0.1.37/oqclib/robot/__init__.py +0 -0
- oqclib-0.1.37/oqclib/robot/dingtalk.py +61 -0
- oqclib-0.1.37/oqclib/robot/lark.py +67 -0
- oqclib-0.1.37/oqclib/tushare_util.py +4 -0
- oqclib-0.1.37/oqclib/utils/__init__.py +0 -0
- oqclib-0.1.37/oqclib/utils/common.py +10 -0
- oqclib-0.1.37/oqclib/utils/datetime_util.py +32 -0
- oqclib-0.1.37/oqclib/utils/http_util.py +18 -0
- oqclib-0.1.37/pyproject.toml +37 -0
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
|
+
|
oqclib-0.1.37/README.md
ADDED
|
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
|
+
|
|
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'] + '×tamp=' + 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))
|
|
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"
|