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/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', ]
@@ -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', ]