hyperquant 0.61__tar.gz → 0.63__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.
Files changed (46) hide show
  1. {hyperquant-0.61 → hyperquant-0.63}/PKG-INFO +1 -1
  2. {hyperquant-0.61 → hyperquant-0.63}/pyproject.toml +1 -1
  3. hyperquant-0.63/src/hyperquant/broker/lbank.py +103 -0
  4. hyperquant-0.63/src/hyperquant/broker/models/lbank.py +222 -0
  5. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/broker/ws.py +7 -0
  6. hyperquant-0.63/tests/test_edgex.py +27 -0
  7. hyperquant-0.63/tests/test_lbank.py +85 -0
  8. {hyperquant-0.61 → hyperquant-0.63}/uv.lock +1 -1
  9. hyperquant-0.61/tests/test_edgex.py +0 -26
  10. hyperquant-0.61/tests/test_lbank.py +0 -34
  11. {hyperquant-0.61 → hyperquant-0.63}/.gitignore +0 -0
  12. {hyperquant-0.61 → hyperquant-0.63}/.python-version +0 -0
  13. {hyperquant-0.61 → hyperquant-0.63}/README.md +0 -0
  14. {hyperquant-0.61 → hyperquant-0.63}/apis.json +0 -0
  15. {hyperquant-0.61 → hyperquant-0.63}/backtest_tmp.py +0 -0
  16. {hyperquant-0.61 → hyperquant-0.63}/data/alpine_smoke.log +0 -0
  17. {hyperquant-0.61 → hyperquant-0.63}/data/logs/notikit.log +0 -0
  18. {hyperquant-0.61 → hyperquant-0.63}/data/logs/test_order_sync.log +0 -0
  19. {hyperquant-0.61 → hyperquant-0.63}/data/records_swap.csv +0 -0
  20. {hyperquant-0.61 → hyperquant-0.63}/data/records_swapc.csv +0 -0
  21. {hyperquant-0.61 → hyperquant-0.63}/deals.json +0 -0
  22. {hyperquant-0.61 → hyperquant-0.63}/pub.sh +0 -0
  23. {hyperquant-0.61 → hyperquant-0.63}/requirements-dev.lock +0 -0
  24. {hyperquant-0.61 → hyperquant-0.63}/requirements.lock +0 -0
  25. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/__init__.py +0 -0
  26. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/broker/auth.py +0 -0
  27. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/broker/edgex.py +0 -0
  28. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/broker/hyperliquid.py +0 -0
  29. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/broker/lib/hpstore.py +0 -0
  30. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  31. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/broker/models/edgex.py +0 -0
  32. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  33. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/broker/models/ourbit.py +0 -0
  34. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/broker/ourbit.py +0 -0
  35. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/core.py +0 -0
  36. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/datavison/_util.py +0 -0
  37. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/datavison/binance.py +0 -0
  38. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/datavison/coinglass.py +0 -0
  39. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/datavison/okx.py +0 -0
  40. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/db.py +0 -0
  41. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/draw.py +0 -0
  42. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/logkit.py +0 -0
  43. {hyperquant-0.61 → hyperquant-0.63}/src/hyperquant/notikit.py +0 -0
  44. {hyperquant-0.61 → hyperquant-0.63}/tests/test_draw.py +0 -0
  45. {hyperquant-0.61 → hyperquant-0.63}/tests/test_ourbit.py +0 -0
  46. {hyperquant-0.61 → hyperquant-0.63}/tests/tmp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.61
3
+ Version: 0.63
4
4
  Summary: A minimal yet hyper-efficient backtesting framework for quantitative trading
5
5
  Project-URL: Homepage, https://github.com/yourusername/hyperquant
6
6
  Project-URL: Issues, https://github.com/yourusername/hyperquant/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperquant"
3
- version = "0.61"
3
+ version = "0.63"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import itertools
5
+ import json
6
+ import logging
7
+ import time
8
+ import zlib
9
+ from typing import Iterable, Literal
10
+
11
+ import pybotters
12
+
13
+ from .models.lbank import LbankDataStore
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # https://ccapi.rerrkvifj.com 似乎是spot的api
18
+ # https://uuapi.rerrkvifj.com 似乎是合约的api
19
+
20
+
21
+ class Lbank:
22
+ """LBank public market-data client (REST + WS)."""
23
+
24
+ def __init__(
25
+ self,
26
+ client: pybotters.Client,
27
+ *,
28
+ front_api: str | None = None,
29
+ rest_api: str | None = None,
30
+ ws_url: str | None = None,
31
+ ) -> None:
32
+ self.client = client
33
+ self.store = LbankDataStore()
34
+ self.front_api = front_api or "https://uuapi.rerrkvifj.com"
35
+ self.rest_api = rest_api or "https://api.lbkex.com"
36
+ self.ws_url = ws_url or "wss://uuws.rerrkvifj.com/ws/v3"
37
+ self._req_id = itertools.count(int(time.time() * 1000))
38
+ self._ws_app = None
39
+
40
+ async def __aenter__(self) -> "Lbank":
41
+ await self.update("detail")
42
+ return self
43
+
44
+ async def __aexit__(self, exc_type, exc, tb) -> None:
45
+ pass
46
+
47
+ async def update(self, update_type: Literal["detail", "ticker", "all"]) -> list[dict]:
48
+ all_urls = [f"{self.front_api}/cfd/agg/v1/instrument"]
49
+ url_map = {"detail": [all_urls[0]], "all": all_urls}
50
+
51
+
52
+ try:
53
+ urls = url_map[update_type]
54
+ except KeyError:
55
+ raise ValueError(f"update_type err: {update_type}")
56
+
57
+ # await self.store.initialize(*(self.client.get(url) for url in urls))
58
+ if update_type == "detail" or update_type == "all":
59
+ await self.store.initialize(
60
+ self.client.post(
61
+ all_urls[0],
62
+ json={"ProductGroup": "SwapU"},
63
+ headers={"source": "4", "versionflage": "true"},
64
+ )
65
+ )
66
+
67
+
68
+ async def sub_orderbook(self, symbols: list[str], limit: int | None = None) -> None:
69
+ """订阅指定交易对的订单簿(遵循 LBank 协议)。
70
+ """
71
+
72
+ send_jsons = []
73
+ y = 3000000001
74
+ if limit:
75
+ self.store.book.limit = limit
76
+
77
+ for symbol in symbols:
78
+
79
+ info = self.store.detail.get({"symbol": symbol})
80
+ if not info:
81
+ raise ValueError(f"Unknown LBank symbol: {symbol}")
82
+
83
+ tick_size = info['tick_size']
84
+ sub_i = symbol + "_" + str(tick_size) + "_25"
85
+ send_jsons.append(
86
+ {
87
+ "x": 3,
88
+ "y": str(y),
89
+ "a": {"i": sub_i},
90
+ "z": 1,
91
+ }
92
+ )
93
+
94
+ self.store.register_book_channel(str(y), symbol)
95
+ y += 1
96
+
97
+ wsapp = self.client.ws_connect(
98
+ self.ws_url,
99
+ send_json=send_jsons,
100
+ hdlr_bytes=self.store.onmessage,
101
+ )
102
+
103
+ await wsapp._event.wait()
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ from typing import Any, Awaitable, TYPE_CHECKING
7
+
8
+ from aiohttp import ClientResponse
9
+ from pybotters.store import DataStore, DataStoreCollection
10
+
11
+ if TYPE_CHECKING:
12
+ from pybotters.typedefs import Item
13
+ from pybotters.ws import ClientWebSocketResponse
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _accuracy_to_step(accuracy: int | str | None) -> str:
19
+ try:
20
+ n = int(accuracy) if accuracy is not None else 0
21
+ except (TypeError, ValueError): # pragma: no cover - defensive guard
22
+ n = 0
23
+ if n <= 0:
24
+ return "1"
25
+ return "0." + "0" * (n - 1) + "1"
26
+
27
+
28
+ class Book(DataStore):
29
+ """LBank order book store parsed from the depth channel."""
30
+
31
+ _KEYS = ["id", "S", "p"]
32
+
33
+ def _init(self) -> None:
34
+ self.limit: int | None = None
35
+ self.symbol_map: dict[str, str] = {}
36
+
37
+
38
+ def _format_levels(
39
+ self,
40
+ levels: list[list[Any]],
41
+ channel_id: str | None,
42
+ side: str,
43
+ symbol: str | None,
44
+ ) -> list[dict[str, Any]]:
45
+ formatted: list[dict[str, Any]] = []
46
+ for level in levels:
47
+ if len(level) < 2:
48
+ continue
49
+ try:
50
+ price = float(level[0])
51
+ size = float(level[1])
52
+ except (TypeError, ValueError): # pragma: no cover - defensive guard
53
+ continue
54
+ formatted.append(
55
+ {
56
+ "id": channel_id,
57
+ "S": side,
58
+ "p": price,
59
+ "q": size,
60
+ "s": symbol,
61
+ }
62
+ )
63
+ if self.limit is not None:
64
+ formatted = formatted[: self.limit]
65
+ return formatted
66
+
67
+ def _on_message(self, msg: Any) -> None:
68
+
69
+ data = json.loads(msg)
70
+
71
+ if not data:
72
+ return
73
+
74
+ channel_id = None
75
+ if data.get("y") is not None:
76
+ channel_id = str(data["y"])
77
+
78
+ symbol = None
79
+ if channel_id:
80
+ symbol = self.symbol_map.get(channel_id)
81
+ if symbol is None and data.get("i"):
82
+ symbol = self.symbol_map.get(str(data["i"]))
83
+
84
+ bids = self._format_levels(data.get("b", []), channel_id, "b", symbol)
85
+ asks = self._format_levels(data.get("s", []), channel_id, "a", symbol)
86
+
87
+ if not (bids or asks):
88
+ return
89
+
90
+ if channel_id is not None:
91
+ self._find_and_delete({"id": channel_id})
92
+ self._insert(bids + asks)
93
+
94
+
95
+ class Detail(DataStore):
96
+ """Futures instrument metadata store obtained from the futures instrument endpoint."""
97
+
98
+ _KEYS = ["symbol"]
99
+
100
+ def _transform(self, entry: dict[str, Any]) -> dict[str, Any] | None:
101
+ try:
102
+ instrument:dict = entry["instrument"]
103
+ fee:dict = entry["fee"]
104
+ market_data:dict = entry["marketData"]
105
+ except (KeyError, TypeError):
106
+ return None
107
+ return {
108
+ "symbol": instrument.get("instrumentID"),
109
+ "instrument_name": instrument.get("instrumentName"),
110
+ "base_currency": instrument.get("baseCurrency"),
111
+ "price_currency": instrument.get("priceCurrency"),
112
+ "min_order_volume": instrument.get("minOrderVolume"),
113
+ "max_order_volume": instrument.get("maxOrderVolume"),
114
+ "tick_size": instrument.get("priceTick"),
115
+ "step_size": instrument.get("volumeTick"),
116
+ "maker_fee": fee.get("makerOpenFeeRate"),
117
+ "taker_fee": fee.get("takerOpenFeeRate"),
118
+ "last_price": market_data.get("lastPrice"),
119
+ "amount24": market_data.get("turnover24"),
120
+ }
121
+
122
+ def _onresponse(self, data: list[dict[str, Any]] | dict[str, Any] | None) -> None:
123
+ if not data:
124
+ self._clear()
125
+ return
126
+ entries = data
127
+ if isinstance(data, dict): # pragma: no cover - defensive guard
128
+ entries = data.get("data") or []
129
+ items: list[dict[str, Any]] = []
130
+ for entry in entries or []:
131
+ transformed = self._transform(entry)
132
+ if transformed:
133
+ items.append(transformed)
134
+ if not items:
135
+ self._clear()
136
+ return
137
+ self._clear()
138
+ self._insert(items)
139
+
140
+
141
+ class LbankDataStore(DataStoreCollection):
142
+ """Aggregates book/detail stores for the LBank public feed."""
143
+
144
+ def _init(self) -> None:
145
+ self._create("book", datastore_class=Book)
146
+ self._create("detail", datastore_class=Detail)
147
+ self._channel_to_symbol: dict[str, str] = {}
148
+
149
+ @property
150
+ def book(self) -> Book:
151
+ """
152
+ 订单簿(Order Book)数据流,按订阅ID(channel_id)索引。
153
+
154
+ 此属性表示通过深度频道(depth channel)接收到的订单簿快照和增量更新,数据结构示例如下:
155
+
156
+ Data structure:
157
+ [
158
+ {
159
+ "id": <channel_id>,
160
+ "S": "b" 或 "a", # "b" 表示买单,"a" 表示卖单
161
+ "p": <价格>,
162
+ "q": <数量>,
163
+ "s": <标准化交易对符号>
164
+ },
165
+ ...
166
+ ]
167
+
168
+ 通过本属性可以获取当前 LBank 订单簿的最新状态,便于后续行情分析和撮合逻辑处理。
169
+ """
170
+ return self._get("book")
171
+
172
+ @property
173
+ def detail(self) -> Detail:
174
+ """
175
+
176
+ _KEYS = ["symbol"]
177
+
178
+ 期货合约详情元数据流。
179
+
180
+ 此属性表示通过期货合约接口获取的合约详情,包括合约ID、合约名称、基础币种、计价币种、最小/最大下单量、价格跳动、交易量跳动、maker/taker手续费率、最新价和24小时成交额等信息。
181
+
182
+ Data structure:
183
+ [
184
+ {
185
+ "symbol": "BTCUSDT", # 合约ID
186
+ "instrument_name": "BTCUSDT", # 合约名称
187
+ "base_currency": "BTC", # 基础币种
188
+ "price_currency": "USDT", # 计价币种
189
+ "min_order_volume": "0.0001", # 最小下单量
190
+ "max_order_volume": "600.0", # 最大下单量
191
+ "tick_size": "0.1", # 最小价格变动单位
192
+ "step_size": "0.0001", # 最小数量变动单位
193
+ "maker_fee": "0.0002", # Maker 手续费率
194
+ "taker_fee": "0.0006", # Taker 手续费率
195
+ "last_price": "117025.5", # 最新价
196
+ "amount24": "807363493.97579747" # 24小时成交额
197
+ },
198
+ ...
199
+ ]
200
+
201
+ 通过本属性可以获取所有支持的期货合约元数据,便于下单参数校验和行情展示。
202
+ """
203
+ return self._get("detail")
204
+
205
+
206
+ def register_book_channel(self, channel_id: str, symbol: str, *, raw_symbol: str | None = None) -> None:
207
+ if channel_id is not None:
208
+ self.book.symbol_map[str(channel_id)] = symbol
209
+ if raw_symbol:
210
+ self.book.symbol_map[str(raw_symbol)] = symbol
211
+
212
+
213
+ async def initialize(self, *aws: Awaitable[ClientResponse]) -> None:
214
+ for fut in asyncio.as_completed(aws):
215
+ res = await fut
216
+ data = await res.json()
217
+ if res.url.path == "/cfd/agg/v1/instrument":
218
+ self.detail._onresponse(data)
219
+
220
+
221
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
222
+ self.book._on_message(msg)
@@ -26,9 +26,16 @@ class Heartbeat:
26
26
  await ws.send_json({"type": "ping", "time": now})
27
27
  await asyncio.sleep(20.0)
28
28
 
29
+ @staticmethod
30
+ async def lbank(ws: ClientWebSocketResponse):
31
+ while not ws.closed:
32
+ await ws.send_str('ping')
33
+ await asyncio.sleep(6)
34
+
29
35
  pybotters.ws.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
30
36
  pybotters.ws.HeartbeatHosts.items['www.ourbit.com'] = Heartbeat.ourbit_spot
31
37
  pybotters.ws.HeartbeatHosts.items['quote.edgex.exchange'] = Heartbeat.edgex
38
+ pybotters.ws.HeartbeatHosts.items['uuws.rerrkvifj.com'] = Heartbeat.lbank
32
39
 
33
40
  class WssAuth:
34
41
  @staticmethod
@@ -0,0 +1,27 @@
1
+ from hyperquant.broker.edgex import Edgex
2
+ import pybotters
3
+ async def main():
4
+ async with pybotters.Client() as client:
5
+ async with Edgex(client) as broker:
6
+ # print(broker.store.detail.find({
7
+ # 'contractName': 'BTCUSD',
8
+ # }))
9
+
10
+ await broker.sub_orderbook(symbols=['BTCUSD'])
11
+ broker.store.book.limit = 1
12
+ while True:
13
+ print(broker.store.book.find({"S": 'b'}))
14
+ await asyncio.sleep(1)
15
+
16
+ # await broker.sub_ticker(all_contracts=True, periodic=True)
17
+ # broker.store.book.limit = 1
18
+ # while True:
19
+ # print(broker.store.ticker.find({
20
+ # 's':'BTCUSD',
21
+ # }))
22
+ # await asyncio.sleep(1)
23
+
24
+
25
+ if __name__ == "__main__":
26
+ import asyncio
27
+ asyncio.run(main())
@@ -0,0 +1,85 @@
1
+ import asyncio
2
+ import zlib
3
+ from aiohttp import ClientWebSocketResponse
4
+ import pybotters
5
+
6
+
7
+ def callback(msg, ws: ClientWebSocketResponse = None):
8
+ # print("Received message:", msg)
9
+ decompressed = zlib.decompress(msg, 16 + zlib.MAX_WBITS)
10
+ text = decompressed.decode("utf-8")
11
+ print(f"Decoded text: {text}")
12
+
13
+ def callback2(msg, ws: ClientWebSocketResponse = None):
14
+ # print("Received message:", msg)
15
+ print(str(msg))
16
+
17
+
18
+ async def main():
19
+ async with pybotters.Client() as client:
20
+ # webData2
21
+ client.ws_connect(
22
+ "wss://ccws.rerrkvifj.com/ws/V3/",
23
+ send_json={
24
+ "dataType": 3,
25
+ "depth": 200,
26
+ "pair": "arb_usdt",
27
+ "action": "subscribe",
28
+ "subscribe": "depth",
29
+ "msgType": 2,
30
+ "limit": 10,
31
+ "type": 10000,
32
+ },
33
+ hdlr_bytes=callback,
34
+ )
35
+
36
+ while True:
37
+ await asyncio.sleep(1)
38
+
39
+
40
+ async def main2():
41
+ async with pybotters.Client() as client:
42
+ # webData2
43
+ # x 为chanel, y为唯一标识, a为参数, z为版本号
44
+ client.ws_connect(
45
+ "wss://uuws.rerrkvifj.com/ws/v3",
46
+ send_json={"x": 3, "y": "3000000001", "a": {"i": "BTCUSDT_0.1_25"}, "z": 1},
47
+ hdlr_bytes=callback2,
48
+ )
49
+
50
+ while True:
51
+ await asyncio.sleep(1)
52
+
53
+ from hyperquant.broker.lbank import Lbank
54
+
55
+ async def test_broker():
56
+ async with pybotters.Client() as client:
57
+ async with Lbank(client) as lb:
58
+ print(lb.store.detail.find())
59
+
60
+
61
+ async def test_broker_detail():
62
+ async with pybotters.Client() as client:
63
+ data = await client.post(
64
+ "https://uuapi.rerrkvifj.com/cfd/agg/v1/instrument",
65
+ headers={"source": "4", "versionflage": "true"},
66
+ json={
67
+ "ProductGroup": "SwapU"
68
+ }
69
+ )
70
+ res = await data.json()
71
+ print(res)
72
+
73
+ async def test_broker_subbook():
74
+ async with pybotters.Client() as client:
75
+ async with Lbank(client) as lb:
76
+ # 取20个symbol 尝试是否可以订阅成功
77
+ symbols = [item["symbol"] for item in lb.store.detail.find()[:4]]
78
+ await lb.sub_orderbook(symbols, limit=1)
79
+ while True:
80
+ await asyncio.sleep(1)
81
+ print(len(lb.store.book.find()))
82
+
83
+
84
+ if __name__ == "__main__":
85
+ asyncio.run(test_broker_subbook())
@@ -530,7 +530,7 @@ wheels = [
530
530
 
531
531
  [[package]]
532
532
  name = "hyperquant"
533
- version = "0.6"
533
+ version = "0.62"
534
534
  source = { editable = "." }
535
535
  dependencies = [
536
536
  { name = "aiohttp" },
@@ -1,26 +0,0 @@
1
- from hyperquant.broker.edgex import Edgex
2
- import pybotters
3
- async def main():
4
- async with pybotters.Client() as client:
5
- async with Edgex(client) as broker:
6
- # print(broker.store.detail.find({
7
- # 'contractName': 'BTCUSD',
8
- # }))
9
-
10
- # await broker.sub_orderbook(symbols=['BTCUSD'])
11
- # broker.store.book.limit = 1
12
- # while True:
13
- # print(broker.store.book.find({"S": 'b'}))
14
- # await asyncio.sleep(1)
15
-
16
- await broker.sub_ticker(all_contracts=True, periodic=True)
17
- while True:
18
- print(broker.store.ticker.find({
19
- 's':'BTCUSD',
20
- }))
21
- await asyncio.sleep(1)
22
-
23
-
24
- if __name__ == "__main__":
25
- import asyncio
26
- asyncio.run(main())
@@ -1,34 +0,0 @@
1
- import asyncio
2
- import zlib
3
- from aiohttp import ClientWebSocketResponse
4
- import pybotters
5
-
6
- def callback(msg, ws: ClientWebSocketResponse = None):
7
- # print("Received message:", msg)
8
- decompressed = zlib.decompress(msg, 16 + zlib.MAX_WBITS)
9
- text = decompressed.decode("utf-8")
10
- print(f"Decoded text: {text}")
11
-
12
- async def main():
13
- async with pybotters.Client() as client:
14
- # webData2
15
- client.ws_connect(
16
- "wss://ccws.rerrkvifj.com/ws/V3/",
17
- send_json={
18
- "dataType": 3,
19
- "depth": 200,
20
- "pair": "arb_usdt",
21
- "action": "subscribe",
22
- "subscribe": "depth",
23
- "msgType": 2,
24
- "limit": 10,
25
- "type": 10000
26
- },
27
- hdlr_bytes=callback
28
- )
29
-
30
- while True:
31
- await asyncio.sleep(1)
32
-
33
- if __name__ == "__main__":
34
- asyncio.run(main())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes