libfinance 0.1.3__tar.gz → 0.1.4__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 (43) hide show
  1. {libfinance-0.1.3 → libfinance-0.1.4}/PKG-INFO +2 -1
  2. {libfinance-0.1.3 → libfinance-0.1.4}/README.md +1 -0
  3. libfinance-0.1.4/VERSION +1 -0
  4. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/shares.py +37 -1
  5. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/client.py +29 -7
  6. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance.egg-info/PKG-INFO +2 -1
  7. {libfinance-0.1.3 → libfinance-0.1.4}/test/test_client_api_contract.py +28 -0
  8. {libfinance-0.1.3 → libfinance-0.1.4}/test/test_client_reconnect.py +74 -0
  9. libfinance-0.1.3/VERSION +0 -1
  10. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/__init__.py +0 -0
  11. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/__init__.py +0 -0
  12. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/calendar.py +0 -0
  13. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/corporate_actions.py +0 -0
  14. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/exfactor.py +0 -0
  15. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/factors.py +0 -0
  16. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/financials.py +0 -0
  17. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/get_price.py +0 -0
  18. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/index_components.py +0 -0
  19. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/industry.py +0 -0
  20. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/instrument.py +0 -0
  21. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/live_quote.py +0 -0
  22. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/api/theme.py +0 -0
  23. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/subscribe/__init__.py +0 -0
  24. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/subscribe/md_protocol.py +0 -0
  25. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/subscribe/quote_api.py +0 -0
  26. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/utils/__init__.py +0 -0
  27. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/utils/cache.py +0 -0
  28. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/utils/datetime_func.py +0 -0
  29. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/utils/decorators.py +0 -0
  30. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/utils/utils.py +0 -0
  31. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance/utils/validators.py +0 -0
  32. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance.egg-info/SOURCES.txt +0 -0
  33. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance.egg-info/dependency_links.txt +0 -0
  34. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance.egg-info/requires.txt +0 -0
  35. {libfinance-0.1.3 → libfinance-0.1.4}/libfinance.egg-info/top_level.txt +0 -0
  36. {libfinance-0.1.3 → libfinance-0.1.4}/pyproject.toml +0 -0
  37. {libfinance-0.1.3 → libfinance-0.1.4}/setup.cfg +0 -0
  38. {libfinance-0.1.3 → libfinance-0.1.4}/setup.py +0 -0
  39. {libfinance-0.1.3 → libfinance-0.1.4}/test/test_contract.py +0 -0
  40. {libfinance-0.1.3 → libfinance-0.1.4}/test/test_docs_contract.py +0 -0
  41. {libfinance-0.1.3 → libfinance-0.1.4}/test/test_rpc_contract.py +0 -0
  42. {libfinance-0.1.3 → libfinance-0.1.4}/test/test_subscribe_live.py +0 -0
  43. {libfinance-0.1.3 → libfinance-0.1.4}/test/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libfinance
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: libfinance is a modern finance API
5
5
  Description-Content-Type: text/markdown
6
6
  Requires-Dist: lz4
@@ -176,6 +176,7 @@ get_trading_dates("2024-01-01", "2024-01-31", market="us") # 不涉及具体
176
176
  | 日频行情 | CN · US | `get_price` | 开高低收、成交量额、涨跌停价,可复权 |
177
177
  | 复权因子 | CN · US | `get_ex_factor` | 逐除权日的单次及累计因子 |
178
178
  | 股本 | CN · US | `get_shares` | 逐交易日的总股本与流通股本 |
179
+ | 换手率 | CN、US | `get_turnover_rate` | 逐交易日的换手率(未复权成交量 ÷ A 股自由流通股本或美股已发行股数) |
179
180
  | 分红 / 拆股 / 配股 | CN · US | `get_dividends`、`get_splits`、`get_allotments` | 除权事件;配股只有 A 股 |
180
181
  | 分拆 | US | `get_spinoffs` | A 股不产生这类事件 |
181
182
  | 财务报表(PIT) | CN · US | `get_pit_financials_ex` | 按季度取,带修订历史 |
@@ -163,6 +163,7 @@ get_trading_dates("2024-01-01", "2024-01-31", market="us") # 不涉及具体
163
163
  | 日频行情 | CN · US | `get_price` | 开高低收、成交量额、涨跌停价,可复权 |
164
164
  | 复权因子 | CN · US | `get_ex_factor` | 逐除权日的单次及累计因子 |
165
165
  | 股本 | CN · US | `get_shares` | 逐交易日的总股本与流通股本 |
166
+ | 换手率 | CN、US | `get_turnover_rate` | 逐交易日的换手率(未复权成交量 ÷ A 股自由流通股本或美股已发行股数) |
166
167
  | 分红 / 拆股 / 配股 | CN · US | `get_dividends`、`get_splits`、`get_allotments` | 除权事件;配股只有 A 股 |
167
168
  | 分拆 | US | `get_spinoffs` | A 股不产生这类事件 |
168
169
  | 财务报表(PIT) | CN · US | `get_pit_financials_ex` | 按季度取,带修订历史 |
@@ -0,0 +1 @@
1
+ 0.1.4
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- """股本:统一字段,CN 与 US 同一套(后端 shares 数据族的 get_shares_panel)。"""
3
+ """股本:统一字段,CN 与 US 同一套(后端 shares 数据族的 get_shares_panel);以及由它算出的日频换手率。"""
4
4
  from typing import List, Optional, Union
5
5
 
6
6
  import pandas as pd
@@ -71,6 +71,42 @@ def get_shares(
71
71
  return _indexed(frame)
72
72
 
73
73
 
74
+ @export_as_api
75
+ def get_turnover_rate(
76
+ order_book_ids: Union[str, List[str]],
77
+ start_date=None,
78
+ end_date=None,
79
+ ) -> pd.DataFrame:
80
+ r"""获取日频换手率。
81
+
82
+ A 股换手率 = 当日未复权成交量 ÷ 当日生效的自由流通股本(没有时用流通股本);
83
+ 美股换手率 = 当日未复权成交量 ÷ 当日已发行股数。结果是\ **小数**\ (0.01 即 1%)。
84
+ A 股结果与 factors-daybar 的因子(如 ``system/broker-daybar/TURN20``\ )读取的换手率同一口径。
85
+
86
+ :param order_book_ids: 单个代码或代码列表,如 ``"600000.XSHG"``、``"AAPL.US"``
87
+ :param start_date: 开始日期;只给一个日期时取那一天
88
+ :param end_date: 结束日期;两个都省略时各市场分别取最近两周内最后一个有行情的交易日
89
+
90
+ :returns: pandas.DataFrame,以 (order_book_id, date) 为索引,一列 ``turnover_rate``\ 。停牌日没有行;
91
+ 没有股本数据的证券或日子为 NaN。
92
+ """
93
+ order_book_ids = ensure_list_of_string(order_book_ids, "order_book_ids")
94
+ if not order_book_ids:
95
+ raise ValueError("order_book_ids: at least one order book id expected")
96
+ if start_date is not None:
97
+ start_date = to_date_str(start_date)
98
+ if end_date is not None:
99
+ end_date = to_date_str(end_date)
100
+ if start_date is not None and end_date is not None and start_date > end_date:
101
+ raise ValueError(
102
+ "invalid date range: [{!r}, {!r}]".format(start_date, end_date)
103
+ )
104
+ frame = get_client().get_turnover_rate(
105
+ order_book_ids=order_book_ids, start_date=start_date, end_date=end_date,
106
+ )
107
+ return _indexed(frame)
108
+
109
+
74
110
  def _indexed(frame):
75
111
  """服务端的扁平表(order_book_id, date, 字段...)-> (order_book_id, date) 索引;已带索引的原样返回。"""
76
112
  if not isinstance(frame, pd.DataFrame) or not {"order_book_id", "date"} <= set(frame.columns):
@@ -3,6 +3,7 @@ import socket
3
3
  import struct
4
4
  import threading
5
5
  import sys
6
+ from collections import deque
6
7
  from concurrent.futures import Future
7
8
  import time
8
9
 
@@ -142,6 +143,7 @@ class RpcClient:
142
143
  """
143
144
  request_timeout = 300
144
145
  request_attempt_count = 3
146
+ _expired_request_limit = 1024
145
147
 
146
148
  def __init__(self, host='127.0.0.1', port=8080):
147
149
  self.host = host
@@ -153,6 +155,11 @@ class RpcClient:
153
155
  self._id_lock = threading.Lock()
154
156
 
155
157
  self._pending = {}
158
+ # A timed-out request keeps running on the server: the wire protocol has no cancellation.
159
+ # Remember a bounded number of those ids so their eventual answers are recognized as late,
160
+ # rather than reported as a protocol error ("unknown request_id").
161
+ self._expired_requests = set()
162
+ self._expired_request_order = deque()
156
163
  self._pending_lock = threading.Lock()
157
164
 
158
165
  self._reader_thread = None
@@ -191,6 +198,8 @@ class RpcClient:
191
198
  for fut in self._pending.values():
192
199
  fut.set_exception(ConnectionError("Connection closed"))
193
200
  self._pending.clear()
201
+ self._expired_requests.clear()
202
+ self._expired_request_order.clear()
194
203
 
195
204
  if self._reader_thread and self._reader_thread.is_alive():
196
205
  self._reader_thread.join(timeout=2)
@@ -209,10 +218,12 @@ class RpcClient:
209
218
  raise ConnectionError(
210
219
  "Not connected to server {}:{}: {}".format(self.host, self.port, error)) from error
211
220
 
212
- def call_raw(self, function_name, args=None, timeout=10.0):
221
+ def call_raw(self, function_name, args=None, timeout=None):
213
222
  """Send an RPC request and wait synchronously for the raw envelope."""
214
223
  if args is None:
215
224
  args = {}
225
+ if timeout is None:
226
+ timeout = self.request_timeout
216
227
  self._ensure_connected()
217
228
 
218
229
  req_id = self._alloc_id()
@@ -233,12 +244,18 @@ class RpcClient:
233
244
  return future.result(timeout=timeout)
234
245
  except TimeoutError:
235
246
  with self._pending_lock:
236
- self._pending.pop(req_id, None)
247
+ # If the reader won the race and already consumed the answer, there will be no
248
+ # late frame to classify. Only remember ids actually removed here.
249
+ if self._pending.pop(req_id, None) is not None:
250
+ self._expired_requests.add(req_id)
251
+ self._expired_request_order.append(req_id)
252
+ while len(self._expired_request_order) > self._expired_request_limit:
253
+ self._expired_requests.discard(self._expired_request_order.popleft())
237
254
  raise TimeoutError(
238
255
  f"RPC '{function_name}' timeout after {timeout}s (req_id={req_id})"
239
256
  )
240
257
 
241
- def call(self, function_name, args=None, timeout=10.0):
258
+ def call(self, function_name, args=None, timeout=None):
242
259
  """Send an RPC request, auto-unwrap envelope, return result or raise RpcError."""
243
260
  resp = self.call_raw(function_name, args, timeout)
244
261
  return self._unwrap_envelope(resp)
@@ -388,10 +405,13 @@ class RpcClient:
388
405
 
389
406
  with self._pending_lock:
390
407
  future = self._pending.pop(request_id, None)
408
+ expired = request_id in self._expired_requests
409
+ if expired:
410
+ self._expired_requests.remove(request_id)
391
411
 
392
412
  if future:
393
413
  future.set_result(response_data)
394
- else:
414
+ elif not expired:
395
415
  print(
396
416
  f"Received response for unknown request_id={request_id}",
397
417
  file=sys.stderr,
@@ -420,12 +440,14 @@ class RpcClient:
420
440
  try:
421
441
  response = self.call(api_name, kwargs)
422
442
  break
443
+ except TimeoutError:
444
+ # The server keeps executing a timed-out request, so retrying it immediately only
445
+ # duplicates expensive work. Connection failures still use the retry path below.
446
+ raise
423
447
  except Exception as ex:
424
448
  err = ex
425
449
  if attempt_index < self.request_attempt_count - 1:
426
450
  time.sleep(0.6)
427
- except ResponseError as ex:
428
- err = ex
429
451
 
430
452
  if response is None and isinstance(err, Exception):
431
453
  if "TSocket read 0 bytes" in str(err):
@@ -472,4 +494,4 @@ def get_client():
472
494
  f"Client auto-connect to {_DEFAULT_HOST}:{_DEFAULT_PORT} failed: {e}. "
473
495
  "Call init_client(host=..., port=...) explicitly before using libfinance.api.*"
474
496
  ) from e
475
- return _CLIENT
497
+ return _CLIENT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libfinance
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: libfinance is a modern finance API
5
5
  Description-Content-Type: text/markdown
6
6
  Requires-Dist: lz4
@@ -176,6 +176,7 @@ get_trading_dates("2024-01-01", "2024-01-31", market="us") # 不涉及具体
176
176
  | 日频行情 | CN · US | `get_price` | 开高低收、成交量额、涨跌停价,可复权 |
177
177
  | 复权因子 | CN · US | `get_ex_factor` | 逐除权日的单次及累计因子 |
178
178
  | 股本 | CN · US | `get_shares` | 逐交易日的总股本与流通股本 |
179
+ | 换手率 | CN、US | `get_turnover_rate` | 逐交易日的换手率(未复权成交量 ÷ A 股自由流通股本或美股已发行股数) |
179
180
  | 分红 / 拆股 / 配股 | CN · US | `get_dividends`、`get_splits`、`get_allotments` | 除权事件;配股只有 A 股 |
180
181
  | 分拆 | US | `get_spinoffs` | A 股不产生这类事件 |
181
182
  | 财务报表(PIT) | CN · US | `get_pit_financials_ex` | 按季度取,带修订历史 |
@@ -344,6 +344,34 @@ def test_get_shares_takes_the_unified_fields_and_as_of(monkeypatch):
344
344
  shares.get_shares("600000.XSHG", fields=["total"])
345
345
 
346
346
 
347
+ def test_get_turnover_rate_is_indexed_and_checks_its_dates(monkeypatch):
348
+ """financialmetrics answers a flat table; the client indexes it by (order_book_id, date) and sends dates as ISO."""
349
+ from datetime import date
350
+ import pandas as pd
351
+ from libfinance.api import shares
352
+
353
+ calls = []
354
+
355
+ class Client:
356
+ def get_turnover_rate(self, **kwargs):
357
+ calls.append(kwargs)
358
+ return pd.DataFrame([{"order_book_id": "600000.XSHG", "date": "2025-06-03", "turnover_rate": 0.0012},
359
+ {"order_book_id": "000001.XSHE", "date": "2025-06-03", "turnover_rate": None}])
360
+
361
+ monkeypatch.setattr(shares, "get_client", lambda: Client())
362
+ out = shares.get_turnover_rate(["600000.XSHG", "000001.XSHE"], date(2025, 6, 1), "2025-06-30")
363
+ assert calls == [{"order_book_ids": ["600000.XSHG", "000001.XSHE"], "start_date": "2025-06-01",
364
+ "end_date": "2025-06-30"}]
365
+ assert list(out.index.names) == ["order_book_id", "date"] and list(out.columns) == ["turnover_rate"]
366
+ assert out.loc[("600000.XSHG", pd.Timestamp("2025-06-03")), "turnover_rate"] == 0.0012
367
+ shares.get_turnover_rate("600000.XSHG") # no dates: the server picks the latest trading day
368
+ assert calls[-1] == {"order_book_ids": ["600000.XSHG"], "start_date": None, "end_date": None}
369
+ with pytest.raises(ValueError):
370
+ shares.get_turnover_rate("600000.XSHG", "2025-06-30", "2025-06-01")
371
+ with pytest.raises(ValueError):
372
+ shares.get_turnover_rate([])
373
+
374
+
347
375
  def test_as_of_keeps_a_timestamp(monkeypatch):
348
376
  import datetime
349
377
  from libfinance.utils.utils import as_of_text
@@ -8,9 +8,11 @@ connection is cut. Seen 2026-10-04 against deploy-libfinanceserver: every call a
8
8
  import socket
9
9
  import struct
10
10
  import threading
11
+ import time
11
12
 
12
13
  import msgpack
13
14
 
15
+ import libfinance.client as client_module
14
16
  from libfinance.client import RpcClient
15
17
 
16
18
 
@@ -61,6 +63,35 @@ class ClosingServer:
61
63
  self.listener.close()
62
64
 
63
65
 
66
+ class DelayedServer(ClosingServer):
67
+ """Answers every call after a delay and records how many requests reached the server."""
68
+
69
+ def __init__(self, delay):
70
+ self.delay = delay
71
+ self.requests = 0
72
+ super().__init__(answers_per_connection=0)
73
+
74
+ def _serve(self):
75
+ try:
76
+ sock, _ = self.listener.accept()
77
+ except OSError:
78
+ return
79
+ self.connections += 1
80
+ try:
81
+ while True:
82
+ (length,) = struct.unpack("!I", self._recv(sock, 4))
83
+ meta_and_payload = self._recv(sock, length)
84
+ request_id = struct.unpack("!I", meta_and_payload[1:5])[0]
85
+ self.requests += 1
86
+ time.sleep(self.delay)
87
+ body = msgpack.packb({"status": "ok", "code": 0, "result": {"message": "pong"}})
88
+ sock.sendall(struct.pack("!I", 6 + len(body)) + bytes([0x02]) + struct.pack("!I", request_id)
89
+ + bytes([0x00]) + body)
90
+ except (ConnectionError, OSError):
91
+ pass
92
+ sock.close()
93
+
94
+
64
95
  def test_a_call_after_the_server_closed_the_connection_reconnects():
65
96
  server = ClosingServer(answers_per_connection=1)
66
97
  client = RpcClient("127.0.0.1", server.port)
@@ -100,3 +131,46 @@ def test_an_unreachable_server_is_a_connection_error_not_a_hang():
100
131
  assert "Not connected to server" in str(error)
101
132
  else:
102
133
  raise AssertionError("expected ConnectionError")
134
+
135
+
136
+ def test_default_timeout_uses_the_configured_request_timeout(monkeypatch):
137
+ observed = []
138
+
139
+ class ConnectedSocket:
140
+ def sendall(self, _frame):
141
+ pass
142
+
143
+ def result(_future, timeout):
144
+ observed.append(timeout)
145
+ return {"status": "ok"}
146
+
147
+ monkeypatch.setattr(client_module.Future, "result", result)
148
+ client = RpcClient()
149
+ client.running = True
150
+ client.sock = ConnectedSocket()
151
+ client.request_timeout = 17.5
152
+ assert client.call_raw("ping") == {"status": "ok"}
153
+ assert observed == [17.5]
154
+
155
+
156
+ def test_api_timeout_is_not_retried_and_late_answer_is_not_unknown(capsys):
157
+ server = DelayedServer(delay=0.08)
158
+ client = RpcClient("127.0.0.1", server.port)
159
+ client.request_timeout = 0.02
160
+ client.connect()
161
+ try:
162
+ client.ping()
163
+ except TimeoutError as error:
164
+ assert "req_id=1" in str(error)
165
+ else:
166
+ raise AssertionError("expected TimeoutError")
167
+
168
+ # Let the reader consume the answer that the server completed after the caller timed out.
169
+ for _ in range(100):
170
+ if not client._expired_requests:
171
+ break
172
+ threading.Event().wait(0.01)
173
+ assert server.requests == 1
174
+ assert "unknown request_id" not in capsys.readouterr().err
175
+ client.close()
176
+ server.close()
libfinance-0.1.3/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.1.3
File without changes
File without changes
File without changes