tea-bond 0.4.3__cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.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.
pybond/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from .bond import Bond
4
+ from .pybond import Future, Ib, Sse, get_version, update_info_from_wind_sql_df
5
+ from .pybond import TfEvaluator as _TfEvaluatorRS
6
+
7
+ __version__ = get_version()
8
+
9
+
10
+ def update_info(df):
11
+ if type(df).__module__.split(".")[0] == "pandas":
12
+ import polars as pl
13
+
14
+ df = pl.from_pandas(df)
15
+ return update_info_from_wind_sql_df(df)
16
+
17
+
18
+ class TfEvaluator(_TfEvaluatorRS):
19
+ def __new__(cls, future, bond, *args, **kwargs):
20
+ if not isinstance(bond, Bond):
21
+ # 便于直接从Wind下载债券基础数据
22
+ bond = Bond(bond)
23
+ return super().__new__(cls, future, bond, *args, **kwargs)
24
+
25
+
26
+ __all__ = ["Bond", "Future", "Ib", "Sse", "TfEvaluator", "__version__"]
pybond/bond.py ADDED
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from importlib.util import find_spec
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ from .pybond import Bond as _BondRS
9
+ from .pybond import Future, download_bond
10
+
11
+ if TYPE_CHECKING:
12
+ from datetime import date
13
+
14
+
15
+ WIND_AVAILABLE = find_spec("WindPy") is not None
16
+
17
+ if os.environ.get("BONDS_INFO_PATH") is not None:
18
+ bonds_info_environ_flag = True
19
+ bonds_info_path = Path(os.environ.get("BONDS_INFO_PATH"))
20
+ else:
21
+ bonds_info_environ_flag = False
22
+ old_default_path = Path(__file__).parent / "data" / "bonds_info"
23
+ bonds_info_path = Path.home() / "tea-bond" / "bonds_info"
24
+ if old_default_path.exists() and not bonds_info_path.exists():
25
+ import shutil
26
+
27
+ shutil.move(str(old_default_path), str(bonds_info_path))
28
+ if not bonds_info_path.exists():
29
+ bonds_info_path.mkdir(parents=True)
30
+ os.environ["BONDS_INFO_PATH"] = str(bonds_info_path)
31
+
32
+ if not bonds_info_path.exists():
33
+ bonds_info_path.mkdir(parents=True)
34
+
35
+
36
+ class Bond(_BondRS):
37
+ def __new__(
38
+ cls,
39
+ code: str | int = "",
40
+ path: str | Path | None = None,
41
+ *,
42
+ download: bool = True,
43
+ ):
44
+ """
45
+ Create a new Bond instance.
46
+
47
+ Args:
48
+ code (str | int): The bond code. If no extension is provided, '.IB' will be appended.
49
+ path (str | Path | None): Path to the bond info file. If None, uses default bonds_info_path.
50
+ download (bool): Whether to automatically download the bond info if it doesn't exist.
51
+
52
+ Returns:
53
+ Bond: A new Bond instance, either loaded from existing JSON file or downloaded.
54
+
55
+ Note:
56
+ If a JSON file for the bond code doesn't exist at the specified path,
57
+ the bond info will be downloaded automatically.
58
+ """
59
+ code = str(code)
60
+ if code == "":
61
+ return super().__new__(cls, "", path)
62
+ if "." not in code:
63
+ code = code + ".IB"
64
+ try:
65
+ return super().__new__(cls, code, path)
66
+ except ValueError as e:
67
+ if download:
68
+ path = bonds_info_path if path is None else Path(path)
69
+ cls.download(code, path)
70
+ return super().__new__(cls, code, path)
71
+ else:
72
+ raise ValueError from e
73
+
74
+ @classmethod
75
+ def from_json(cls, data: str | dict) -> Bond:
76
+ if isinstance(data, str):
77
+ import json
78
+
79
+ data = json.loads(data)
80
+ bond = Bond()
81
+ for k, v in data.items():
82
+ setattr(bond, k, v)
83
+ return bond
84
+
85
+ @staticmethod
86
+ def download(
87
+ code: str, path: str | None = None, source: str | None = None, save=True
88
+ ):
89
+ """
90
+ Download bond information from a specified source.
91
+
92
+ This method downloads bond information for a given bond code from either Wind or Rust.
93
+ If no source is specified, it defaults to Wind if the WindPy module is available; otherwise,
94
+ it falls back to Rust.
95
+
96
+ If the source is 'rust', the method will download IB bond information from China Money and
97
+ SH bond information from SSE (Shanghai Stock Exchange).
98
+
99
+ Args:
100
+ code (str): The bond code in the format 'XXXXXX.YY'. The code must include a dot.
101
+ path (str | None): The directory path where the downloaded bond information should be saved.
102
+ If None, the default path is used.
103
+ source (str | None): The source from which to download the bond information. Valid options are
104
+ 'wind' or 'rust'. If None, the source is automatically determined.
105
+ save (bool): Whether to save the downloaded bond information to the specified path.
106
+ Defaults to True.
107
+
108
+ Returns:
109
+ Bond: The downloaded bond object if the source is 'rust' and save is False.
110
+ Otherwise, returns None.
111
+
112
+ Raises:
113
+ AssertionError: If the code is not in the correct format or if the source is invalid.
114
+ """
115
+ if source is None:
116
+ # 优先从wind下载
117
+ source = "wind" if WIND_AVAILABLE else "rust"
118
+ assert "." in code, "code should be in the format of XXXXXX.YY"
119
+ assert source in ("wind", "rust")
120
+ if source == "wind":
121
+ from .download import fetch_symbols, login
122
+
123
+ print(f"Start downloading bond info for {code} from Wind")
124
+ login()
125
+ fetch_symbols([code], save=save, save_folder=path)
126
+ else:
127
+ # let rust side handle the download
128
+ print(f"download {code}")
129
+ bond = download_bond(code)
130
+ if save:
131
+ bond.save(path)
132
+ return bond
133
+
134
+ def accrued_interest(
135
+ self, date: date, cp_dates: tuple[date, date] | None = None
136
+ ) -> float:
137
+ """
138
+ 计算应计利息
139
+
140
+ 银行间和交易所的计算规则不同,银行间是算头不算尾,而交易所是算头又算尾
141
+ """
142
+ return self.calc_accrued_interest(date, cp_dates=cp_dates)
143
+
144
+ def dirty_price(
145
+ self,
146
+ ytm: float,
147
+ date: date,
148
+ cp_dates: tuple[date, date] | None = None,
149
+ remain_cp_num: int | None = None,
150
+ ) -> float:
151
+ """通过ytm计算债券全价"""
152
+ return self.calc_dirty_price_with_ytm(
153
+ ytm, date, cp_dates=cp_dates, remain_cp_num=remain_cp_num
154
+ )
155
+
156
+ def clean_price(
157
+ self,
158
+ ytm: float,
159
+ date: date,
160
+ cp_dates: tuple[date, date] | None = None,
161
+ remain_cp_num: int | None = None,
162
+ ) -> float:
163
+ """通过ytm计算债券净价"""
164
+ return self.calc_clean_price_with_ytm(
165
+ ytm, date, cp_dates=cp_dates, remain_cp_num=remain_cp_num
166
+ )
167
+
168
+ def macaulay_duration(
169
+ self,
170
+ ytm: float,
171
+ date: date,
172
+ cp_dates: tuple[date, date] | None = None,
173
+ remain_cp_num: int | None = None,
174
+ ) -> float:
175
+ """计算麦考利久期"""
176
+ return self.calc_macaulay_duration(
177
+ ytm, date, cp_dates=cp_dates, remain_cp_num=remain_cp_num
178
+ )
179
+
180
+ def duration(
181
+ self,
182
+ ytm: float,
183
+ date: date,
184
+ cp_dates: tuple[date, date] | None = None,
185
+ remain_cp_num: int | None = None,
186
+ ) -> float:
187
+ """计算修正久期"""
188
+ return self.calc_duration(
189
+ ytm, date, cp_dates=cp_dates, remain_cp_num=remain_cp_num
190
+ )
191
+
192
+ def cf(self, future: str | Future) -> float:
193
+ """计算转换因子"""
194
+ from .pybond import TfEvaluator
195
+
196
+ return TfEvaluator(future, self).cf
197
+
198
+ def calc_ytm_with_clean_price(
199
+ self,
200
+ clean_price: float,
201
+ date: date,
202
+ cp_dates: tuple[date, date] | None = None,
203
+ remain_cp_num: int | None = None,
204
+ ) -> float:
205
+ """通过净价计算债券ytm"""
206
+ dirty_price = clean_price + self.accrued_interest(date, cp_dates=cp_dates)
207
+ return self.calc_ytm_with_price(
208
+ dirty_price, date, cp_dates=cp_dates, remain_cp_num=remain_cp_num
209
+ )
pybond/download.py ADDED
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import date
5
+ from decimal import Decimal
6
+ from pathlib import Path
7
+
8
+ from WindPy import w
9
+
10
+ default_save_folder = Path("bonds_info")
11
+
12
+
13
+ def save_json(path: Path | str, data: dict) -> None:
14
+ """
15
+ Save data into json file in temp path.
16
+ """
17
+ path = Path(path)
18
+ with path.open(mode="w+", encoding="UTF-8") as f:
19
+ json.dump(data, f, indent=4, ensure_ascii=False)
20
+
21
+
22
+ def get_interest_type(typ: str):
23
+ if typ == "固定利率":
24
+ return "Fixed"
25
+ elif typ == "浮动利率":
26
+ return "Floating"
27
+ elif typ == "累进利率":
28
+ return "Progressive"
29
+ elif typ == "零息":
30
+ return "Zero"
31
+ else:
32
+ msg = f"Unknown interest type: {typ}"
33
+ raise ValueError(msg)
34
+
35
+
36
+ def get_payment_type(typ: str):
37
+ if typ == "附息":
38
+ return "Coupon_Bear"
39
+ elif typ == "到期一次还本付息":
40
+ return "One_Time"
41
+ elif typ == "贴现":
42
+ return "Zero_Coupon"
43
+ else:
44
+ msg = f"Unknown payment type: {typ}"
45
+ raise ValueError(msg)
46
+
47
+
48
+ def fetch_symbols(
49
+ symbols: list[str],
50
+ *,
51
+ save: bool = True,
52
+ skip: bool = True,
53
+ save_folder: Path | str | None = None,
54
+ ):
55
+ if save_folder is None:
56
+ save_folder = default_save_folder
57
+ if isinstance(save_folder, str):
58
+ save_folder = Path(save_folder)
59
+ if skip:
60
+ symbols = [s for s in symbols if not (save_folder / f"{s}.json").exists()]
61
+ data = w.wss(
62
+ symbols,
63
+ "sec_name,carrydate,maturitydate,interesttype,couponrate,paymenttype,actualbenchmark,coupon,interestfrequency,latestpar",
64
+ f"tradeDate={date.today()}",
65
+ ).Data
66
+ returns = []
67
+ for i, symbol in enumerate(symbols):
68
+ m = {"bond_code": symbol}
69
+ m["mkt"] = symbol.split(".")[1].upper()
70
+ m["abbr"] = data[0][i] # 债券简称
71
+ m["par_value"] = float(data[9][i]) # 面值
72
+ m["cp_type"] = get_payment_type(data[7][i]) # 付息频率
73
+ m["interest_type"] = get_interest_type(data[3][i]) # 付息方式
74
+ m["cp_rate_1st"] = float(Decimal(str(data[4][i])) / 100) # 票面利率
75
+ m["base_rate"] = None
76
+ m["rate_spread"] = None
77
+ if m["cp_type"] == "Coupon_Bear":
78
+ m["inst_freq"] = int(data[8][i]) # 年付息次数
79
+ elif m["cp_type"] == "One_Time":
80
+ m["inst_freq"] = 1
81
+ elif m["cp_type"] == "Zero_Coupon":
82
+ m["inst_freq"] = 0
83
+ m["carry_date"] = data[1][i].strftime("%Y-%m-%d") # 起息日
84
+ m["maturity_date"] = data[2][i].strftime("%Y-%m-%d") # 到期日
85
+ m["day_count"] = data[6][i] # 实际基准
86
+ returns.append(m)
87
+ print(m)
88
+ if save:
89
+ if not save_folder.exists():
90
+ save_folder.mkdir(parents=True)
91
+ path = save_folder / f"{symbol}.json"
92
+ save_json(path, m)
93
+ return returns
94
+
95
+
96
+ WAIT_LOGIN = False
97
+
98
+
99
+ def login():
100
+ global WAIT_LOGIN
101
+ if w.isconnected():
102
+ return
103
+ if WAIT_LOGIN:
104
+ import time
105
+
106
+ time.sleep(0.2)
107
+ login()
108
+ WAIT_LOGIN = True
109
+ login_res = w.start(waitTime=8)
110
+ WAIT_LOGIN = False
111
+ if login_res.ErrorCode != 0:
112
+ msg = f"Failed to login to Wind: {login_res.ErrorCode}"
113
+ raise RuntimeError(msg)
114
+
115
+
116
+ def get_all_symbols():
117
+ sector_ids = (
118
+ # "a101010101000000", # 国债银行间
119
+ # "a101010104000000", # 政策性银行债
120
+ "a101010201000000", # 上交所国债
121
+ )
122
+ res = []
123
+ names = []
124
+ for sector_id in sector_ids:
125
+ all_symbols = w.wset(
126
+ "sectorconstituent", f"sectorid={sector_id};field=wind_code,sec_name"
127
+ ).Data
128
+ res.extend(all_symbols[0])
129
+ names.extend(all_symbols[1])
130
+ print("共有", len(res), "只债券")
131
+ return res
132
+
133
+
134
+ if __name__ == "__main__":
135
+ login()
136
+ # symbols = ["220003.IB", "220021.IB", "220006.IB", "220010.IB"]
137
+ # symbols = ["240006.IB"]
138
+
139
+ # symbols = ["019733.SH"]
140
+ # symbols = ["020647.SH"]
141
+ # symbols = ["019727.SH"]
142
+ symbols = ["2400006.IB"]
143
+ # symbols = get_all_symbols()
144
+
145
+ fetch_symbols(symbols, save=0, skip=True)
pybond/ffi/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .bond import *
2
+ from .datetime import *
3
+ from .duration import *
4
+ from .evaluators import *
pybond/ffi/bond.py ADDED
@@ -0,0 +1,68 @@
1
+ import ctypes
2
+
3
+ from .lib import lib
4
+
5
+ create_bond = lib.create_bond
6
+ create_bond.argtypes = (ctypes.c_void_p, ctypes.c_size_t)
7
+ create_bond.restype = ctypes.c_void_p
8
+
9
+ free_bond = lib.free_bond
10
+ free_bond.argtypes = [ctypes.c_void_p]
11
+ free_bond.restype = None
12
+
13
+ bond_coupon_rate = lib.bond_coupon_rate
14
+ bond_coupon_rate.argtypes = [ctypes.c_void_p]
15
+ bond_coupon_rate.restype = ctypes.c_double
16
+
17
+ bond_full_code = lib.bond_full_code
18
+ bond_full_code.argtypes = [ctypes.c_void_p]
19
+ bond_full_code.restype = ctypes.c_char_p
20
+
21
+ bond_calc_ytm = lib.bond_calc_ytm
22
+ bond_calc_ytm.argtypes = [
23
+ ctypes.c_void_p,
24
+ ctypes.c_double,
25
+ ctypes.c_uint32,
26
+ ctypes.c_uint32,
27
+ ctypes.c_uint32,
28
+ ]
29
+ bond_calc_ytm.restype = ctypes.c_double
30
+
31
+ bond_duration = lib.bond_duration
32
+ bond_duration.argtypes = [
33
+ ctypes.c_void_p,
34
+ ctypes.c_double,
35
+ ctypes.c_uint32,
36
+ ctypes.c_uint32,
37
+ ctypes.c_uint32,
38
+ ]
39
+ bond_duration.restype = ctypes.c_double
40
+
41
+ bond_accrued_interest = lib.bond_accrued_interest
42
+ bond_accrued_interest.argtypes = [
43
+ ctypes.c_void_p,
44
+ ctypes.c_uint32,
45
+ ctypes.c_uint32,
46
+ ctypes.c_uint32,
47
+ ]
48
+ bond_accrued_interest.restype = ctypes.c_double
49
+
50
+ bond_dirty_price = lib.bond_dirty_price
51
+ bond_dirty_price.argtypes = [
52
+ ctypes.c_void_p,
53
+ ctypes.c_double,
54
+ ctypes.c_uint32,
55
+ ctypes.c_uint32,
56
+ ctypes.c_uint32,
57
+ ]
58
+ bond_dirty_price.restype = ctypes.c_double
59
+
60
+ bond_clean_price = lib.bond_clean_price
61
+ bond_clean_price.argtypes = [
62
+ ctypes.c_void_p,
63
+ ctypes.c_double,
64
+ ctypes.c_uint32,
65
+ ctypes.c_uint32,
66
+ ctypes.c_uint32,
67
+ ]
68
+ bond_clean_price.restype = ctypes.c_double
pybond/ffi/datetime.py ADDED
@@ -0,0 +1,58 @@
1
+ import ctypes
2
+
3
+ from .lib import lib
4
+
5
+ build_datetime_ns = lib.build_datetime_ns
6
+ build_datetime_ns.argtypes = (ctypes.c_int64,)
7
+ build_datetime_ns.restype = ctypes.c_void_p
8
+
9
+ build_datetime_from_utc_ns = lib.build_datetime_from_utc_ns
10
+ build_datetime_from_utc_ns.argtypes = (ctypes.c_int64,)
11
+ build_datetime_from_utc_ns.restype = ctypes.c_void_p
12
+
13
+ local_timestamp_nanos = lib.local_timestamp_nanos
14
+ local_timestamp_nanos.argtypes = (ctypes.c_void_p,)
15
+ local_timestamp_nanos.restype = ctypes.c_int64
16
+
17
+ timestamp_nanos = lib.timestamp_nanos
18
+ timestamp_nanos.argtypes = (ctypes.c_void_p,)
19
+ timestamp_nanos.restype = ctypes.c_int64
20
+
21
+ utc_timestamp_to_local = lib.utc_timestamp_to_local
22
+ utc_timestamp_to_local.argtypes = (ctypes.c_int64,)
23
+ utc_timestamp_to_local.restype = ctypes.c_int64
24
+
25
+ _free_datetime = lib.free_datetime
26
+ _free_datetime.argtypes = (ctypes.c_void_p,)
27
+
28
+ get_datetime_year = lib.get_datetime_year
29
+ get_datetime_year.argtypes = (ctypes.c_void_p,)
30
+ get_datetime_year.restype = ctypes.c_int32
31
+
32
+ get_datetime_month = lib.get_datetime_month
33
+ get_datetime_month.argtypes = (ctypes.c_void_p,)
34
+ get_datetime_month.restype = ctypes.c_int32
35
+
36
+ get_datetime_day = lib.get_datetime_day
37
+ get_datetime_day.argtypes = (ctypes.c_void_p,)
38
+ get_datetime_day.restype = ctypes.c_int32
39
+
40
+ get_datetime_hour = lib.get_datetime_hour
41
+ get_datetime_hour.argtypes = (ctypes.c_void_p,)
42
+ get_datetime_hour.restype = ctypes.c_int32
43
+
44
+ get_datetime_minute = lib.get_datetime_minute
45
+ get_datetime_minute.argtypes = (ctypes.c_void_p,)
46
+ get_datetime_minute.restype = ctypes.c_int32
47
+
48
+ get_datetime_second = lib.get_datetime_second
49
+ get_datetime_second.argtypes = (ctypes.c_void_p,)
50
+ get_datetime_second.restype = ctypes.c_int32
51
+
52
+ get_datetime_nanosecond = lib.get_datetime_nanosecond
53
+ get_datetime_nanosecond.argtypes = (ctypes.c_void_p,)
54
+ get_datetime_nanosecond.restype = ctypes.c_int32
55
+
56
+ datetime_with_time = lib.datetime_with_time
57
+ datetime_with_time.argtypes = (ctypes.c_void_p, (ctypes.c_uint32 * 6))
58
+ datetime_with_time.restype = ctypes.c_void_p
pybond/ffi/duration.py ADDED
@@ -0,0 +1,19 @@
1
+ import ctypes
2
+
3
+ from .lib import lib
4
+
5
+ parse_duration = lib.parse_duration
6
+ parse_duration.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
7
+ parse_duration.restype = ctypes.c_void_p
8
+
9
+ datetime_sub_datetime = lib.datetime_sub_datetime
10
+ datetime_sub_datetime.argtypes = [ctypes.c_int64, ctypes.c_int64]
11
+ datetime_sub_datetime.restype = ctypes.c_void_p
12
+
13
+ datetime_add_duration = lib.datetime_add_duration
14
+ datetime_add_duration.argtypes = [ctypes.c_int64, ctypes.c_void_p]
15
+ datetime_add_duration.restype = ctypes.c_int64
16
+
17
+ datetime_sub_duration = lib.datetime_sub_duration
18
+ datetime_sub_duration.argtypes = [ctypes.c_int64, ctypes.c_void_p]
19
+ datetime_sub_duration.restype = ctypes.c_int64