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/pnl.py ADDED
@@ -0,0 +1,280 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ import polars as pl
7
+
8
+ if TYPE_CHECKING:
9
+ from polars.type_aliases import IntoExpr
10
+
11
+ from .polars_utils import parse_into_expr, register_plugin
12
+
13
+
14
+ class Fee:
15
+ def to_dict(self) -> dict[str, Any]:
16
+ raise NotImplementedError
17
+
18
+ def __add__(self, other: Fee) -> FeeSum:
19
+ if isinstance(self, FeeSum):
20
+ if isinstance(other, FeeSum):
21
+ return FeeSum(items=self.items + other.items)
22
+ return FeeSum(items=[*self.items, other])
23
+ else:
24
+ if isinstance(other, FeeSum):
25
+ return FeeSum(items=[self, *other.items])
26
+ return FeeSum(items=[self, other])
27
+
28
+ @staticmethod
29
+ def trade(fee: float) -> TradeFee:
30
+ return TradeFee(fee)
31
+
32
+ @staticmethod
33
+ def qty(fee: float) -> QtyFee:
34
+ return QtyFee(fee)
35
+
36
+ @staticmethod
37
+ def percent(fee: float) -> PercentFee:
38
+ return PercentFee(fee)
39
+
40
+ def zero(self) -> FeeZero:
41
+ return FeeZero()
42
+
43
+ def min(self, fee: float) -> MinFee:
44
+ return MinFee(fee, self)
45
+
46
+ def max(self, fee: float) -> MaxFee:
47
+ return MaxFee(fee, self)
48
+
49
+
50
+ @dataclass
51
+ class FeeZero(Fee):
52
+ def to_dict(self):
53
+ return {"kind": "zero"}
54
+
55
+
56
+ @dataclass
57
+ class PercentFee(Fee):
58
+ """Represents a fee based on a percentage of the trade amount."""
59
+
60
+ rate: float
61
+
62
+ def to_dict(self):
63
+ return {"kind": "percent", "rate": self.rate}
64
+
65
+
66
+ @dataclass
67
+ class QtyFee(Fee):
68
+ """Represents a fee based on the quantity of a trade."""
69
+
70
+ per_qty: float
71
+
72
+ def to_dict(self):
73
+ return {"kind": "per_qty", "per_qty": self.per_qty}
74
+
75
+
76
+ @dataclass
77
+ class TradeFee(Fee):
78
+ """Represents a fixed fee for a trade."""
79
+
80
+ per_trade: float
81
+
82
+ def to_dict(self):
83
+ return {"kind": "per_trade", "per_trade": self.per_trade}
84
+
85
+
86
+ @dataclass
87
+ class FeeSum(Fee):
88
+ items: list[Fee] = field(default_factory=list)
89
+
90
+ def to_dict(self):
91
+ return {
92
+ "kind": "sum",
93
+ "items": [f.to_dict() for f in self.items],
94
+ }
95
+
96
+
97
+ @dataclass
98
+ class MinFee(Fee):
99
+ """Represents a minimum fee for a trade."""
100
+
101
+ cap: float
102
+ fee: Fee
103
+
104
+ def to_dict(self):
105
+ return {"kind": "min", "cap": self.cap, "fee": self.fee.to_dict()}
106
+
107
+
108
+ @dataclass
109
+ class MaxFee(Fee):
110
+ """Represents a maximum fee for a trade."""
111
+
112
+ floor: float
113
+ fee: Fee
114
+
115
+ def to_dict(self):
116
+ return {"kind": "max", "floor": self.floor, "fee": self.fee.to_dict()}
117
+
118
+
119
+ def calc_bond_trade_pnl(
120
+ symbol: IntoExpr,
121
+ settle_time: IntoExpr,
122
+ qty: IntoExpr | None = None,
123
+ clean_price: IntoExpr | None = None,
124
+ clean_close: IntoExpr = "close",
125
+ bond_info_path: str | None = None,
126
+ multiplier: float = 1,
127
+ fee: Fee | None = None,
128
+ borrowing_cost: float = 0,
129
+ capital_rate: float = 0,
130
+ begin_state: IntoExpr | None = None,
131
+ ) -> pl.Expr:
132
+ """
133
+ 计算债券交易pnl
134
+ symbol: 交易的标的名称, 如果不是债券传⼊空字符串即可。
135
+ settle_time: 结算时间,
136
+ 如果settle_time传入代表Trade的struct Series(包含time, price, qty三个field), 则可以不传qty和clean_price
137
+ qty: 成交量, 正负号表⽰⽅向
138
+ clean_price: 成交的净价
139
+ clean_close: 当前时间段的最新价格(净价)
140
+ bond_info_path: 可以指定债券信息的存放⽂件夹, 不传⼊则使⽤默认路径.
141
+ multiplier: 合约乘数, 例如对于债券, 1000的成交对应1000w, 合约乘数应为100
142
+ fee: 交易费⽤
143
+ 费⽤设置说明:
144
+ TradeFee: 每笔成交⽀付的费⽤
145
+ QtyFee: 每⼿需要⽀付的费⽤
146
+ PercentFee: 按照成交⾦额百分⽐⽀付的费⽤
147
+ 费⽤⽀持相加, 例如 QtyFee(120) + TradeFee(20)
148
+ """
149
+ assert clean_close is not None
150
+ if fee is None:
151
+ fee = FeeZero()
152
+ fee = fee.to_dict()
153
+ symbol = parse_into_expr(symbol)
154
+ settle_time = parse_into_expr(settle_time)
155
+ clean_close = parse_into_expr(clean_close)
156
+ if begin_state is not None and not isinstance(begin_state, dict):
157
+ begin_state = parse_into_expr(begin_state)
158
+ if bond_info_path is None:
159
+ from .bond import bonds_info_path as path
160
+
161
+ bond_info_path = str(path)
162
+
163
+ if begin_state is None:
164
+ begin_state = pl.lit(
165
+ {
166
+ "pos": 0,
167
+ "avg_price": 0,
168
+ "pnl": 0,
169
+ "realized_pnl": 0,
170
+ "pos_price": 0,
171
+ "unrealized_pnl": 0,
172
+ "coupon_paid": 0,
173
+ "amt": 0,
174
+ "fee": 0,
175
+ }
176
+ )
177
+ kwargs = {
178
+ "multiplier": multiplier,
179
+ "fee": fee,
180
+ "borrowing_cost": borrowing_cost,
181
+ "capital_rate": capital_rate,
182
+ "bond_info_path": bond_info_path,
183
+ }
184
+ if all(x is None for x in [qty, clean_price]):
185
+ # struct settle_time, contains trade info
186
+ args = [symbol, settle_time, clean_close, begin_state]
187
+ else:
188
+ qty = parse_into_expr(qty)
189
+ clean_price = parse_into_expr(clean_price)
190
+ args = [symbol, settle_time, qty, clean_price, clean_close, begin_state]
191
+ return register_plugin(
192
+ args=args,
193
+ kwargs=kwargs,
194
+ symbol="calc_bond_trade_pnl",
195
+ is_elementwise=False,
196
+ )
197
+
198
+
199
+ def calc_trade_pnl(
200
+ time: IntoExpr,
201
+ qty: IntoExpr | None = None,
202
+ price: IntoExpr | None = None,
203
+ close: IntoExpr = "close",
204
+ multiplier: float = 1,
205
+ fee: Fee | None = None,
206
+ begin_state: IntoExpr | None = None,
207
+ ):
208
+ """
209
+ 计算交易pnl
210
+ symbol: 交易的标的名称, 如果不是债券传⼊空字符串即可。
211
+ time: 交易时间,
212
+ 如果time传入代表Trade的struct Series(包含time, price, qty三个field), 则可以不传qty和clean_price
213
+ qty: 成交量, 正负号表⽰⽅向
214
+ clean_price: 成交的净价
215
+ clean_close: 当前时间段的最新价格(净价)
216
+ multiplier: 合约乘数, 例如对于债券, 1000的成交对应1000w, 合约乘数应为100
217
+ fee: 交易费⽤
218
+ 费⽤设置说明:
219
+ TradeFee: 每笔成交⽀付的费⽤
220
+ QtyFee: 每⼿需要⽀付的费⽤
221
+ PercentFee: 按照成交⾦额百分⽐⽀付的费⽤
222
+ 费⽤⽀持相加, 例如 QtyFee(120) + TradeFee(20)
223
+ """
224
+ return calc_bond_trade_pnl(
225
+ symbol=pl.lit(""),
226
+ settle_time=time,
227
+ qty=qty,
228
+ clean_price=price,
229
+ clean_close=close,
230
+ multiplier=multiplier,
231
+ fee=fee,
232
+ begin_state=begin_state,
233
+ )
234
+
235
+
236
+ def trading_from_pos(
237
+ time: IntoExpr,
238
+ pos: IntoExpr,
239
+ open: IntoExpr,
240
+ finish_price: IntoExpr | None = None,
241
+ cash: IntoExpr = 1e8,
242
+ multiplier: float = 1,
243
+ qty_tick: float = 1.0,
244
+ min_adjust_amt: float = 0.0,
245
+ *,
246
+ stop_on_finish: bool = False,
247
+ keep_shape: bool = False,
248
+ ) -> pl.Expr:
249
+ """
250
+ 生成交易记录
251
+ time: ⽤于⽣成成交时间, ⽀持任意可以转为polars表达式的输⼊
252
+ pos: 当前时间的实际仓位, -1 ~ 1, 表⽰百分⽐
253
+ open: 当前周期的开仓价格
254
+ cash: 总资⾦, ⽤于计算实际开仓⼿数
255
+ multiplier: 合约乘数
256
+ qty_tick: 最⼩开仓⼿数, 例如0.01, 0.1, 1, 100
257
+ stop_on_finish: 当前标的没有数据后是否平仓
258
+ finish_price: 当前标的没数据时的平仓价格, ⽀持polars表达式
259
+ keep_shape: 是否维持表达式的长度, 不保留则只返回实际发生的交易
260
+ """
261
+ time = parse_into_expr(time)
262
+ pos = parse_into_expr(pos)
263
+ open = parse_into_expr(open)
264
+ finish_price = parse_into_expr(finish_price)
265
+ cash = parse_into_expr(cash)
266
+ kwargs = {
267
+ "cash": None, # 会从表达式中获取
268
+ "multiplier": float(multiplier),
269
+ "qty_tick": float(qty_tick),
270
+ "stop_on_finish": stop_on_finish,
271
+ "finish_price": None, # 会从表达式中获取
272
+ "min_adjust_amt": float(min_adjust_amt),
273
+ "keep_shape": bool(keep_shape),
274
+ }
275
+ return register_plugin(
276
+ args=[time, pos, open, finish_price, cash],
277
+ kwargs=kwargs,
278
+ symbol="trading_from_pos",
279
+ is_elementwise=False,
280
+ )
pybond/polars_utils.py ADDED
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import polars as pl
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Sequence
11
+
12
+ from polars.type_aliases import IntoExpr, PolarsDataType
13
+
14
+
15
+ def parse_into_expr(
16
+ expr: IntoExpr,
17
+ *,
18
+ str_as_lit: bool = False,
19
+ list_as_lit: bool = True,
20
+ dtype: PolarsDataType | None = None,
21
+ ) -> pl.Expr:
22
+ """
23
+ Parse a single input into an expression.
24
+
25
+ Parameters
26
+ ----------
27
+ expr
28
+ The input to be parsed as an expression.
29
+ str_as_lit
30
+ Interpret string input as a string literal. If set to `False` (default),
31
+ strings are parsed as column names.
32
+ list_as_lit
33
+ Interpret list input as a lit literal, If set to `False`,
34
+ lists are parsed as `Series` literals.
35
+ dtype
36
+ If the input is expected to resolve to a literal with a known dtype, pass
37
+ this to the `lit` constructor.
38
+
39
+ Returns
40
+ -------
41
+ polars.Expr
42
+ """
43
+ if isinstance(expr, pl.Expr):
44
+ pass
45
+ elif isinstance(expr, str) and not str_as_lit:
46
+ expr = pl.col(expr)
47
+ elif isinstance(expr, list) and not list_as_lit:
48
+ expr = pl.lit(pl.Series(expr), dtype=dtype)
49
+ else:
50
+ expr = pl.lit(expr, dtype=dtype)
51
+
52
+ return expr
53
+
54
+
55
+ def register_plugin(
56
+ *,
57
+ symbol: str,
58
+ is_elementwise: bool,
59
+ kwargs: dict[str, Any] | None = None,
60
+ args: list[IntoExpr],
61
+ # lib: str | Path,
62
+ ) -> pl.Expr:
63
+ global lib
64
+ if parse_version(pl.__version__) < parse_version("0.20.16"):
65
+ assert isinstance(args[0], pl.Expr)
66
+ assert isinstance(lib, str)
67
+ return args[0].register_plugin(
68
+ lib=lib,
69
+ symbol=symbol,
70
+ args=args[1:],
71
+ kwargs=kwargs,
72
+ is_elementwise=is_elementwise,
73
+ )
74
+ from polars.plugins import register_plugin_function
75
+
76
+ return register_plugin_function(
77
+ args=args,
78
+ plugin_path=lib,
79
+ function_name=symbol,
80
+ kwargs=kwargs,
81
+ is_elementwise=is_elementwise,
82
+ )
83
+
84
+
85
+ def parse_version(version: Sequence[str | int]) -> tuple[int, ...]:
86
+ # Simple version parser; split into a tuple of ints for comparison.
87
+ # vendored from Polars
88
+ if isinstance(version, str):
89
+ version = version.split(".")
90
+ return tuple(int(re.sub(r"\D", "", str(v))) for v in version)
91
+
92
+
93
+ if parse_version(pl.__version__) < parse_version("0.20.16"):
94
+ from polars.utils.udfs import _get_shared_lib_location
95
+
96
+ lib: str | Path = _get_shared_lib_location(__file__)
97
+ else:
98
+ lib = Path(__file__).parent
pybond/pybond.abi3.so ADDED
Binary file