tea-bond 0.3.11__tar.gz → 0.3.13__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.

Potentially problematic release.


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

Files changed (101) hide show
  1. {tea_bond-0.3.11 → tea_bond-0.3.13}/Cargo.lock +3 -3
  2. {tea_bond-0.3.11 → tea_bond-0.3.13}/Cargo.toml +1 -1
  3. {tea_bond-0.3.11 → tea_bond-0.3.13}/PKG-INFO +1 -1
  4. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/bond.py +4 -2
  5. {tea_bond-0.3.11/pybond → tea_bond-0.3.13}/pybond/download.py +0 -2
  6. {tea_bond-0.3.11/pybond → tea_bond-0.3.13}/pybond/pd.py +17 -1
  7. {tea_bond-0.3.11/pybond → tea_bond-0.3.13}/pybond/pl.py +19 -0
  8. {tea_bond-0.3.11/pybond → tea_bond-0.3.13}/pybond/pnl.py +30 -0
  9. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/polars_utils.py +3 -1
  10. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/bond.py +4 -2
  11. {tea_bond-0.3.11 → tea_bond-0.3.13/pybond}/pybond/download.py +0 -2
  12. {tea_bond-0.3.11 → tea_bond-0.3.13/pybond}/pybond/pd.py +17 -1
  13. {tea_bond-0.3.11 → tea_bond-0.3.13/pybond}/pybond/pl.py +19 -0
  14. {tea_bond-0.3.11 → tea_bond-0.3.13/pybond}/pybond/pnl.py +30 -0
  15. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/polars_utils.py +3 -1
  16. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/batch_eval.rs +31 -11
  17. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/pnl.rs +76 -34
  18. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/test.py +29 -0
  19. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/bond/mod.rs +1 -0
  20. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/pnl/mod.rs +3 -0
  21. tea_bond-0.3.13/tea-bond/src/pnl/trade_from_signal.rs +112 -0
  22. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/tf_evaluator/evaluator.rs +21 -4
  23. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/.gitignore +0 -0
  24. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/Cargo.toml +0 -0
  25. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/__init__.py +0 -0
  26. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/ffi/__init__.py +0 -0
  27. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/ffi/bond.py +0 -0
  28. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/ffi/datetime.py +0 -0
  29. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/ffi/duration.py +0 -0
  30. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/ffi/evaluators.py +0 -0
  31. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/ffi/lib.py +0 -0
  32. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/nb/__init__.py +0 -0
  33. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/nb/ir_utils.py +0 -0
  34. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/nb/nb_bond.py +0 -0
  35. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/nb/nb_date.py +0 -0
  36. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/nb/nb_datetime.py +0 -0
  37. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/nb/nb_duration.py +0 -0
  38. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/nb/nb_evaluators.py +0 -0
  39. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/nb/nb_time.py +0 -0
  40. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/nb_test.py +0 -0
  41. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/__init__.py +0 -0
  42. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/ffi/__init__.py +0 -0
  43. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/ffi/bond.py +0 -0
  44. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/ffi/datetime.py +0 -0
  45. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/ffi/duration.py +0 -0
  46. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/ffi/evaluators.py +0 -0
  47. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/ffi/lib.py +0 -0
  48. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/nb/__init__.py +0 -0
  49. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/nb/ir_utils.py +0 -0
  50. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/nb/nb_bond.py +0 -0
  51. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/nb/nb_date.py +0 -0
  52. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/nb/nb_datetime.py +0 -0
  53. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/nb/nb_duration.py +0 -0
  54. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/nb/nb_evaluators.py +0 -0
  55. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/nb/nb_time.py +0 -0
  56. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond/pybond.pyi +0 -0
  57. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/pybond.pyi +0 -0
  58. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/bond.rs +0 -0
  59. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/calendar.rs +0 -0
  60. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/ffi/bond.rs +0 -0
  61. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/ffi/datetime.rs +0 -0
  62. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/ffi/duration.rs +0 -0
  63. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/ffi/evaluators.rs +0 -0
  64. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/ffi/mod.rs +0 -0
  65. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/ffi/utils.rs +0 -0
  66. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/future.rs +0 -0
  67. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/lib.rs +0 -0
  68. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/tf_evaluator.rs +0 -0
  69. {tea_bond-0.3.11 → tea_bond-0.3.13}/pybond/src/utils.rs +0 -0
  70. {tea_bond-0.3.11 → tea_bond-0.3.13}/pyproject.toml +0 -0
  71. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/Cargo.toml +0 -0
  72. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/batch/evaluator.rs +0 -0
  73. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/batch/mod.rs +0 -0
  74. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/bond/bond_ytm.rs +0 -0
  75. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/bond/cached_bond.rs +0 -0
  76. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/bond/download/china_money.rs +0 -0
  77. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/bond/download/mod.rs +0 -0
  78. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/bond/download/sse.rs +0 -0
  79. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/bond/enums.rs +0 -0
  80. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/bond/impl_convert.rs +0 -0
  81. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/bond/impl_traits.rs +0 -0
  82. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/bond/io.rs +0 -0
  83. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/day_counter.rs +0 -0
  84. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/export.rs +0 -0
  85. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/future/future_price.rs +0 -0
  86. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/future/future_type.rs +0 -0
  87. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/future/impls.rs +0 -0
  88. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/future/mod.rs +0 -0
  89. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/lib.rs +0 -0
  90. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/pnl/fee.rs +0 -0
  91. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/tf_evaluator/impl_traits.rs +0 -0
  92. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/tf_evaluator/mod.rs +0 -0
  93. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/tf_evaluator/update_with_new_info.rs +0 -0
  94. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-bond/src/utils.rs +0 -0
  95. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-calendar/Cargo.toml +0 -0
  96. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-calendar/src/calendars/china/ib.rs +0 -0
  97. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-calendar/src/calendars/china/mod.rs +0 -0
  98. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-calendar/src/calendars/china/others.rs +0 -0
  99. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-calendar/src/calendars/china/sse.rs +0 -0
  100. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-calendar/src/calendars/mod.rs +0 -0
  101. {tea_bond-0.3.11 → tea_bond-0.3.13}/tea-calendar/src/lib.rs +0 -0
@@ -2195,7 +2195,7 @@ dependencies = [
2195
2195
 
2196
2196
  [[package]]
2197
2197
  name = "pybond"
2198
- version = "0.3.11"
2198
+ version = "0.3.13"
2199
2199
  dependencies = [
2200
2200
  "chrono",
2201
2201
  "itertools 0.14.0",
@@ -3117,7 +3117,7 @@ dependencies = [
3117
3117
 
3118
3118
  [[package]]
3119
3119
  name = "tea-bond"
3120
- version = "0.3.11"
3120
+ version = "0.3.13"
3121
3121
  dependencies = [
3122
3122
  "anyhow",
3123
3123
  "chrono",
@@ -3136,7 +3136,7 @@ dependencies = [
3136
3136
 
3137
3137
  [[package]]
3138
3138
  name = "tea-calendar"
3139
- version = "0.3.11"
3139
+ version = "0.3.13"
3140
3140
  dependencies = [
3141
3141
  "chrono",
3142
3142
  ]
@@ -4,7 +4,7 @@ default-members = ["tea-bond", "tea-calendar"]
4
4
  resolver = "2"
5
5
 
6
6
  [workspace.package]
7
- version = "0.3.11"
7
+ version = "0.3.13"
8
8
  edition = "2024"
9
9
 
10
10
  [workspace.dependencies]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tea-bond
3
- Version: 0.3.11
3
+ Version: 0.3.13
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -1,14 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- from datetime import date
5
4
  from importlib.util import find_spec
6
5
  from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
7
 
8
8
  from .pybond import Bond as _BondRS
9
9
  from .pybond import Future, download_bond
10
10
 
11
- # from .ffi.utils import set_bond_data_path
11
+ if TYPE_CHECKING:
12
+ from datetime import date
13
+
12
14
 
13
15
  WIND_AVAILABLE = find_spec("WindPy") is not None
14
16
 
@@ -8,8 +8,6 @@ from pathlib import Path
8
8
  from WindPy import w
9
9
 
10
10
  default_save_folder = Path("bonds_info")
11
- # if not default_save_folder.exists():
12
- # default_save_folder.mkdir()
13
11
 
14
12
 
15
13
  def save_json(path: Path | str, data: dict) -> None:
@@ -1,12 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
- import pandas as pd
3
+ from typing import TYPE_CHECKING
4
+
4
5
  import polars as pl
5
6
 
6
7
  from .pl import Bonds as PlBonds
7
8
  from .pl import Futures as PlFutures
8
9
  from .pl import TfEvaluators as PlTfEvaluators
9
10
 
11
+ if TYPE_CHECKING:
12
+ import pandas as pd
13
+
10
14
 
11
15
  class TfEvaluators:
12
16
  """
@@ -287,6 +291,12 @@ class TfEvaluators:
287
291
  "last_trading_date"
288
292
  ].to_pandas()
289
293
 
294
+ @property
295
+ def remain_year(self):
296
+ return self.pl_df.select(remain_year=self._evaluators.remain_year)[
297
+ "remain_year"
298
+ ].to_pandas()
299
+
290
300
 
291
301
  class Bonds:
292
302
  """
@@ -383,6 +393,12 @@ class Bonds:
383
393
  "remain_cp_num"
384
394
  ].to_pandas()
385
395
 
396
+ def remain_year(self, date: str | pd.Series):
397
+ df = pl.DataFrame({"bond": self.bond, "date": date})
398
+ return df.select(remain_year=PlBonds("bond").remain_year("date"))[
399
+ "remain_year"
400
+ ].to_pandas()
401
+
386
402
 
387
403
  class Futures:
388
404
  def __init__(self, future: str | pd.Series):
@@ -269,6 +269,19 @@ class TfEvaluators:
269
269
  """
270
270
  return self._call_plugin("evaluators_last_trading_date")
271
271
 
272
+ @property
273
+ def remain_year(self):
274
+ """
275
+ Calculate bond remaining year (债券剩余期限).
276
+
277
+ Args:
278
+ date: Evaluation date column expression
279
+
280
+ Returns:
281
+ Polars expression for bond remaining year
282
+ """
283
+ return self._call_plugin("evaluators_remain_year")
284
+
272
285
 
273
286
  class Bonds:
274
287
  """
@@ -310,6 +323,12 @@ class Bonds:
310
323
  reinvest_rate=None,
311
324
  )
312
325
 
326
+ def remain_year(self, date: IntoExpr = "date"):
327
+ """
328
+ Calculate remain year for the bond (剩余期限).
329
+ """
330
+ return self._evaluator(date=date).remain_year
331
+
313
332
  def accrued_interest(self, date: IntoExpr = "date"):
314
333
  """
315
334
  Calculate accrued interest for the bond (应计利息).
@@ -108,3 +108,33 @@ def calc_bond_trade_pnl(
108
108
  symbol="calc_bond_trade_pnl",
109
109
  is_elementwise=False,
110
110
  )
111
+
112
+
113
+ def trading_from_pos(
114
+ time: IntoExpr,
115
+ pos: IntoExpr,
116
+ open: IntoExpr,
117
+ finish_price: IntoExpr | None = None,
118
+ cash: float = 1e8,
119
+ multiplier: float = 1,
120
+ qty_tick: float = 1.0,
121
+ *,
122
+ stop_on_finish: bool = False,
123
+ ) -> pl.Expr:
124
+ time = parse_into_expr(time)
125
+ pos = parse_into_expr(pos)
126
+ open = parse_into_expr(open)
127
+ finish_price = parse_into_expr(finish_price)
128
+ kwargs = {
129
+ "cash": cash,
130
+ "multiplier": multiplier,
131
+ "qty_tick": qty_tick,
132
+ "stop_on_finish": stop_on_finish,
133
+ "finish_price": None,
134
+ }
135
+ return register_plugin(
136
+ args=[time, pos, open, finish_price],
137
+ kwargs=kwargs,
138
+ symbol="trading_from_pos",
139
+ is_elementwise=False,
140
+ )
@@ -2,11 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  from pathlib import Path
5
- from typing import TYPE_CHECKING, Any, Sequence
5
+ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  import polars as pl
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from collections.abc import Sequence
11
+
10
12
  from polars.type_aliases import IntoExpr, PolarsDataType
11
13
 
12
14
 
@@ -1,14 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- from datetime import date
5
4
  from importlib.util import find_spec
6
5
  from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
7
 
8
8
  from .pybond import Bond as _BondRS
9
9
  from .pybond import Future, download_bond
10
10
 
11
- # from .ffi.utils import set_bond_data_path
11
+ if TYPE_CHECKING:
12
+ from datetime import date
13
+
12
14
 
13
15
  WIND_AVAILABLE = find_spec("WindPy") is not None
14
16
 
@@ -8,8 +8,6 @@ from pathlib import Path
8
8
  from WindPy import w
9
9
 
10
10
  default_save_folder = Path("bonds_info")
11
- # if not default_save_folder.exists():
12
- # default_save_folder.mkdir()
13
11
 
14
12
 
15
13
  def save_json(path: Path | str, data: dict) -> None:
@@ -1,12 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
- import pandas as pd
3
+ from typing import TYPE_CHECKING
4
+
4
5
  import polars as pl
5
6
 
6
7
  from .pl import Bonds as PlBonds
7
8
  from .pl import Futures as PlFutures
8
9
  from .pl import TfEvaluators as PlTfEvaluators
9
10
 
11
+ if TYPE_CHECKING:
12
+ import pandas as pd
13
+
10
14
 
11
15
  class TfEvaluators:
12
16
  """
@@ -287,6 +291,12 @@ class TfEvaluators:
287
291
  "last_trading_date"
288
292
  ].to_pandas()
289
293
 
294
+ @property
295
+ def remain_year(self):
296
+ return self.pl_df.select(remain_year=self._evaluators.remain_year)[
297
+ "remain_year"
298
+ ].to_pandas()
299
+
290
300
 
291
301
  class Bonds:
292
302
  """
@@ -383,6 +393,12 @@ class Bonds:
383
393
  "remain_cp_num"
384
394
  ].to_pandas()
385
395
 
396
+ def remain_year(self, date: str | pd.Series):
397
+ df = pl.DataFrame({"bond": self.bond, "date": date})
398
+ return df.select(remain_year=PlBonds("bond").remain_year("date"))[
399
+ "remain_year"
400
+ ].to_pandas()
401
+
386
402
 
387
403
  class Futures:
388
404
  def __init__(self, future: str | pd.Series):
@@ -269,6 +269,19 @@ class TfEvaluators:
269
269
  """
270
270
  return self._call_plugin("evaluators_last_trading_date")
271
271
 
272
+ @property
273
+ def remain_year(self):
274
+ """
275
+ Calculate bond remaining year (债券剩余期限).
276
+
277
+ Args:
278
+ date: Evaluation date column expression
279
+
280
+ Returns:
281
+ Polars expression for bond remaining year
282
+ """
283
+ return self._call_plugin("evaluators_remain_year")
284
+
272
285
 
273
286
  class Bonds:
274
287
  """
@@ -310,6 +323,12 @@ class Bonds:
310
323
  reinvest_rate=None,
311
324
  )
312
325
 
326
+ def remain_year(self, date: IntoExpr = "date"):
327
+ """
328
+ Calculate remain year for the bond (剩余期限).
329
+ """
330
+ return self._evaluator(date=date).remain_year
331
+
313
332
  def accrued_interest(self, date: IntoExpr = "date"):
314
333
  """
315
334
  Calculate accrued interest for the bond (应计利息).
@@ -108,3 +108,33 @@ def calc_bond_trade_pnl(
108
108
  symbol="calc_bond_trade_pnl",
109
109
  is_elementwise=False,
110
110
  )
111
+
112
+
113
+ def trading_from_pos(
114
+ time: IntoExpr,
115
+ pos: IntoExpr,
116
+ open: IntoExpr,
117
+ finish_price: IntoExpr | None = None,
118
+ cash: float = 1e8,
119
+ multiplier: float = 1,
120
+ qty_tick: float = 1.0,
121
+ *,
122
+ stop_on_finish: bool = False,
123
+ ) -> pl.Expr:
124
+ time = parse_into_expr(time)
125
+ pos = parse_into_expr(pos)
126
+ open = parse_into_expr(open)
127
+ finish_price = parse_into_expr(finish_price)
128
+ kwargs = {
129
+ "cash": cash,
130
+ "multiplier": multiplier,
131
+ "qty_tick": qty_tick,
132
+ "stop_on_finish": stop_on_finish,
133
+ "finish_price": None,
134
+ }
135
+ return register_plugin(
136
+ args=[time, pos, open, finish_price],
137
+ kwargs=kwargs,
138
+ symbol="trading_from_pos",
139
+ is_elementwise=False,
140
+ )
@@ -2,11 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  from pathlib import Path
5
- from typing import TYPE_CHECKING, Any, Sequence
5
+ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  import polars as pl
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from collections.abc import Sequence
11
+
10
12
  from polars.type_aliases import IntoExpr, PolarsDataType
11
13
 
12
14
 
@@ -207,7 +207,7 @@ fn evaluators_net_basis_spread(
207
207
  inputs,
208
208
  kwargs,
209
209
  |e: TfEvaluator| e.with_net_basis_spread().unwrap(),
210
- |e: &TfEvaluator| e.net_basis_spread,
210
+ |e: &TfEvaluator| e.net_basis_spread.filter(|v| !v.is_nan()),
211
211
  true,
212
212
  true,
213
213
  )?
@@ -273,7 +273,7 @@ fn evaluators_dirty_price(inputs: &[Series], kwargs: EvaluatorBatchParams) -> Po
273
273
  inputs,
274
274
  kwargs,
275
275
  |e: TfEvaluator| e.with_dirty_price().unwrap(),
276
- |e: &TfEvaluator| e.dirty_price,
276
+ |e: &TfEvaluator| e.dirty_price.filter(|v| !v.is_nan()),
277
277
  false,
278
278
  true,
279
279
  )?
@@ -288,7 +288,7 @@ fn evaluators_clean_price(inputs: &[Series], kwargs: EvaluatorBatchParams) -> Po
288
288
  inputs,
289
289
  kwargs,
290
290
  |e: TfEvaluator| e.with_clean_price().unwrap(),
291
- |e: &TfEvaluator| e.clean_price,
291
+ |e: &TfEvaluator| e.clean_price.filter(|v| !v.is_nan()),
292
292
  false,
293
293
  true,
294
294
  )?
@@ -306,7 +306,7 @@ fn evaluators_future_dirty_price(
306
306
  inputs,
307
307
  kwargs,
308
308
  |e: TfEvaluator| e.with_future_dirty_price().unwrap(),
309
- |e: &TfEvaluator| e.future_dirty_price,
309
+ |e: &TfEvaluator| e.future_dirty_price.filter(|v| !v.is_nan()),
310
310
  true,
311
311
  true,
312
312
  )?
@@ -324,7 +324,7 @@ fn evaluators_deliver_cost(
324
324
  inputs,
325
325
  kwargs,
326
326
  |e: TfEvaluator| e.with_deliver_cost().unwrap(),
327
- |e: &TfEvaluator| e.deliver_cost,
327
+ |e: &TfEvaluator| e.deliver_cost.filter(|v| !v.is_nan()),
328
328
  true,
329
329
  true,
330
330
  )?
@@ -342,7 +342,7 @@ fn evaluators_basis_spread(
342
342
  inputs,
343
343
  kwargs,
344
344
  |e: TfEvaluator| e.with_basis_spread().unwrap(),
345
- |e: &TfEvaluator| e.basis_spread,
345
+ |e: &TfEvaluator| e.basis_spread.filter(|v| !v.is_nan()),
346
346
  true,
347
347
  true,
348
348
  )?
@@ -357,7 +357,7 @@ fn evaluators_f_b_spread(inputs: &[Series], kwargs: EvaluatorBatchParams) -> Pol
357
357
  inputs,
358
358
  kwargs,
359
359
  |e: TfEvaluator| e.with_f_b_spread().unwrap(),
360
- |e: &TfEvaluator| e.f_b_spread,
360
+ |e: &TfEvaluator| e.f_b_spread.filter(|v| !v.is_nan()),
361
361
  true,
362
362
  true,
363
363
  )?
@@ -372,7 +372,7 @@ fn evaluators_carry(inputs: &[Series], kwargs: EvaluatorBatchParams) -> PolarsRe
372
372
  inputs,
373
373
  kwargs,
374
374
  |e: TfEvaluator| e.with_carry().unwrap(),
375
- |e: &TfEvaluator| e.carry,
375
+ |e: &TfEvaluator| e.carry.filter(|v| !v.is_nan()),
376
376
  true,
377
377
  true,
378
378
  )?
@@ -387,7 +387,7 @@ fn evaluators_duration(inputs: &[Series], kwargs: EvaluatorBatchParams) -> Polar
387
387
  inputs,
388
388
  kwargs,
389
389
  |e: TfEvaluator| e.with_duration().unwrap(),
390
- |e: &TfEvaluator| e.duration,
390
+ |e: &TfEvaluator| e.duration.filter(|v| !v.is_nan()),
391
391
  false,
392
392
  true,
393
393
  )?
@@ -402,7 +402,7 @@ fn evaluators_irr(inputs: &[Series], kwargs: EvaluatorBatchParams) -> PolarsResu
402
402
  inputs,
403
403
  kwargs,
404
404
  |e: TfEvaluator| e.with_irr().unwrap(),
405
- |e: &TfEvaluator| e.irr,
405
+ |e: &TfEvaluator| e.irr.filter(|v| !v.is_nan()),
406
406
  true,
407
407
  true,
408
408
  )?
@@ -417,7 +417,7 @@ fn evaluators_future_ytm(inputs: &[Series], kwargs: EvaluatorBatchParams) -> Pol
417
417
  inputs,
418
418
  kwargs,
419
419
  |e: TfEvaluator| e.with_future_ytm().unwrap(),
420
- |e: &TfEvaluator| e.future_ytm,
420
+ |e: &TfEvaluator| e.future_ytm.filter(|v| !v.is_nan()),
421
421
  true,
422
422
  true,
423
423
  )?
@@ -524,6 +524,26 @@ fn evaluators_last_trading_date(
524
524
  Ok(result.into_date().into_series())
525
525
  }
526
526
 
527
+ #[polars_expr(output_type=Date)]
528
+ fn evaluators_remain_year(
529
+ inputs: &[Series],
530
+ kwargs: EvaluatorBatchParams,
531
+ ) -> PolarsResult<Series> {
532
+ let result: Float64Chunked = batch_eval(
533
+ inputs,
534
+ kwargs,
535
+ |e: TfEvaluator| e,
536
+ |e: &TfEvaluator| {
537
+ Some(e.bond.remain_year(e.date))
538
+ },
539
+ false,
540
+ true,
541
+ )?
542
+ .into_iter()
543
+ .collect_trusted();
544
+ Ok(result.into_series())
545
+ }
546
+
527
547
  #[derive(Deserialize)]
528
548
  struct FindWorkdayKwargs {
529
549
  market: Market,
@@ -86,7 +86,7 @@ pub fn pnl_report_vec_to_series(reports: &[PnlReport]) -> Series {
86
86
  res.into_series()
87
87
  }
88
88
 
89
- fn get_output_type(_input_fields: &[Field]) -> PolarsResult<Field> {
89
+ fn get_pnl_output_type(_input_fields: &[Field]) -> PolarsResult<Field> {
90
90
  let dtype = DataType::Struct(vec![
91
91
  Field::new("pos".into(), DataType::Float64),
92
92
  Field::new("avg_price".into(), DataType::Float64),
@@ -101,57 +101,99 @@ fn get_output_type(_input_fields: &[Field]) -> PolarsResult<Field> {
101
101
  Ok(Field::new("pnl_report".into(), dtype))
102
102
  }
103
103
 
104
- // #[polars_expr(output_type_func=get_output_type)]
105
- // fn calc_bond_trade_pnl(inputs: &[Series], kwargs: BondTradePnlOpt) -> PolarsResult<Series> {
106
- // let (time, qty, clean_price, clean_close) = (&inputs[0], &inputs[1], &inputs[2], &inputs[3]);
107
- // let (qty, clean_price, clean_close) = auto_cast!(Float64(qty, clean_price, clean_close));
108
- // let time = match time.dtype() {
109
- // DataType::Date => time.clone(),
110
- // _ => time.cast(&DataType::Date)?,
111
- // };
112
- // let profit_vec = pnl::calc_bond_trade_pnl(
113
- // time.date()?.physical(),
114
- // qty.f64()?,
115
- // clean_price.f64()?,
116
- // clean_close.f64()?,
117
- // &kwargs,
118
- // );
119
- // let out = pnl_report_vec_to_series(&profit_vec);
120
- // Ok(out)
121
- // }
122
-
123
-
124
-
125
- #[polars_expr(output_type_func=get_output_type)]
104
+ #[polars_expr(output_type_func=get_pnl_output_type)]
126
105
  fn calc_bond_trade_pnl(inputs: &[Series], kwargs: BondTradePnlOpt) -> PolarsResult<Series> {
127
- let (symbol, time, qty, clean_price, clean_close) = (&inputs[0], &inputs[1], &inputs[2], &inputs[3], &inputs[4]);
106
+ let (symbol, time, qty, clean_price, clean_close) =
107
+ (&inputs[0], &inputs[1], &inputs[2], &inputs[3], &inputs[4]);
128
108
  let symbol = auto_cast!(String(symbol));
129
109
  let symbol = if let Some(s) = symbol.str()?.iter().next() {
130
110
  s
131
111
  } else {
132
- return Ok(pnl_report_vec_to_series(&[]))
112
+ return Ok(pnl_report_vec_to_series(&[]));
133
113
  };
134
114
  let (qty, clean_price, clean_close) = auto_cast!(Float64(qty, clean_price, clean_close));
135
115
  let time = match time.dtype() {
136
116
  DataType::Date => time.clone(),
137
117
  _ => time.cast(&DataType::Date)?,
138
118
  };
139
- let opt = BondTradePnlOpt {
140
- bond_info_path: kwargs.bond_info_path,
141
- multiplier: kwargs.multiplier,
142
- fee: kwargs.fee,
143
- borrowing_cost: kwargs.borrowing_cost,
144
- capital_rate: kwargs.capital_rate,
145
- begin_state: kwargs.begin_state
146
- };
147
119
  let profit_vec = pnl::calc_bond_trade_pnl(
148
120
  symbol,
149
121
  time.date()?.physical(),
150
122
  qty.f64()?,
151
123
  clean_price.f64()?,
152
124
  clean_close.f64()?,
153
- &opt,
125
+ &kwargs,
154
126
  );
155
127
  let out = pnl_report_vec_to_series(&profit_vec);
156
128
  Ok(out)
157
129
  }
130
+
131
+ fn get_trading_output_type(input_fields: &[Field]) -> PolarsResult<Field> {
132
+ let dtype = DataType::Struct(vec![
133
+ input_fields[0].clone(), // time
134
+ Field::new("price".into(), DataType::Float64),
135
+ Field::new("qty".into(), DataType::Float64),
136
+ ]);
137
+ Ok(Field::new("pnl_report".into(), dtype))
138
+ }
139
+
140
+ #[polars_expr(output_type_func=get_trading_output_type)]
141
+ fn trading_from_pos(inputs: &[Series], mut kwargs: pnl::TradeFromPosOpt) -> PolarsResult<Series> {
142
+ use pyo3_polars::export::polars_core::utils::CustomIterTools;
143
+ use tevec::export::polars::prelude::*;
144
+ let (time, pos, open, finish_price) = (&inputs[0], &inputs[1], &inputs[2], &inputs[3]);
145
+ let (pos, open, finish_price) = auto_cast!(Float64(pos, open, finish_price));
146
+ if let Some(p) = finish_price.f64()?.iter().next() {
147
+ kwargs.finish_price = p
148
+ };
149
+ let res = match time.dtype() {
150
+ DataType::Date => {
151
+ let trade_vec =
152
+ pnl::trading_from_pos(time.date()?.physical(), pos.f64()?, open.f64()?, &kwargs);
153
+ let time: Int32Chunked = trade_vec.iter().map(|t| t.time).collect_trusted();
154
+ let time = time.into_date().into_series();
155
+ let price: Float64Chunked = trade_vec.iter().map(|t| Some(t.price)).collect_trusted();
156
+ let price = price.into_series();
157
+ let qty: Float64Chunked = trade_vec.iter().map(|t| Some(t.qty)).collect_trusted();
158
+ StructChunked::from_series(
159
+ "trade".into(),
160
+ time.len(),
161
+ [
162
+ time.with_name("time".into()),
163
+ price.into_series().with_name("price".into()),
164
+ qty.into_series().with_name("qty".into()),
165
+ ]
166
+ .iter(),
167
+ )
168
+ .unwrap()
169
+ .into_series()
170
+ }
171
+ _ => {
172
+ let time_ca = time.datetime()?;
173
+ let time_unit = time_ca.time_unit();
174
+ let time_zone = time_ca.time_zone();
175
+ let trade_vec =
176
+ pnl::trading_from_pos(time_ca.physical(), pos.f64()?, open.f64()?, &kwargs);
177
+ let time: Int64Chunked = trade_vec.iter().map(|t| t.time).collect_trusted();
178
+ let time = time
179
+ .into_datetime(time_unit, time_zone.clone())
180
+ .into_series();
181
+ let price: Float64Chunked = trade_vec.iter().map(|t| Some(t.price)).collect_trusted();
182
+ let price = price.into_series();
183
+ let qty: Float64Chunked = trade_vec.iter().map(|t| Some(t.qty)).collect_trusted();
184
+ StructChunked::from_series(
185
+ "trade".into(),
186
+ time.len(),
187
+ [
188
+ time.with_name("time".into()),
189
+ price.into_series().with_name("price".into()),
190
+ qty.into_series().with_name("qty".into()),
191
+ ]
192
+ .iter(),
193
+ )
194
+ .unwrap()
195
+ .into_series()
196
+ }
197
+ };
198
+ Ok(res)
199
+ }
@@ -2,6 +2,7 @@ import datetime
2
2
 
3
3
  import numpy as np
4
4
  import polars as pl
5
+ from IPython.display import display
5
6
 
6
7
  # import os
7
8
  # os.environ["POLARS_VERBOSE"] = "1"
@@ -10,6 +11,34 @@ from pybond.pd import Bonds as PdBonds
10
11
  from pybond.pd import TfEvaluators as PdTfEvaluators
11
12
  from pybond.pd import find_workday as pd_find_workday
12
13
  from pybond.pl import Bonds, TfEvaluators, find_workday, is_business_day
14
+ from pybond.pnl import trading_from_pos
15
+
16
+ signal_df = (
17
+ pl.DataFrame(
18
+ {
19
+ "time": pl.date_range(
20
+ start=datetime.date(2025, 8, 1),
21
+ end=datetime.date(2025, 8, 8),
22
+ eager=True,
23
+ ),
24
+ "pos": [1.0, 0.5, 0.2, 0.4, 0, -0.1, -0.1, 1],
25
+ "price": [100, 101, 102, 103, 104, 105, 106, 107],
26
+ }
27
+ )
28
+ .select(
29
+ trading_from_pos(
30
+ pl.col("time").cast(pl.Datetime("ms")),
31
+ "pos",
32
+ "price",
33
+ finish_price=110,
34
+ cash=10000,
35
+ stop_on_finish=True,
36
+ )
37
+ )
38
+ .unnest("time")
39
+ .with_columns(cum_qty=pl.col("qty").cum_sum())
40
+ )
41
+ display(signal_df)
13
42
 
14
43
  e = TfEvaluator("T2509", 250205, "2025-07-15", 100, 0.02, 0.018)
15
44
  e.net_basis_spread
@@ -115,6 +115,7 @@ impl Bond {
115
115
  0 => Ok(Months::new(0)),
116
116
  1 => Ok(Months::new(12)),
117
117
  2 => Ok(Months::new(6)),
118
+ 4 => Ok(Months::new(3)),
118
119
  _ => bail!("Invalid inst_freq: {}", self.inst_freq),
119
120
  }
120
121
  }
@@ -1,4 +1,7 @@
1
1
  mod fee;
2
+ mod trade_from_signal;
3
+
4
+ pub use trade_from_signal::{TradeFromPosOpt, trading_from_pos};
2
5
 
3
6
  use std::path::PathBuf;
4
7
 
@@ -0,0 +1,112 @@
1
+ use itertools::izip;
2
+ use serde::Deserialize;
3
+ use tevec::prelude::{EPS, IsNone, Number, Vec1View};
4
+ // use super::EPOCH;
5
+
6
+ pub struct Trade<T> {
7
+ pub time: T,
8
+ pub price: f64,
9
+ pub qty: f64,
10
+ }
11
+
12
+ #[derive(Deserialize)]
13
+ pub struct TradeFromPosOpt {
14
+ // pub symbol: SmallStr,
15
+ pub cash: f64,
16
+ pub multiplier: f64,
17
+ pub qty_tick: f64,
18
+ pub stop_on_finish: bool,
19
+ pub finish_price: Option<f64>,
20
+ }
21
+
22
+ const INIT_TRADE_COUNT: usize = 512;
23
+
24
+ fn quantize_inside(q: f64, tick: f64) -> f64 {
25
+ if tick <= 0.0 {
26
+ return q;
27
+ }
28
+ if q >= 0.0 {
29
+ (q / tick).floor() * tick // 买单向下取整
30
+ } else {
31
+ (q / tick).ceil() * tick // 卖单向上取整(数值更接近 0)
32
+ }
33
+ }
34
+
35
+ pub fn trading_from_pos<DT, T, VT, V>(
36
+ time_vec: &VT,
37
+ pos_vec: &V,
38
+ open_vec: &V,
39
+ opt: &TradeFromPosOpt,
40
+ ) -> Vec<Trade<DT>>
41
+ where
42
+ DT: Clone,
43
+ T: IsNone,
44
+ T::Inner: Number,
45
+ VT: Vec1View<DT>,
46
+ V: Vec1View<T>,
47
+ {
48
+ let mut last_pos = 0.;
49
+ let mut open_price: f64 = 0.;
50
+ let mut open_qty: f64 = 0.;
51
+ let mut trades = Vec::with_capacity(INIT_TRADE_COUNT);
52
+
53
+ // 记录最后一个可用 (time, price),用于 stop_on_finish
54
+ let mut last_tp: Option<(DT, f64)> = None;
55
+
56
+ izip!(time_vec.titer(), pos_vec.titer(), open_vec.titer()).for_each(|(time, pos, open)| {
57
+ if pos.not_none() && open.not_none() {
58
+ let pos = pos.unwrap().f64();
59
+ let price = open.unwrap().f64();
60
+
61
+ // 记录最新可用的时间与价格
62
+ last_tp = Some((time.clone(), price));
63
+
64
+ let dpos = pos - last_pos;
65
+ if dpos.abs() > EPS {
66
+ // 目标名义 -> 成交量(正买负卖)
67
+ let qty = if pos.abs() > EPS {
68
+ let p = if open_price > 0. { open_price } else { price };
69
+ let raw_qty = dpos * opt.cash / (p * opt.multiplier);
70
+ // 量化到最小变动单位(朝 0 截断,避免超买/超卖)
71
+ quantize_inside(raw_qty, opt.qty_tick)
72
+ } else {
73
+ -open_qty
74
+ };
75
+ if dpos.signum() == open_qty.signum() {
76
+ open_price = (open_price * open_qty + qty * price) / (qty + open_qty)
77
+ } else if open_qty.abs() > qty.abs() {
78
+ // 反向加仓, 价格为新的开仓价格
79
+ open_price = price
80
+ };
81
+ // 减仓情况的价格不改变
82
+
83
+ // 若量化后仍非 0,则下单
84
+ if qty.abs() > 0.0 {
85
+ trades.push(Trade {
86
+ time: time.clone(),
87
+ price,
88
+ qty,
89
+ });
90
+ open_qty += qty;
91
+ }
92
+ }
93
+
94
+ // 维持 last_pos 为目标(策略)层面的持仓比例/规模
95
+ last_pos = pos;
96
+ }
97
+ });
98
+
99
+ // 收尾是否强制平仓
100
+ if opt.stop_on_finish && open_qty != 0.0 {
101
+ if let Some((t, p)) = last_tp {
102
+ let p = if let Some(p) = opt.finish_price { p } else { p };
103
+ trades.push(Trade {
104
+ time: t,
105
+ price: p,
106
+ qty: -open_qty,
107
+ });
108
+ }
109
+ }
110
+
111
+ trades
112
+ }
@@ -139,8 +139,12 @@ impl TfEvaluator {
139
139
  }
140
140
  /// 计算剩余付息次数
141
141
  #[inline]
142
- pub fn with_remain_cp_num(self) -> Result<Self> {
142
+ pub fn with_remain_cp_num(mut self) -> Result<Self> {
143
143
  if self.remain_cp_num.is_none() {
144
+ if self.bond.is_zero_coupon() {
145
+ self.remain_cp_num = Some(0);
146
+ return Ok(self);
147
+ }
144
148
  let mut out = self.with_nearest_cp_dates()?;
145
149
  out.remain_cp_num = Some(
146
150
  out.bond
@@ -154,8 +158,12 @@ impl TfEvaluator {
154
158
 
155
159
  /// 计算应计利息
156
160
  #[inline]
157
- pub fn with_accrued_interest(self) -> Result<Self> {
161
+ pub fn with_accrued_interest(mut self) -> Result<Self> {
158
162
  if self.accrued_interest.is_none() {
163
+ if self.bond.is_zero_coupon() {
164
+ self.accrued_interest = Some(0.);
165
+ return Ok(self);
166
+ }
159
167
  let mut out = self.with_nearest_cp_dates()?;
160
168
  out.accrued_interest = Some(
161
169
  out.bond
@@ -169,8 +177,13 @@ impl TfEvaluator {
169
177
 
170
178
  /// 计算债券全价
171
179
  #[inline]
172
- pub fn with_dirty_price(self) -> Result<Self> {
180
+ pub fn with_dirty_price(mut self) -> Result<Self> {
173
181
  if self.dirty_price.is_none() {
182
+ if self.bond.is_zero_coupon() {
183
+ let remain_year = self.bond.remain_year(self.date);
184
+ self.dirty_price = Some(100.0 / (1.0+self.bond.ytm()).powf(remain_year));
185
+ return Ok(self);
186
+ }
174
187
  let mut out = self.with_remain_cp_num()?;
175
188
  out.dirty_price = Some(out.bond.calc_dirty_price_with_ytm(
176
189
  out.bond.ytm(),
@@ -198,8 +211,12 @@ impl TfEvaluator {
198
211
 
199
212
  /// 计算久期
200
213
  #[inline]
201
- pub fn with_duration(self) -> Result<Self> {
214
+ pub fn with_duration(mut self) -> Result<Self> {
202
215
  if self.duration.is_none() {
216
+ if self.bond.is_zero_coupon() {
217
+ self.duration = Some(self.bond.remain_year(self.date));
218
+ return Ok(self);
219
+ }
203
220
  let mut out = self.with_remain_cp_num()?;
204
221
  out.duration = Some(out.bond.calc_duration(
205
222
  out.bond.ytm(),
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
File without changes
File without changes
File without changes