httptrading 1.0.0__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.
- httptrading/__init__.py +6 -0
- httptrading/broker/__init__.py +0 -0
- httptrading/broker/base.py +151 -0
- httptrading/broker/futu.py +418 -0
- httptrading/broker/interactive_brokers.py +318 -0
- httptrading/broker/longbridge.py +380 -0
- httptrading/broker/tiger.py +347 -0
- httptrading/http_server.py +295 -0
- httptrading/model.py +174 -0
- httptrading/tool/__init__.py +0 -0
- httptrading/tool/leaky_bucket.py +87 -0
- httptrading/tool/locate.py +85 -0
- httptrading/tool/time.py +77 -0
- httptrading-1.0.0.dist-info/METADATA +538 -0
- httptrading-1.0.0.dist-info/RECORD +18 -0
- httptrading-1.0.0.dist-info/WHEEL +5 -0
- httptrading-1.0.0.dist-info/licenses/LICENSE +21 -0
- httptrading-1.0.0.dist-info/top_level.txt +1 -0
httptrading/model.py
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
import enum
|
2
|
+
from datetime import datetime
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
|
5
|
+
|
6
|
+
class TradeType(enum.Enum):
|
7
|
+
Securities = enum.auto()
|
8
|
+
Cryptocurrencies = enum.auto()
|
9
|
+
|
10
|
+
|
11
|
+
class Unit(enum.Enum):
|
12
|
+
Share = enum.auto()
|
13
|
+
RoundLot = enum.auto()
|
14
|
+
Satoshi = enum.auto()
|
15
|
+
|
16
|
+
|
17
|
+
class OrderType(enum.Enum):
|
18
|
+
"""
|
19
|
+
订单的类型
|
20
|
+
"""
|
21
|
+
Limit = enum.auto() # 限价单
|
22
|
+
Market = enum.auto() # 市价单
|
23
|
+
|
24
|
+
|
25
|
+
class TimeInForce(enum.Enum):
|
26
|
+
"""
|
27
|
+
订单的有效期, 一般的交易通道均支持当日有效和取消前有效
|
28
|
+
"""
|
29
|
+
DAY = enum.auto()
|
30
|
+
GTC = enum.auto()
|
31
|
+
|
32
|
+
|
33
|
+
class Lifecycle(enum.Enum):
|
34
|
+
"""
|
35
|
+
订单交易时段
|
36
|
+
"""
|
37
|
+
RTH = enum.auto() # 盘中
|
38
|
+
ETH = enum.auto() # 盘中 + 盘前盘后
|
39
|
+
OVERNIGHT = enum.auto() # 仅夜盘
|
40
|
+
|
41
|
+
|
42
|
+
class UnifiedStatus(enum.Enum):
|
43
|
+
UNKNOWN = enum.auto() # 已知信息不能映射到的状态
|
44
|
+
OVERNIGHT = enum.auto() # 夜盘
|
45
|
+
PRE_HOURS = enum.auto() # 盘前
|
46
|
+
RTH = enum.auto() # 正常交易时段
|
47
|
+
REST = enum.auto() # 休市
|
48
|
+
AFTER_HOURS = enum.auto() # 盘后
|
49
|
+
CLOSED = enum.auto() # 收盘
|
50
|
+
|
51
|
+
|
52
|
+
@dataclass(frozen=True)
|
53
|
+
class Contract:
|
54
|
+
"""
|
55
|
+
Contract 定义了交易品种的精确描述.
|
56
|
+
根据交易种类, 区分为证券和加密货币;
|
57
|
+
根据 ticker 设置交易标的的代码;
|
58
|
+
对于支持多个市场的交易通道, 例如证券, 需要额外提供 region 加以区分标的的所属市场.
|
59
|
+
"""
|
60
|
+
trade_type: TradeType
|
61
|
+
ticker: str
|
62
|
+
region: str
|
63
|
+
|
64
|
+
@property
|
65
|
+
def unique_pair(self):
|
66
|
+
return self.trade_type, self.ticker, self.region,
|
67
|
+
|
68
|
+
def __hash__(self):
|
69
|
+
return self.unique_pair.__hash__()
|
70
|
+
|
71
|
+
def __eq__(self, other):
|
72
|
+
if isinstance(other, Contract):
|
73
|
+
return self.unique_pair == other.unique_pair
|
74
|
+
return False
|
75
|
+
|
76
|
+
|
77
|
+
@dataclass(frozen=True)
|
78
|
+
class Position:
|
79
|
+
broker: str
|
80
|
+
broker_display: str
|
81
|
+
contract: Contract
|
82
|
+
unit: Unit
|
83
|
+
currency: str
|
84
|
+
qty: int
|
85
|
+
|
86
|
+
|
87
|
+
@dataclass(frozen=True)
|
88
|
+
class Cash:
|
89
|
+
currency: str
|
90
|
+
amount: float
|
91
|
+
|
92
|
+
|
93
|
+
@dataclass(frozen=True)
|
94
|
+
class MarketStatus:
|
95
|
+
region: str
|
96
|
+
origin_status: str
|
97
|
+
unified_status: UnifiedStatus
|
98
|
+
|
99
|
+
|
100
|
+
@dataclass(frozen=True)
|
101
|
+
class Quote:
|
102
|
+
contract: Contract
|
103
|
+
currency: str
|
104
|
+
is_tradable: bool
|
105
|
+
latest: float
|
106
|
+
pre_close: float
|
107
|
+
open_price: float
|
108
|
+
high_price: float
|
109
|
+
low_price: float
|
110
|
+
time: datetime
|
111
|
+
|
112
|
+
|
113
|
+
@dataclass(frozen=True)
|
114
|
+
class Order:
|
115
|
+
order_id: str
|
116
|
+
currency: str
|
117
|
+
qty: int
|
118
|
+
filled_qty: int = field(default=0)
|
119
|
+
avg_price: float = field(default=0.0)
|
120
|
+
error_reason: str = field(default='')
|
121
|
+
is_canceled: bool = field(default=False)
|
122
|
+
|
123
|
+
@property
|
124
|
+
def is_filled(self) -> bool:
|
125
|
+
is_filled = False
|
126
|
+
if self.filled_qty >= self.qty:
|
127
|
+
is_filled = True
|
128
|
+
return is_filled
|
129
|
+
|
130
|
+
@property
|
131
|
+
def is_completed(self) -> bool:
|
132
|
+
is_completed = False
|
133
|
+
if self.filled_qty >= self.qty:
|
134
|
+
is_completed = True
|
135
|
+
elif self.is_canceled:
|
136
|
+
is_completed = True
|
137
|
+
elif self.error_reason:
|
138
|
+
is_completed = True
|
139
|
+
return is_completed
|
140
|
+
|
141
|
+
|
142
|
+
@dataclass(frozen=True)
|
143
|
+
class DetectPkg:
|
144
|
+
"""
|
145
|
+
如果需要在 BaseBroker 对象创建时检测相关的 sdk 包是否可以导入,
|
146
|
+
这个结构用于在 @broker_register 装饰器的参数中说明需要导入的模块名以及对应包的安装名.
|
147
|
+
"""
|
148
|
+
pkg_name: str
|
149
|
+
import_name: str
|
150
|
+
|
151
|
+
|
152
|
+
@dataclass(frozen=True)
|
153
|
+
class BrokerMeta:
|
154
|
+
name: str
|
155
|
+
display: str
|
156
|
+
detect_package: DetectPkg = None
|
157
|
+
|
158
|
+
|
159
|
+
__all__ = [
|
160
|
+
'TradeType',
|
161
|
+
'Unit',
|
162
|
+
'OrderType',
|
163
|
+
'TimeInForce',
|
164
|
+
'Lifecycle',
|
165
|
+
'UnifiedStatus',
|
166
|
+
'Contract',
|
167
|
+
'Position',
|
168
|
+
'Cash',
|
169
|
+
'MarketStatus',
|
170
|
+
'Quote',
|
171
|
+
'Order',
|
172
|
+
'DetectPkg',
|
173
|
+
'BrokerMeta',
|
174
|
+
]
|
File without changes
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import math
|
2
|
+
import asyncio
|
3
|
+
from threading import Lock
|
4
|
+
from httptrading.tool.time import TimeTools
|
5
|
+
|
6
|
+
|
7
|
+
class LeakyBucket:
|
8
|
+
def __init__(self, leak_rate: float = 10, capacity: int = None, used_tokens: int = None):
|
9
|
+
assert isinstance(leak_rate, (int, float, ))
|
10
|
+
assert leak_rate > 0
|
11
|
+
|
12
|
+
if capacity is None:
|
13
|
+
capacity = 1
|
14
|
+
assert isinstance(capacity, int)
|
15
|
+
assert capacity > 0
|
16
|
+
|
17
|
+
if used_tokens is None:
|
18
|
+
used_tokens = 0
|
19
|
+
assert isinstance(used_tokens, int)
|
20
|
+
assert used_tokens >= 0
|
21
|
+
|
22
|
+
assert capacity >= used_tokens
|
23
|
+
self._capacity = capacity
|
24
|
+
self._used_tokens = used_tokens
|
25
|
+
self._leak_rate = float(leak_rate)
|
26
|
+
self._last_time = TimeTools.utc_now().timestamp()
|
27
|
+
self._lock = Lock()
|
28
|
+
self._alock = asyncio.Lock()
|
29
|
+
|
30
|
+
async def __aenter__(self):
|
31
|
+
await self.consume_async()
|
32
|
+
|
33
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
34
|
+
pass
|
35
|
+
|
36
|
+
def __enter__(self):
|
37
|
+
self.consume()
|
38
|
+
|
39
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
40
|
+
pass
|
41
|
+
|
42
|
+
@property
|
43
|
+
def used_tokens(self):
|
44
|
+
return self._get_used_tokens(rewrite_tokens=False)
|
45
|
+
|
46
|
+
@property
|
47
|
+
def available_tokens(self):
|
48
|
+
return self._capacity - self.used_tokens
|
49
|
+
|
50
|
+
def _get_used_tokens(self, rewrite_tokens=False):
|
51
|
+
now = TimeTools.utc_now().timestamp()
|
52
|
+
delta = self._leak_rate / 60.0 * (now - self._last_time)
|
53
|
+
delta = math.floor(delta)
|
54
|
+
new_used_tokens = max(0, self._used_tokens - delta)
|
55
|
+
if rewrite_tokens:
|
56
|
+
self._used_tokens = new_used_tokens
|
57
|
+
return new_used_tokens
|
58
|
+
|
59
|
+
def _consume(self):
|
60
|
+
while True:
|
61
|
+
with self._lock:
|
62
|
+
if 1 + self._get_used_tokens(rewrite_tokens=True) <= self._capacity:
|
63
|
+
self._used_tokens += 1
|
64
|
+
self._last_time = TimeTools.utc_now().timestamp()
|
65
|
+
break
|
66
|
+
last_time = self._last_time
|
67
|
+
now = TimeTools.utc_now().timestamp()
|
68
|
+
secs = last_time + 60.0 / self._leak_rate - now
|
69
|
+
TimeTools.sleep(secs=secs)
|
70
|
+
|
71
|
+
def consume(self):
|
72
|
+
self._consume()
|
73
|
+
|
74
|
+
async def consume_async(self):
|
75
|
+
while True:
|
76
|
+
async with self._alock:
|
77
|
+
if 1 + self._get_used_tokens(rewrite_tokens=True) <= self._capacity:
|
78
|
+
self._used_tokens += 1
|
79
|
+
self._last_time = TimeTools.utc_now().timestamp()
|
80
|
+
break
|
81
|
+
last_time = self._last_time
|
82
|
+
now = TimeTools.utc_now().timestamp()
|
83
|
+
secs = last_time + 60.0 / self._leak_rate - now
|
84
|
+
await asyncio.sleep(secs)
|
85
|
+
|
86
|
+
|
87
|
+
__all__ = ["LeakyBucket", ]
|
@@ -0,0 +1,85 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
import sys
|
4
|
+
import pkgutil
|
5
|
+
import importlib
|
6
|
+
|
7
|
+
|
8
|
+
class LocateTools:
|
9
|
+
"""
|
10
|
+
文件系统定位工具
|
11
|
+
帮助在 $PATH, IDE 和 $PYTHONPATH 等位置中搜索需要的文件和目录
|
12
|
+
"""
|
13
|
+
ENVIRONMENT_KEY_PYTHON_PATH = 'PYTHONPATH'
|
14
|
+
ENVIRONMENT_KEY_IDE_ROOTS = 'IDE_PROJECT_ROOTS'
|
15
|
+
|
16
|
+
@classmethod
|
17
|
+
def _build_path_list(cls) -> list:
|
18
|
+
path_list = list()
|
19
|
+
python_path_list = list()
|
20
|
+
if cls.ENVIRONMENT_KEY_PYTHON_PATH in os.environ:
|
21
|
+
python_path_list = os.environ[cls.ENVIRONMENT_KEY_PYTHON_PATH].split(":")
|
22
|
+
if cls.ENVIRONMENT_KEY_IDE_ROOTS in os.environ:
|
23
|
+
path_list.append(os.environ[cls.ENVIRONMENT_KEY_IDE_ROOTS])
|
24
|
+
path_list.extend(python_path_list)
|
25
|
+
path_list.extend(sys.path)
|
26
|
+
return path_list
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
def locate_file(cls, file_path) -> str:
|
30
|
+
path_list = cls._build_path_list()
|
31
|
+
for path in path_list:
|
32
|
+
detect_path = os.path.join(path, file_path)
|
33
|
+
if os.path.isfile(detect_path):
|
34
|
+
return detect_path
|
35
|
+
raise FileNotFoundError
|
36
|
+
|
37
|
+
@classmethod
|
38
|
+
def locate_folder(cls, folder_path) -> str:
|
39
|
+
path_list = cls._build_path_list()
|
40
|
+
for path in path_list:
|
41
|
+
detect_path = os.path.join(path, folder_path)
|
42
|
+
if os.path.isdir(detect_path):
|
43
|
+
return detect_path
|
44
|
+
raise FileNotFoundError
|
45
|
+
|
46
|
+
@classmethod
|
47
|
+
def scan_folder(cls, folder_path, re_search: str) -> list[str]:
|
48
|
+
result = list()
|
49
|
+
for root, dirs, files in os.walk(folder_path):
|
50
|
+
for file in files:
|
51
|
+
if re.search(re_search, file.lower()):
|
52
|
+
path = os.path.join(root, file)
|
53
|
+
result.append(path)
|
54
|
+
return result
|
55
|
+
|
56
|
+
@classmethod
|
57
|
+
def read_file(cls, path: str) -> None | str:
|
58
|
+
if os.path.exists(path):
|
59
|
+
with open(path, 'r', encoding='utf8') as f:
|
60
|
+
text = f.read()
|
61
|
+
return text
|
62
|
+
else:
|
63
|
+
return None
|
64
|
+
|
65
|
+
@classmethod
|
66
|
+
def write_file(cls, path: str, text: str, mode: str = 'w'):
|
67
|
+
args = dict(
|
68
|
+
file=path,
|
69
|
+
mode=mode,
|
70
|
+
)
|
71
|
+
if mode == 'w':
|
72
|
+
args |= dict(encoding='utf8')
|
73
|
+
with open(**args) as f:
|
74
|
+
f.write(text)
|
75
|
+
|
76
|
+
@classmethod
|
77
|
+
def discover_plugins(cls, package_name):
|
78
|
+
package = importlib.import_module(package_name)
|
79
|
+
for _, module_name, is_pkg in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
80
|
+
if is_pkg:
|
81
|
+
continue
|
82
|
+
importlib.import_module(module_name)
|
83
|
+
|
84
|
+
|
85
|
+
__all__ = ['LocateTools', ]
|
httptrading/tool/time.py
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
import re
|
2
|
+
import time
|
3
|
+
from zoneinfo import ZoneInfo
|
4
|
+
from datetime import datetime, timedelta, UTC, timezone
|
5
|
+
import humanize
|
6
|
+
|
7
|
+
|
8
|
+
class TimeTools:
|
9
|
+
@classmethod
|
10
|
+
def _get_utc(cls):
|
11
|
+
return datetime.now(timezone.utc)
|
12
|
+
|
13
|
+
@classmethod
|
14
|
+
def utc_now(cls):
|
15
|
+
return cls._get_utc()
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
def date_to_ymd(cls, date: datetime, join=True) -> str:
|
19
|
+
if join:
|
20
|
+
return date.strftime('%Y-%m-%d')
|
21
|
+
else:
|
22
|
+
return date.strftime('%Y%m%d')
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def format_ymd(cls, s: str | int) -> str:
|
26
|
+
if s is None:
|
27
|
+
return '--'
|
28
|
+
elif isinstance(s, int):
|
29
|
+
s = str(s)
|
30
|
+
if m := re.match(r'^(\d{4})(\d{2})(\d{2})$', s):
|
31
|
+
yyyy, mm, dd = m.groups()
|
32
|
+
return f'{yyyy}-{mm}-{dd}'
|
33
|
+
elif re.match(r'^(\d{4})-(\d{2})-(\d{2})$', s):
|
34
|
+
return s
|
35
|
+
else:
|
36
|
+
return s
|
37
|
+
|
38
|
+
@classmethod
|
39
|
+
def timedelta(cls, date: datetime, days=0, minutes=0, seconds=0):
|
40
|
+
return date + timedelta(days=days, minutes=minutes, seconds=seconds)
|
41
|
+
|
42
|
+
@classmethod
|
43
|
+
def from_timestamp(cls, timestamp, tz: str = None) -> datetime:
|
44
|
+
"""
|
45
|
+
秒单位时间戳
|
46
|
+
:param timestamp:
|
47
|
+
:param tz:
|
48
|
+
:return:
|
49
|
+
"""
|
50
|
+
tz = tz if tz else cls.current_tz()
|
51
|
+
date = datetime.fromtimestamp(timestamp, UTC)
|
52
|
+
return date.astimezone(ZoneInfo(tz))
|
53
|
+
|
54
|
+
@classmethod
|
55
|
+
def from_params(cls, year: int, month: int, day: int, hour: int, minute: int, second: int, tz: str):
|
56
|
+
return datetime(
|
57
|
+
year=year,
|
58
|
+
month=month,
|
59
|
+
day=day,
|
60
|
+
hour=hour,
|
61
|
+
minute=minute,
|
62
|
+
second=second,
|
63
|
+
tzinfo=ZoneInfo(tz),
|
64
|
+
)
|
65
|
+
|
66
|
+
@classmethod
|
67
|
+
def sleep(cls, secs: float):
|
68
|
+
if secs <= 0.0:
|
69
|
+
return
|
70
|
+
time.sleep(secs)
|
71
|
+
|
72
|
+
@classmethod
|
73
|
+
def precisedelta(cls, value, minimum_unit='seconds', suppress=(), format='%0.2f'):
|
74
|
+
return humanize.precisedelta(value=value, minimum_unit=minimum_unit, suppress=suppress, format=format)
|
75
|
+
|
76
|
+
|
77
|
+
__all__ = ['TimeTools', ]
|