tea-bond 0.2.8__cp310-abi3-macosx_11_0_arm64.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.

Potentially problematic release.


This version of tea-bond might be problematic. Click here for more details.

pybond/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from .bond import Bond
4
+
5
+ # from .pnl import calc_bond_trade_pnl
6
+ from .pybond import Future, Ib, Sse
7
+ from .pybond import TfEvaluator as _TfEvaluatorRS
8
+
9
+
10
+ class TfEvaluator(_TfEvaluatorRS):
11
+ def __new__(cls, future, bond, *args, **kwargs):
12
+ if not isinstance(bond, Bond):
13
+ bond = Bond(bond)
14
+ return super().__new__(cls, future, bond, *args, **kwargs)
15
+
16
+
17
+ __all__ = ["Bond", "Future", "Ib", "Sse", "TfEvaluator"]
pybond/bond.py ADDED
@@ -0,0 +1,185 @@
1
+ import os
2
+ from datetime import date
3
+ from importlib.util import find_spec
4
+ from pathlib import Path
5
+
6
+ from .pybond import Bond as _BondRS
7
+ from .pybond import Future, download_bond
8
+
9
+ WIND_AVAILABLE = find_spec("WindPy") is not None
10
+
11
+ if os.environ.get("BONDS_INFO_PATH") is not None:
12
+ bonds_info_environ_flag = True
13
+ bonds_info_path = Path(os.environ.get("BONDS_INFO_PATH"))
14
+ else:
15
+ bonds_info_environ_flag = False
16
+ bonds_info_path = Path(__file__).parent / "data" / "bonds_info"
17
+
18
+ if not bonds_info_path.exists():
19
+ bonds_info_path.mkdir(parents=True)
20
+
21
+
22
+ class Bond(_BondRS):
23
+ def __new__(cls, code: str | int = "", path: str | Path | None = None):
24
+ """
25
+ Create a new Bond instance.
26
+
27
+ Args:
28
+ code (str | int): The bond code. If no extension is provided, '.IB' will be appended.
29
+ path (str | Path | None): Path to the bond info file. If None, uses default bonds_info_path.
30
+
31
+ Returns:
32
+ Bond: A new Bond instance, either loaded from existing JSON file or downloaded.
33
+
34
+ Note:
35
+ If a JSON file for the bond code doesn't exist at the specified path,
36
+ the bond info will be downloaded automatically.
37
+ """
38
+ code = str(code)
39
+ if code == "":
40
+ return super().__new__(cls, "", path)
41
+ if "." not in code:
42
+ code = code + ".IB"
43
+ path = bonds_info_path if path is None else Path(path)
44
+ if (path / (code + ".json")).exists():
45
+ return super().__new__(cls, code, path)
46
+ else:
47
+ cls.download(code, path)
48
+ return super().__new__(cls, code, path)
49
+
50
+ @classmethod
51
+ def from_json(cls, data: str | dict) -> "Bond":
52
+ if isinstance(data, str):
53
+ import json
54
+
55
+ data = json.loads(data)
56
+ bond = Bond()
57
+ for k, v in data.items():
58
+ setattr(bond, k, v)
59
+ return bond
60
+
61
+ @staticmethod
62
+ def download(
63
+ code: str, path: str | None = None, source: str | None = None, save=True
64
+ ):
65
+ """
66
+ Download bond information from a specified source.
67
+
68
+ This method downloads bond information for a given bond code from either Wind or Rust.
69
+ If no source is specified, it defaults to Wind if the WindPy module is available; otherwise,
70
+ it falls back to Rust.
71
+
72
+ If the source is 'rust', the method will download IB bond information from China Money and
73
+ SH bond information from SSE (Shanghai Stock Exchange).
74
+
75
+ Args:
76
+ code (str): The bond code in the format 'XXXXXX.YY'. The code must include a dot.
77
+ path (str | None): The directory path where the downloaded bond information should be saved.
78
+ If None, the default path is used.
79
+ source (str | None): The source from which to download the bond information. Valid options are
80
+ 'wind' or 'rust'. If None, the source is automatically determined.
81
+ save (bool): Whether to save the downloaded bond information to the specified path.
82
+ Defaults to True.
83
+
84
+ Returns:
85
+ Bond: The downloaded bond object if the source is 'rust' and save is False.
86
+ Otherwise, returns None.
87
+
88
+ Raises:
89
+ AssertionError: If the code is not in the correct format or if the source is invalid.
90
+ """
91
+ if source is None:
92
+ # 优先从wind下载
93
+ source = "wind" if WIND_AVAILABLE else "rust"
94
+ assert "." in code, "code should be in the format of XXXXXX.YY"
95
+ assert source in ("wind", "rust")
96
+ if source == "wind":
97
+ from .download import fetch_symbols, login
98
+
99
+ print(f"Start downloading bond info for {code} from Wind")
100
+ login()
101
+ fetch_symbols([code], save=save, save_folder=path)
102
+ else:
103
+ # let rust side handle the download
104
+ print(f"download {code}")
105
+ bond = download_bond(code)
106
+ if save:
107
+ bond.save(path)
108
+ return bond
109
+
110
+ def accrued_interest(
111
+ self, date: date, cp_dates: tuple[date, date] | None = None
112
+ ) -> float:
113
+ """
114
+ 计算应计利息
115
+
116
+ 银行间和交易所的计算规则不同,银行间是算头不算尾,而交易所是算头又算尾
117
+ """
118
+ return self.calc_accrued_interest(date, cp_dates=cp_dates)
119
+
120
+ def dirty_price(
121
+ self,
122
+ ytm: float,
123
+ date: date,
124
+ cp_dates: tuple[date, date] | None = None,
125
+ remain_cp_num: int | None = None,
126
+ ) -> float:
127
+ """通过ytm计算债券全价"""
128
+ return self.calc_dirty_price_with_ytm(
129
+ ytm, date, cp_dates=cp_dates, remain_cp_num=remain_cp_num
130
+ )
131
+
132
+ def clean_price(
133
+ self,
134
+ ytm: float,
135
+ date: date,
136
+ cp_dates: tuple[date, date] | None = None,
137
+ remain_cp_num: int | None = None,
138
+ ) -> float:
139
+ """通过ytm计算债券净价"""
140
+ return self.calc_clean_price_with_ytm(
141
+ ytm, date, cp_dates=cp_dates, remain_cp_num=remain_cp_num
142
+ )
143
+
144
+ def macaulay_duration(
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
+ """计算麦考利久期"""
152
+ return self.calc_macaulay_duration(
153
+ ytm, date, cp_dates=cp_dates, remain_cp_num=remain_cp_num
154
+ )
155
+
156
+ def duration(
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
+ """计算修正久期"""
164
+ return self.calc_duration(
165
+ ytm, date, cp_dates=cp_dates, remain_cp_num=remain_cp_num
166
+ )
167
+
168
+ def cf(self, future: str | Future) -> float:
169
+ """计算转换因子"""
170
+ from .pybond import TfEvaluator
171
+
172
+ return TfEvaluator(future, self).cf
173
+
174
+ def calc_ytm_with_clean_price(
175
+ self,
176
+ clean_price: float,
177
+ date: date,
178
+ cp_dates: tuple[date, date] | None = None,
179
+ remain_cp_num: int | None = None,
180
+ ) -> float:
181
+ """通过净价计算债券ytm"""
182
+ dirty_price = clean_price + self.accrued_interest(date, cp_dates=cp_dates)
183
+ return self.calc_ytm_with_price(
184
+ dirty_price, date, cp_dates=cp_dates, remain_cp_num=remain_cp_num
185
+ )
pybond/download.py ADDED
@@ -0,0 +1,147 @@
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
+ # if not default_save_folder.exists():
12
+ # default_save_folder.mkdir()
13
+
14
+
15
+ def save_json(path: Path | str, data: dict) -> None:
16
+ """
17
+ Save data into json file in temp path.
18
+ """
19
+ path = Path(path)
20
+ with path.open(mode="w+", encoding="UTF-8") as f:
21
+ json.dump(data, f, indent=4, ensure_ascii=False)
22
+
23
+
24
+ def get_interest_type(typ: str):
25
+ if typ == "固定利率":
26
+ return "Fixed"
27
+ elif typ == "浮动利率":
28
+ return "Floating"
29
+ elif typ == "累进利率":
30
+ return "Progressive"
31
+ elif typ == "零息":
32
+ return "Zero"
33
+ else:
34
+ msg = f"Unknown interest type: {typ}"
35
+ raise ValueError(msg)
36
+
37
+
38
+ def get_payment_type(typ: str):
39
+ if typ == "附息":
40
+ return "Coupon_Bear"
41
+ elif typ == "到期一次还本付息":
42
+ return "One_Time"
43
+ elif typ == "贴现":
44
+ return "Zero_Coupon"
45
+ else:
46
+ msg = f"Unknown payment type: {typ}"
47
+ raise ValueError(msg)
48
+
49
+
50
+ def fetch_symbols(
51
+ symbols: list[str],
52
+ *,
53
+ save: bool = True,
54
+ skip: bool = True,
55
+ save_folder: Path | str | None = None,
56
+ ):
57
+ if save_folder is None:
58
+ save_folder = default_save_folder
59
+ if isinstance(save_folder, str):
60
+ save_folder = Path(save_folder)
61
+ if skip:
62
+ symbols = [s for s in symbols if not (save_folder / f"{s}.json").exists()]
63
+ data = w.wss(
64
+ symbols,
65
+ "sec_name,carrydate,maturitydate,interesttype,couponrate,paymenttype,actualbenchmark,coupon,interestfrequency,latestpar",
66
+ f"tradeDate={date.today()}",
67
+ ).Data
68
+ returns = []
69
+ for i, symbol in enumerate(symbols):
70
+ m = {"bond_code": symbol}
71
+ m["mkt"] = symbol.split(".")[1].upper()
72
+ m["abbr"] = data[0][i] # 债券简称
73
+ m["par_value"] = float(data[9][i]) # 面值
74
+ m["cp_type"] = get_payment_type(data[7][i]) # 付息频率
75
+ m["interest_type"] = get_interest_type(data[3][i]) # 付息方式
76
+ m["cp_rate_1st"] = float(Decimal(str(data[4][i])) / 100) # 票面利率
77
+ m["base_rate"] = None
78
+ m["rate_spread"] = None
79
+ if m["cp_type"] == "Coupon_Bear":
80
+ m["inst_freq"] = int(data[8][i]) # 年付息次数
81
+ elif m["cp_type"] == "One_Time":
82
+ m["inst_freq"] = 1
83
+ elif m["cp_type"] == "Zero_Coupon":
84
+ m["inst_freq"] = 0
85
+ m["carry_date"] = data[1][i].strftime("%Y-%m-%d") # 起息日
86
+ m["maturity_date"] = data[2][i].strftime("%Y-%m-%d") # 到期日
87
+ m["day_count"] = data[6][i] # 实际基准
88
+ returns.append(m)
89
+ print(m)
90
+ if save:
91
+ if not save_folder.exists():
92
+ save_folder.mkdir(parents=True)
93
+ path = save_folder / f"{symbol}.json"
94
+ save_json(path, m)
95
+ return returns
96
+
97
+
98
+ WAIT_LOGIN = False
99
+
100
+
101
+ def login():
102
+ global WAIT_LOGIN
103
+ if w.isconnected():
104
+ return
105
+ if WAIT_LOGIN:
106
+ import time
107
+
108
+ time.sleep(0.2)
109
+ login()
110
+ WAIT_LOGIN = True
111
+ login_res = w.start(waitTime=8)
112
+ WAIT_LOGIN = False
113
+ if login_res.ErrorCode != 0:
114
+ msg = f"Failed to login to Wind: {login_res.ErrorCode}"
115
+ raise RuntimeError(msg)
116
+
117
+
118
+ def get_all_symbols():
119
+ sector_ids = (
120
+ # "a101010101000000", # 国债银行间
121
+ # "a101010104000000", # 政策性银行债
122
+ "a101010201000000", # 上交所国债
123
+ )
124
+ res = []
125
+ names = []
126
+ for sector_id in sector_ids:
127
+ all_symbols = w.wset(
128
+ "sectorconstituent", f"sectorid={sector_id};field=wind_code,sec_name"
129
+ ).Data
130
+ res.extend(all_symbols[0])
131
+ names.extend(all_symbols[1])
132
+ print("共有", len(res), "只债券")
133
+ return res
134
+
135
+
136
+ if __name__ == "__main__":
137
+ login()
138
+ # symbols = ["220003.IB", "220021.IB", "220006.IB", "220010.IB"]
139
+ # symbols = ["240006.IB"]
140
+
141
+ # symbols = ["019733.SH"]
142
+ # symbols = ["020647.SH"]
143
+ # symbols = ["019727.SH"]
144
+ symbols = ["2400006.IB"]
145
+ # symbols = get_all_symbols()
146
+
147
+ fetch_symbols(symbols, save=0, skip=True)
pybond/ffi/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .bond import *
2
+ from .datetime import *
3
+ from .duration 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.argstypes = (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
pybond/ffi/lib.py ADDED
@@ -0,0 +1,8 @@
1
+ import ctypes
2
+
3
+ import llvmlite.binding
4
+
5
+ from pybond import pybond
6
+
7
+ lib = ctypes.cdll.LoadLibrary(pybond.__file__)
8
+ llvmlite.binding.load_library_permanently(pybond.__file__)
pybond/nb/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ # from datetime import date, datetime, time
2
+
3
+ from .nb_bond import Bond, BondType, bond_type
4
+ from .nb_date import DateType, date_type
5
+ from .nb_datetime import DateTime, DateTimeType, datetime_type
6
+ from .nb_duration import Duration, DurationType, duration_type
7
+ from .nb_time import Time, TimeType, time_type
8
+
9
+ __all__ = [
10
+ "Bond",
11
+ "BondType",
12
+ "bond_type",
13
+ "DateTime",
14
+ "DateTimeType",
15
+ "DateType",
16
+ "Duration",
17
+ "DurationType",
18
+ "duration_type",
19
+ "Time",
20
+ "TimeType",
21
+ # "date",
22
+ "date_type",
23
+ # "datetime",
24
+ "datetime_type",
25
+ # "time",
26
+ "time_type",
27
+ ]
pybond/nb/ir_utils.py ADDED
@@ -0,0 +1,47 @@
1
+ from llvmlite import ir
2
+ from numba.core import cgutils
3
+
4
+
5
+ def ir_isinstance(pyapi, obj, typ):
6
+ fnty = ir.FunctionType(ir.IntType(1), [pyapi.pyobj, pyapi.pyobj])
7
+ fn = cgutils.get_or_insert_function(
8
+ pyapi.builder.module, fnty, name="PyObject_IsInstance"
9
+ )
10
+ return pyapi.builder.call(fn, (obj, typ))
11
+
12
+
13
+ def ir_build_datetime(val, builder, *, from_utc: bool = False):
14
+ mod = builder.module
15
+ fnty = ir.FunctionType(ir.PointerType(ir.IntType(8)), [ir.IntType(64)])
16
+ if not from_utc:
17
+ build_datetime_ns_fn = cgutils.get_or_insert_function(
18
+ mod, fnty, "build_datetime_ns"
19
+ )
20
+ else:
21
+ build_datetime_ns_fn = cgutils.get_or_insert_function(
22
+ mod, fnty, "build_datetime_from_utc_ns"
23
+ )
24
+ ptr = builder.call(build_datetime_ns_fn, [val])
25
+ return ptr
26
+
27
+
28
+ def ir_timestamp_nanos(ptr, builder):
29
+ mod = builder.module
30
+ fnty = ir.FunctionType(ir.IntType(64), [ir.PointerType(ir.IntType(8))])
31
+ fn = cgutils.get_or_insert_function(mod, fnty, "timestamp_nanos")
32
+ ptr = builder.call(fn, [ptr])
33
+ return ptr
34
+
35
+
36
+ def ir_local_timestamp_nanos(ptr, builder):
37
+ mod = builder.module
38
+ fnty = ir.FunctionType(ir.IntType(64), [ir.PointerType(ir.IntType(8))])
39
+ fn = cgutils.get_or_insert_function(mod, fnty, "local_timestamp_nanos")
40
+ ptr = builder.call(fn, [ptr])
41
+ return ptr
42
+
43
+
44
+ def long_as_ulong(pyapi, numobj):
45
+ fnty = ir.FunctionType(pyapi.ulong, [pyapi.pyobj])
46
+ fn = pyapi._get_function(fnty, name="PyLong_AsUnsignedLong")
47
+ return pyapi.builder.call(fn, [numobj])