quantark 0.1.0__py3-none-any.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.
Files changed (399) hide show
  1. quantark/__init__.py +3 -0
  2. quantark/_compat.py +150 -0
  3. quantark/asset/__init__.py +8 -0
  4. quantark/asset/bond/__init__.py +2 -0
  5. quantark/asset/bond/engine/__init__.py +44 -0
  6. quantark/asset/bond/engine/analytical/__init__.py +12 -0
  7. quantark/asset/bond/engine/analytical/black_engine.py +583 -0
  8. quantark/asset/bond/engine/analytical/bond_forward_engine.py +390 -0
  9. quantark/asset/bond/engine/analytical/bond_futures_engine.py +569 -0
  10. quantark/asset/bond/engine/convertible/__init__.py +12 -0
  11. quantark/asset/bond/engine/convertible/convertible_bond_engine.py +800 -0
  12. quantark/asset/bond/engine/discount/__init__.py +10 -0
  13. quantark/asset/bond/engine/discount/bond_discount_engine.py +517 -0
  14. quantark/asset/bond/engine/discount/frn_engine.py +913 -0
  15. quantark/asset/bond/engine/pde/__init__.py +14 -0
  16. quantark/asset/bond/engine/pde/convertible/__init__.py +21 -0
  17. quantark/asset/bond/engine/pde/convertible/jump_diffusion_engine.py +603 -0
  18. quantark/asset/bond/engine/pde/convertible/pde_params.py +59 -0
  19. quantark/asset/bond/engine/pde/convertible/tf_engine.py +546 -0
  20. quantark/asset/bond/engine/tree/__init__.py +14 -0
  21. quantark/asset/bond/engine/tree/convertible/__init__.py +21 -0
  22. quantark/asset/bond/engine/tree/convertible/binomial_engine.py +488 -0
  23. quantark/asset/bond/engine/tree/convertible/tree_params.py +72 -0
  24. quantark/asset/bond/engine/tree/convertible/trinomial_engine.py +1341 -0
  25. quantark/asset/bond/product/__init__.py +37 -0
  26. quantark/asset/bond/product/base_bond_product.py +114 -0
  27. quantark/asset/bond/product/convertible/__init__.py +16 -0
  28. quantark/asset/bond/product/convertible/convertible_bond.py +595 -0
  29. quantark/asset/bond/product/couponbond/__init__.py +12 -0
  30. quantark/asset/bond/product/couponbond/fixed_bond.py +285 -0
  31. quantark/asset/bond/product/couponbond/frn.py +538 -0
  32. quantark/asset/bond/product/forward/__init__.py +9 -0
  33. quantark/asset/bond/product/forward/base_bond_forward.py +92 -0
  34. quantark/asset/bond/product/forward/bond_forward.py +335 -0
  35. quantark/asset/bond/product/futures/__init__.py +8 -0
  36. quantark/asset/bond/product/futures/bond_futures.py +532 -0
  37. quantark/asset/bond/product/option/__init__.py +9 -0
  38. quantark/asset/bond/product/option/euro_short_term_bond_option.py +231 -0
  39. quantark/asset/bond/riskmeasures/__init__.py +13 -0
  40. quantark/asset/bond/riskmeasures/bond_greeks_calculator.py +484 -0
  41. quantark/asset/bond/schedule/__init__.py +21 -0
  42. quantark/asset/bond/schedule/cashflow.py +595 -0
  43. quantark/asset/equity/__init__.py +11 -0
  44. quantark/asset/equity/analysis/__init__.py +4 -0
  45. quantark/asset/equity/analysis/autocallable_path_analyzer.py +257 -0
  46. quantark/asset/equity/engine/__init__.py +84 -0
  47. quantark/asset/equity/engine/analytical/__init__.py +37 -0
  48. quantark/asset/equity/engine/analytical/american_option_engine.py +682 -0
  49. quantark/asset/equity/engine/analytical/asian_option_analytical_engine.py +1102 -0
  50. quantark/asset/equity/engine/analytical/barrier_analytical_engine.py +455 -0
  51. quantark/asset/equity/engine/analytical/black_scholes_engine.py +322 -0
  52. quantark/asset/equity/engine/analytical/deltaone_engine.py +340 -0
  53. quantark/asset/equity/engine/analytical/digital_option_engine.py +168 -0
  54. quantark/asset/equity/engine/analytical/double_barrier_option_engine.py +481 -0
  55. quantark/asset/equity/engine/analytical/double_sharkfin_option_analytical_engine.py +508 -0
  56. quantark/asset/equity/engine/analytical/one_touch_analytical_engine.py +302 -0
  57. quantark/asset/equity/engine/analytical/range_accrual_analytical_engine.py +396 -0
  58. quantark/asset/equity/engine/analytical/single_sharkfin_option_analytical_engine.py +229 -0
  59. quantark/asset/equity/engine/base_engine.py +137 -0
  60. quantark/asset/equity/engine/event_stats.py +85 -0
  61. quantark/asset/equity/engine/mc/__init__.py +31 -0
  62. quantark/asset/equity/engine/mc/american_option_mc_engine.py +485 -0
  63. quantark/asset/equity/engine/mc/asian_option_mc_engine.py +678 -0
  64. quantark/asset/equity/engine/mc/barrier_option_mc_engine.py +726 -0
  65. quantark/asset/equity/engine/mc/digital_option_mc_engine.py +419 -0
  66. quantark/asset/equity/engine/mc/double_sharkfin_option_mc_engine.py +676 -0
  67. quantark/asset/equity/engine/mc/euro_mc_engine.py +423 -0
  68. quantark/asset/equity/engine/mc/phoenix_mc_engine.py +1206 -0
  69. quantark/asset/equity/engine/mc/range_accrual_mc_engine.py +738 -0
  70. quantark/asset/equity/engine/mc/single_sharkfin_option_mc_engine.py +549 -0
  71. quantark/asset/equity/engine/mc/snowball_mc_engine.py +2250 -0
  72. quantark/asset/equity/engine/pde/__init__.py +36 -0
  73. quantark/asset/equity/engine/pde/american_pde_solver.py +211 -0
  74. quantark/asset/equity/engine/pde/barrier_pde_solver.py +692 -0
  75. quantark/asset/equity/engine/pde/base_pde_solver.py +994 -0
  76. quantark/asset/equity/engine/pde/double_barrier_pde_solver.py +510 -0
  77. quantark/asset/equity/engine/pde/double_one_touch_pde_solver.py +435 -0
  78. quantark/asset/equity/engine/pde/european_pde_solver.py +170 -0
  79. quantark/asset/equity/engine/pde/ko_reset_snowball_pde_solver.py +477 -0
  80. quantark/asset/equity/engine/pde/one_touch_pde_solver.py +439 -0
  81. quantark/asset/equity/engine/pde/phoenix_pde_solver.py +613 -0
  82. quantark/asset/equity/engine/pde/snowball_pde_solver.py +1810 -0
  83. quantark/asset/equity/engine/pde/spatial_grid.py +750 -0
  84. quantark/asset/equity/engine/pde/time_grid.py +308 -0
  85. quantark/asset/equity/engine/pde_engine.py +238 -0
  86. quantark/asset/equity/engine/quad/__init__.py +23 -0
  87. quantark/asset/equity/engine/quad/discrete_quad_engine.py +106 -0
  88. quantark/asset/equity/engine/quad/european_quad_engine.py +325 -0
  89. quantark/asset/equity/engine/quad/ko_reset_snowball_quad_engine.py +362 -0
  90. quantark/asset/equity/engine/quad/phoenix_quad_engine.py +614 -0
  91. quantark/asset/equity/engine/quad/quad_adapters.py +1260 -0
  92. quantark/asset/equity/engine/quad/quad_core.py +513 -0
  93. quantark/asset/equity/engine/quad/quad_math.py +219 -0
  94. quantark/asset/equity/engine/quad/snowball_quad_engine.py +1137 -0
  95. quantark/asset/equity/engine/validation/script/benchmark_check_american_analytical.py +117 -0
  96. quantark/asset/equity/engine/validation/script/benchmark_check_american_pde.py +114 -0
  97. quantark/asset/equity/engine/validation/script/benchmark_check_asian_analytical.py +440 -0
  98. quantark/asset/equity/engine/validation/script/benchmark_check_barrier_analytical.py +269 -0
  99. quantark/asset/equity/engine/validation/script/benchmark_check_barrier_pde_solver.py +636 -0
  100. quantark/asset/equity/engine/validation/script/benchmark_check_digital_option.py +256 -0
  101. quantark/asset/equity/engine/validation/script/benchmark_check_snowball_pde_solver.py +807 -0
  102. quantark/asset/equity/engine/validation/script/boundary_check_american_analytical.py +290 -0
  103. quantark/asset/equity/engine/validation/script/boundary_check_american_pde.py +242 -0
  104. quantark/asset/equity/engine/validation/script/boundary_check_asian_analytical.py +612 -0
  105. quantark/asset/equity/engine/validation/script/boundary_check_barrier_analytical.py +434 -0
  106. quantark/asset/equity/engine/validation/script/boundary_check_barrier_pde_solver.py +748 -0
  107. quantark/asset/equity/engine/validation/script/boundary_check_digital_option.py +575 -0
  108. quantark/asset/equity/engine/validation/script/boundary_check_snowball_pde_solver.py +1101 -0
  109. quantark/asset/equity/engine/validation/script/greeks_check_digital_option.py +349 -0
  110. quantark/asset/equity/engine/validation/script/mc_comparison_barrier_pde.py +270 -0
  111. quantark/asset/equity/engine/validation/script/quick_mc_compare.py +51 -0
  112. quantark/asset/equity/engine/validation/script/validation_stepdown_improved.py +97 -0
  113. quantark/asset/equity/param/__init__.py +24 -0
  114. quantark/asset/equity/param/engine_param_profiles.py +325 -0
  115. quantark/asset/equity/param/engine_params.py +728 -0
  116. quantark/asset/equity/process/__init__.py +7 -0
  117. quantark/asset/equity/process/bsm/__init__.py +7 -0
  118. quantark/asset/equity/process/bsm/bsm_process.py +108 -0
  119. quantark/asset/equity/process/bsm/qmc_brownian_bridge.py +401 -0
  120. quantark/asset/equity/process/bsm/qmc_path_generator.py +694 -0
  121. quantark/asset/equity/process/bsm/qmc_rqmc_driver.py +163 -0
  122. quantark/asset/equity/process/bsm/qmc_sobol.py +195 -0
  123. quantark/asset/equity/process/bsm/qmc_variance_reduction.py +292 -0
  124. quantark/asset/equity/product/__init__.py +8 -0
  125. quantark/asset/equity/product/base_equity_product.py +72 -0
  126. quantark/asset/equity/product/deltaone/__init__.py +22 -0
  127. quantark/asset/equity/product/deltaone/base_deltaone_product.py +147 -0
  128. quantark/asset/equity/product/deltaone/futures.py +485 -0
  129. quantark/asset/equity/product/deltaone/spot_instrument.py +118 -0
  130. quantark/asset/equity/product/option/__init__.py +104 -0
  131. quantark/asset/equity/product/option/american_option.py +114 -0
  132. quantark/asset/equity/product/option/asian_option.py +531 -0
  133. quantark/asset/equity/product/option/barrier_option.py +289 -0
  134. quantark/asset/equity/product/option/base_equity_option.py +659 -0
  135. quantark/asset/equity/product/option/digital_option.py +102 -0
  136. quantark/asset/equity/product/option/double_barrier_option.py +286 -0
  137. quantark/asset/equity/product/option/double_one_touch_option.py +310 -0
  138. quantark/asset/equity/product/option/double_sharkfin_option.py +466 -0
  139. quantark/asset/equity/product/option/european_vanilla_option.py +103 -0
  140. quantark/asset/equity/product/option/ko_reset_snowball_option.py +563 -0
  141. quantark/asset/equity/product/option/observation_schedule.py +530 -0
  142. quantark/asset/equity/product/option/one_touch_option.py +287 -0
  143. quantark/asset/equity/product/option/phoenix_config.py +116 -0
  144. quantark/asset/equity/product/option/phoenix_helpers.py +576 -0
  145. quantark/asset/equity/product/option/phoenix_option.py +1167 -0
  146. quantark/asset/equity/product/option/range_accrual_config.py +288 -0
  147. quantark/asset/equity/product/option/range_accrual_helpers.py +608 -0
  148. quantark/asset/equity/product/option/range_accrual_option.py +526 -0
  149. quantark/asset/equity/product/option/single_sharkfin_option.py +420 -0
  150. quantark/asset/equity/product/option/snowball_config.py +261 -0
  151. quantark/asset/equity/product/option/snowball_helpers.py +977 -0
  152. quantark/asset/equity/product/option/snowball_option.py +1242 -0
  153. quantark/asset/equity/report/__init__.py +15 -0
  154. quantark/asset/equity/report/autocallable_risk_report.py +2118 -0
  155. quantark/asset/equity/report/plotting.py +87 -0
  156. quantark/asset/equity/report/snowball_risk_comparison_report.py +2230 -0
  157. quantark/asset/equity/report/surfaces.py +123 -0
  158. quantark/asset/equity/report/term_structure.py +126 -0
  159. quantark/asset/equity/riskmeasures/__init__.py +7 -0
  160. quantark/asset/equity/riskmeasures/greeks_calculator.py +1204 -0
  161. quantark/asset/rate/__init__.py +58 -0
  162. quantark/asset/rate/engine/__init__.py +25 -0
  163. quantark/asset/rate/engine/cap_floor_engine.py +514 -0
  164. quantark/asset/rate/engine/fra_engine.py +286 -0
  165. quantark/asset/rate/engine/irs_discount_engine.py +891 -0
  166. quantark/asset/rate/engine/swaption_engine.py +587 -0
  167. quantark/asset/rate/product/__init__.py +67 -0
  168. quantark/asset/rate/product/cap_floor.py +550 -0
  169. quantark/asset/rate/product/fra.py +219 -0
  170. quantark/asset/rate/product/irs.py +1223 -0
  171. quantark/asset/rate/product/swaption.py +372 -0
  172. quantark/backtest/__init__.py +153 -0
  173. quantark/backtest/base.py +263 -0
  174. quantark/backtest/dashboard.py +874 -0
  175. quantark/backtest/equity/__init__.py +35 -0
  176. quantark/backtest/equity/config.py +118 -0
  177. quantark/backtest/equity/engine.py +408 -0
  178. quantark/backtest/equity/hedge_executor.py +374 -0
  179. quantark/backtest/equity/metrics.py +396 -0
  180. quantark/backtest/equity/results.py +232 -0
  181. quantark/backtest/equity/state.py +252 -0
  182. quantark/backtest/examples/__init__.py +4 -0
  183. quantark/backtest/examples/advanced_backtest.py +345 -0
  184. quantark/backtest/examples/basic_delta_hedge.py +246 -0
  185. quantark/backtest/examples/fi_dv01_hedge.py +267 -0
  186. quantark/backtest/fi/__init__.py +30 -0
  187. quantark/backtest/fi/config.py +114 -0
  188. quantark/backtest/fi/engine.py +378 -0
  189. quantark/backtest/fi/hedge_executor.py +254 -0
  190. quantark/backtest/fi/metrics.py +308 -0
  191. quantark/backtest/fi/results.py +193 -0
  192. quantark/backtest/fi/state.py +212 -0
  193. quantark/backtest/logger.py +393 -0
  194. quantark/backtest/otc/__init__.py +74 -0
  195. quantark/backtest/otc/_replay.py +637 -0
  196. quantark/backtest/otc/book_engine.py +587 -0
  197. quantark/backtest/otc/config.py +175 -0
  198. quantark/backtest/otc/dashboard.py +1006 -0
  199. quantark/backtest/otc/engine.py +420 -0
  200. quantark/backtest/otc/engine_factory.py +138 -0
  201. quantark/backtest/otc/market.py +216 -0
  202. quantark/backtest/otc/results.py +107 -0
  203. quantark/backtest/otc/state.py +166 -0
  204. quantark/backtest/report_generator.py +608 -0
  205. quantark/backtest/strategy/__init__.py +28 -0
  206. quantark/backtest/strategy/base_strategy.py +235 -0
  207. quantark/backtest/strategy/convexity_neutral_strategy.py +247 -0
  208. quantark/backtest/strategy/delta_neutral_strategy.py +283 -0
  209. quantark/backtest/strategy/dv01_neutral_strategy.py +283 -0
  210. quantark/backtest/transaction_costs.py +485 -0
  211. quantark/backtest/visualizer.py +1019 -0
  212. quantark/cashleg/__init__.py +31 -0
  213. quantark/cashleg/accrual_leg.py +120 -0
  214. quantark/cashleg/base.py +48 -0
  215. quantark/cashleg/base_amount.py +60 -0
  216. quantark/cashleg/deterministic_leg.py +39 -0
  217. quantark/cashleg/event_distribution.py +262 -0
  218. quantark/cashleg/fixed_payoff_leg.py +92 -0
  219. quantark/cashleg/leg_schedule.py +95 -0
  220. quantark/cashleg/leg_valuator.py +40 -0
  221. quantark/dynamicscenario/__init__.py +97 -0
  222. quantark/dynamicscenario/base.py +297 -0
  223. quantark/dynamicscenario/config.py +122 -0
  224. quantark/dynamicscenario/engine.py +703 -0
  225. quantark/dynamicscenario/equity/__init__.py +14 -0
  226. quantark/dynamicscenario/fi/__init__.py +24 -0
  227. quantark/dynamicscenario/fi/config.py +149 -0
  228. quantark/dynamicscenario/fi/engine.py +500 -0
  229. quantark/dynamicscenario/fi/results.py +503 -0
  230. quantark/dynamicscenario/path/__init__.py +17 -0
  231. quantark/dynamicscenario/path/day_path.py +397 -0
  232. quantark/dynamicscenario/path/fi_path_library.py +488 -0
  233. quantark/dynamicscenario/path/path_builder.py +726 -0
  234. quantark/dynamicscenario/path/path_library.py +620 -0
  235. quantark/dynamicscenario/report/__init__.py +12 -0
  236. quantark/dynamicscenario/report/dynamic_report.py +1175 -0
  237. quantark/dynamicscenario/report/visualizer.py +1586 -0
  238. quantark/dynamicscenario/results/__init__.py +19 -0
  239. quantark/dynamicscenario/results/dynamic_results.py +579 -0
  240. quantark/dynamicscenario/results/result_exporter.py +438 -0
  241. quantark/param/__init__.py +75 -0
  242. quantark/param/basis/__init__.py +19 -0
  243. quantark/param/basis/basis_yield.py +301 -0
  244. quantark/param/div/__init__.py +16 -0
  245. quantark/param/div/dividend_yield.py +123 -0
  246. quantark/param/index/__init__.py +52 -0
  247. quantark/param/index/rate_index.py +568 -0
  248. quantark/param/quote/__init__.py +7 -0
  249. quantark/param/quote/spot_quote.py +35 -0
  250. quantark/param/rrf/__init__.py +22 -0
  251. quantark/param/rrf/rate_curve.py +436 -0
  252. quantark/param/vol/__init__.py +6 -0
  253. quantark/param/vol/vol_surface.py +118 -0
  254. quantark/portfolio/__init__.py +61 -0
  255. quantark/portfolio/base.py +203 -0
  256. quantark/portfolio/equity/__init__.py +17 -0
  257. quantark/portfolio/equity/portfolio.py +391 -0
  258. quantark/portfolio/equity/position.py +368 -0
  259. quantark/portfolio/fi/__init__.py +14 -0
  260. quantark/portfolio/fi/portfolio.py +424 -0
  261. quantark/portfolio/fi/position.py +272 -0
  262. quantark/portfolio/portfolio_snapshot.py +221 -0
  263. quantark/portfolio/portfolio_storage.py +414 -0
  264. quantark/priceenv/__init__.py +7 -0
  265. quantark/priceenv/pricing_environment.py +196 -0
  266. quantark/rfq/__init__.py +32 -0
  267. quantark/rfq/builders.py +102 -0
  268. quantark/rfq/models.py +214 -0
  269. quantark/rfq/registry.py +611 -0
  270. quantark/rfq/service.py +237 -0
  271. quantark/simm/__init__.py +155 -0
  272. quantark/simm/calibration/__init__.py +206 -0
  273. quantark/simm/calibration/accessors.py +439 -0
  274. quantark/simm/calibration/commodity.py +156 -0
  275. quantark/simm/calibration/credit_non_qualifying.py +79 -0
  276. quantark/simm/calibration/credit_qualifying.py +130 -0
  277. quantark/simm/calibration/cross_risk.py +39 -0
  278. quantark/simm/calibration/equity.py +125 -0
  279. quantark/simm/calibration/fx.py +92 -0
  280. quantark/simm/calibration/ir.py +152 -0
  281. quantark/simm/calibration/version.py +33 -0
  282. quantark/simm/config.py +186 -0
  283. quantark/simm/crif/__init__.py +35 -0
  284. quantark/simm/crif/models.py +230 -0
  285. quantark/simm/crif/parser.py +585 -0
  286. quantark/simm/engines/__init__.py +62 -0
  287. quantark/simm/engines/aggregation/__init__.py +67 -0
  288. quantark/simm/engines/aggregation/addon.py +141 -0
  289. quantark/simm/engines/aggregation/bucket_aggregator.py +298 -0
  290. quantark/simm/engines/aggregation/concentration.py +349 -0
  291. quantark/simm/engines/aggregation/product_class_aggregator.py +183 -0
  292. quantark/simm/engines/aggregation/risk_class_aggregator.py +403 -0
  293. quantark/simm/engines/aggregation/simm_calculator.py +430 -0
  294. quantark/simm/engines/aggregation/weighted_sensitivity.py +272 -0
  295. quantark/simm/engines/base.py +231 -0
  296. quantark/simm/engines/classification/__init__.py +10 -0
  297. quantark/simm/engines/classification/bucket_mapper.py +347 -0
  298. quantark/simm/engines/factory.py +137 -0
  299. quantark/simm/engines/portfolio_adapter.py +336 -0
  300. quantark/simm/engines/result.py +176 -0
  301. quantark/simm/engines/risk_class/__init__.py +18 -0
  302. quantark/simm/engines/risk_class/equity_engine.py +263 -0
  303. quantark/simm/engines/risk_class/ir_engine.py +264 -0
  304. quantark/simm/report/__init__.py +17 -0
  305. quantark/simm/report/crif_export.py +284 -0
  306. quantark/simm/report/excel_generator.py +401 -0
  307. quantark/simm/report/html_generator.py +840 -0
  308. quantark/simm/results/__init__.py +38 -0
  309. quantark/simm/results/attribution.py +313 -0
  310. quantark/simm/results/simm_result.py +339 -0
  311. quantark/simm/results/whatif.py +268 -0
  312. quantark/simm/sensitivity.py +533 -0
  313. quantark/simm/taxonomy.py +416 -0
  314. quantark/stresstest/__init__.py +67 -0
  315. quantark/stresstest/base.py +116 -0
  316. quantark/stresstest/config.py +5 -0
  317. quantark/stresstest/engine.py +5 -0
  318. quantark/stresstest/equity/__init__.py +17 -0
  319. quantark/stresstest/equity/config.py +69 -0
  320. quantark/stresstest/equity/engine.py +272 -0
  321. quantark/stresstest/equity/report/__init__.py +7 -0
  322. quantark/stresstest/equity/report/report_generator.py +423 -0
  323. quantark/stresstest/equity/report/visualizer.py +328 -0
  324. quantark/stresstest/equity/results.py +145 -0
  325. quantark/stresstest/fi/__init__.py +15 -0
  326. quantark/stresstest/fi/config.py +59 -0
  327. quantark/stresstest/fi/engine.py +213 -0
  328. quantark/stresstest/fi/metrics.py +60 -0
  329. quantark/stresstest/fi/results.py +64 -0
  330. quantark/stresstest/report/__init__.py +12 -0
  331. quantark/stresstest/report/report_generator.py +5 -0
  332. quantark/stresstest/report/visualizer.py +5 -0
  333. quantark/stresstest/results/__init__.py +16 -0
  334. quantark/stresstest/results/result_aggregator.py +325 -0
  335. quantark/stresstest/results/result_exporter.py +286 -0
  336. quantark/stresstest/results/stress_results.py +5 -0
  337. quantark/stresstest/scenario/__init__.py +13 -0
  338. quantark/stresstest/scenario/scenario.py +242 -0
  339. quantark/stresstest/scenario/scenario_builder.py +376 -0
  340. quantark/stresstest/scenario/scenario_library.py +435 -0
  341. quantark/stresstest/scenario/scenario_storage.py +224 -0
  342. quantark/stresstest/stress/__init__.py +13 -0
  343. quantark/stresstest/stress/stress_applicator.py +590 -0
  344. quantark/stresstest/stress/stress_types.py +142 -0
  345. quantark/util/__init__.py +23 -0
  346. quantark/util/barrier_shift.py +44 -0
  347. quantark/util/calendar/__init__.py +27 -0
  348. quantark/util/calendar/business_calendar.py +584 -0
  349. quantark/util/calendar/day_counter.py +517 -0
  350. quantark/util/calendar/holidayfile/china.csv +1920 -0
  351. quantark/util/calendar/holidayfile/china_sse.csv +1462 -0
  352. quantark/util/enum/__init__.py +81 -0
  353. quantark/util/enum/bond_enums.py +112 -0
  354. quantark/util/enum/deltaone_enums.py +16 -0
  355. quantark/util/enum/engine_enums.py +137 -0
  356. quantark/util/enum/greeks_enums.py +29 -0
  357. quantark/util/enum/option_enums.py +221 -0
  358. quantark/util/exceptions.py +66 -0
  359. quantark/util/marketdata/__init__.py +39 -0
  360. quantark/util/marketdata/adapter/base_adapter.py +203 -0
  361. quantark/util/marketdata/adapter/mock_adapter.py +265 -0
  362. quantark/util/marketdata/converter.py +289 -0
  363. quantark/util/marketdata/example_usage.py +314 -0
  364. quantark/util/marketdata/generator/__init__.py +7 -0
  365. quantark/util/marketdata/generator/mock_generator.py +466 -0
  366. quantark/util/marketdata/models.py +358 -0
  367. quantark/util/marketdata/storage/__init__.py +7 -0
  368. quantark/util/marketdata/storage/parquet_storage.py +340 -0
  369. quantark/util/numerical/__init__.py +98 -0
  370. quantark/util/numerical/comparison.py +219 -0
  371. quantark/util/numerical/constants.py +98 -0
  372. quantark/util/numerical/formatting.py +380 -0
  373. quantark/util/numerical/pnl.py +17 -0
  374. quantark/util/numerical/safe_math.py +238 -0
  375. quantark/util/numerical/validation.py +315 -0
  376. quantark/var/__init__.py +39 -0
  377. quantark/var/attribution.py +398 -0
  378. quantark/var/backtest/__init__.py +7 -0
  379. quantark/var/backtest/var_backtester.py +309 -0
  380. quantark/var/base.py +63 -0
  381. quantark/var/config.py +219 -0
  382. quantark/var/engines/__init__.py +13 -0
  383. quantark/var/engines/historical.py +925 -0
  384. quantark/var/engines/monte_carlo.py +870 -0
  385. quantark/var/engines/parametric.py +1199 -0
  386. quantark/var/results/__init__.py +16 -0
  387. quantark/var/results/incremental_var_result.py +131 -0
  388. quantark/var/results/var_report.py +346 -0
  389. quantark/var/results/var_result.py +134 -0
  390. quantark/var/risk_factors/__init__.py +22 -0
  391. quantark/var/risk_factors/base.py +41 -0
  392. quantark/var/risk_factors/equity_factors.py +158 -0
  393. quantark/var/risk_factors/fi_factors.py +99 -0
  394. quantark-0.1.0.dist-info/METADATA +351 -0
  395. quantark-0.1.0.dist-info/RECORD +399 -0
  396. quantark-0.1.0.dist-info/WHEEL +4 -0
  397. quantark-0.1.0.dist-info/licenses/LICENSE +202 -0
  398. quantark-0.1.0.dist-info/licenses/NOTICE +2 -0
  399. quantark_compat.pth +1 -0
@@ -0,0 +1,913 @@
1
+ """
2
+ Discount-based pricing engine for Floating Rate Notes (FRNs).
3
+
4
+ Provides pricing, yield calculations (Discount Margin and Simple Margin),
5
+ and risk metrics for floating rate bonds.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from datetime import datetime
10
+ from typing import Optional, Tuple
11
+ import math
12
+
13
+ from quantark.asset.bond.product.couponbond.frn import FloatingRateBond
14
+ from quantark.asset.bond.schedule.cashflow import FloatingCashFlow
15
+ from quantark.priceenv import PricingEnvironment
16
+ from quantark.param.rrf import RateCurve
17
+ from quantark.util.exceptions import ValidationError, MarketDataError
18
+
19
+
20
+ @dataclass
21
+ class FRNPricingResults:
22
+ """
23
+ Results container for FRN pricing.
24
+
25
+ Attributes:
26
+ dirty_price: Price including accrued interest
27
+ clean_price: Price excluding accrued interest
28
+ accrued_interest: Accrued interest amount
29
+ discount_margin: Spread over forward that equates price to PV
30
+ simple_margin: Simple yield-based margin
31
+ yield_to_maturity: YTM assuming constant index rate
32
+ effective_duration: Interest rate sensitivity
33
+ spread_duration: Credit spread sensitivity
34
+ weighted_average_life: WAL in years
35
+ current_coupon: Current period coupon rate
36
+ assumed_index_rate: Index rate used for YTM calculation
37
+ """
38
+
39
+ dirty_price: float
40
+ clean_price: float
41
+ accrued_interest: float
42
+ discount_margin: Optional[float] = None
43
+ simple_margin: Optional[float] = None
44
+ yield_to_maturity: Optional[float] = None
45
+ effective_duration: Optional[float] = None
46
+ spread_duration: Optional[float] = None
47
+ weighted_average_life: Optional[float] = None
48
+ current_coupon: Optional[float] = None
49
+ assumed_index_rate: Optional[float] = None
50
+
51
+
52
+ class FRNDiscountEngine:
53
+ """
54
+ Pricing engine for Floating Rate Notes.
55
+
56
+ Supports:
57
+ - Dirty/Clean price calculation using forward rates
58
+ - Discount Margin (DM) calculation
59
+ - Simple Margin calculation
60
+ - Effective duration and spread duration
61
+ - Weighted average life
62
+ """
63
+
64
+ def __init__(self, pricing_env: PricingEnvironment):
65
+ """
66
+ Initialize FRN discount engine.
67
+
68
+ Args:
69
+ pricing_env: Pricing environment with rate curve
70
+ """
71
+ if pricing_env is None:
72
+ raise ValidationError("Pricing environment is required")
73
+
74
+ if pricing_env.rate_curve is None:
75
+ raise MarketDataError("Rate curve is required for FRN pricing")
76
+
77
+ self.pricing_env = pricing_env
78
+
79
+ def price(
80
+ self,
81
+ frn: FloatingRateBond,
82
+ valuation_date: Optional[datetime] = None,
83
+ settlement_date: Optional[datetime] = None,
84
+ spread_adjustment: float = 0.0,
85
+ ) -> float:
86
+ """
87
+ Calculate FRN dirty price (present value including accrued interest).
88
+
89
+ Args:
90
+ frn: Floating rate bond to price
91
+ valuation_date: Date to value the bond (default: pricing env date)
92
+ settlement_date: Settlement date for trade (default: valuation_date)
93
+ spread_adjustment: Additional spread for discount margin calculation
94
+
95
+ Returns:
96
+ Dirty price (present value of all future cashflows)
97
+ """
98
+ if valuation_date is None:
99
+ valuation_date = self.pricing_env.valuation_date
100
+
101
+ if settlement_date is None:
102
+ settlement_date = valuation_date
103
+
104
+ # Check if bond has matured
105
+ if frn.is_expired(valuation_date):
106
+ return 0.0
107
+
108
+ # Update forward rates in the FRN
109
+ frn.update_forward_rates(self.pricing_env.rate_curve, valuation_date)
110
+
111
+ # Get future floating cashflows
112
+ floating_cfs = frn.get_floating_cashflows(settlement_date)
113
+
114
+ if not floating_cfs:
115
+ return 0.0
116
+
117
+ # Discount each cashflow
118
+ pv = 0.0
119
+ for cf in floating_cfs:
120
+ # Calculate time to payment
121
+ time_to_payment = (cf.payment_date - valuation_date).days / 365.0
122
+
123
+ if time_to_payment < 0:
124
+ continue
125
+
126
+ # Get discount rate (base rate + spread adjustment)
127
+ base_discount_rate = self.pricing_env.get_rate(time_to_payment)
128
+ discount_rate = base_discount_rate + spread_adjustment
129
+
130
+ # Discount factor
131
+ discount_factor = math.exp(-discount_rate * time_to_payment)
132
+
133
+ # Add discounted cashflow
134
+ pv += cf.amount * discount_factor
135
+
136
+ # Add discounted principal (at maturity)
137
+ time_to_maturity = (frn.maturity_date - valuation_date).days / 365.0
138
+ if time_to_maturity > 0:
139
+ base_rate = self.pricing_env.get_rate(time_to_maturity)
140
+ discount_rate = base_rate + spread_adjustment
141
+ df_maturity = math.exp(-discount_rate * time_to_maturity)
142
+ pv += frn.denominator * df_maturity
143
+
144
+ return pv
145
+
146
+ def dirty_price(
147
+ self,
148
+ frn: FloatingRateBond,
149
+ valuation_date: Optional[datetime] = None,
150
+ settlement_date: Optional[datetime] = None,
151
+ ) -> float:
152
+ """
153
+ Calculate FRN dirty price (alias for price method).
154
+
155
+ Args:
156
+ frn: Floating rate bond to price
157
+ valuation_date: Date to value the bond
158
+ settlement_date: Settlement date for trade
159
+
160
+ Returns:
161
+ Dirty price
162
+ """
163
+ return self.price(frn, valuation_date, settlement_date)
164
+
165
+ def clean_price(
166
+ self,
167
+ frn: FloatingRateBond,
168
+ valuation_date: Optional[datetime] = None,
169
+ settlement_date: Optional[datetime] = None,
170
+ ) -> float:
171
+ """
172
+ Calculate FRN clean price (dirty price - accrued interest).
173
+
174
+ Args:
175
+ frn: Floating rate bond to price
176
+ valuation_date: Date to value the bond
177
+ settlement_date: Settlement date for trade
178
+
179
+ Returns:
180
+ Clean price
181
+ """
182
+ if valuation_date is None:
183
+ valuation_date = self.pricing_env.valuation_date
184
+
185
+ if settlement_date is None:
186
+ settlement_date = valuation_date
187
+
188
+ dirty = self.dirty_price(frn, valuation_date, settlement_date)
189
+ accrued = frn.calculate_accrued_interest(settlement_date)
190
+
191
+ return dirty - accrued
192
+
193
+ def accrued_interest(
194
+ self, frn: FloatingRateBond, settlement_date: Optional[datetime] = None
195
+ ) -> float:
196
+ """
197
+ Calculate accrued interest.
198
+
199
+ Args:
200
+ frn: Floating rate bond
201
+ settlement_date: Settlement date
202
+
203
+ Returns:
204
+ Accrued interest amount
205
+ """
206
+ if settlement_date is None:
207
+ settlement_date = self.pricing_env.valuation_date
208
+
209
+ return frn.calculate_accrued_interest(settlement_date)
210
+
211
+ def discount_margin(
212
+ self,
213
+ frn: FloatingRateBond,
214
+ market_price: float,
215
+ valuation_date: Optional[datetime] = None,
216
+ settlement_date: Optional[datetime] = None,
217
+ clean_price: bool = True,
218
+ max_iterations: int = 100,
219
+ tolerance: float = 1e-6,
220
+ ) -> float:
221
+ """
222
+ Calculate Discount Margin (DM) using Newton-Raphson iteration.
223
+
224
+ The discount margin is the spread over the forward rate curve that
225
+ makes the present value of future cashflows equal to the market price.
226
+
227
+ Args:
228
+ frn: Floating rate bond
229
+ market_price: Market price (clean or dirty based on clean_price flag)
230
+ valuation_date: Valuation date
231
+ settlement_date: Settlement date
232
+ clean_price: Whether price is clean price (default: True)
233
+ max_iterations: Maximum iterations for solver
234
+ tolerance: Convergence tolerance
235
+
236
+ Returns:
237
+ Discount margin (annualized spread)
238
+
239
+ Raises:
240
+ ValidationError: If convergence fails
241
+ """
242
+ if valuation_date is None:
243
+ valuation_date = self.pricing_env.valuation_date
244
+
245
+ if settlement_date is None:
246
+ settlement_date = valuation_date
247
+
248
+ if market_price <= 0:
249
+ raise ValidationError(f"Price must be positive, got {market_price}")
250
+
251
+ # Convert clean price to dirty price if needed
252
+ if clean_price:
253
+ accrued = frn.calculate_accrued_interest(settlement_date)
254
+ target_price = market_price + accrued
255
+ else:
256
+ target_price = market_price
257
+
258
+ # Initial guess: use bond spread
259
+ dm = frn.spread
260
+
261
+ # Newton-Raphson iteration
262
+ for iteration in range(max_iterations):
263
+ # Calculate price at current spread
264
+ current_price = self.price(
265
+ frn, valuation_date, settlement_date, spread_adjustment=dm
266
+ )
267
+
268
+ # Price difference
269
+ price_diff = current_price - target_price
270
+
271
+ if abs(price_diff) < tolerance:
272
+ return dm
273
+
274
+ # Calculate numerical derivative (sensitivity to spread)
275
+ bump = 0.0001 # 1bp bump
276
+ bumped_price = self.price(
277
+ frn, valuation_date, settlement_date, spread_adjustment=dm + bump
278
+ )
279
+
280
+ dP_dDM = (bumped_price - current_price) / bump
281
+
282
+ if abs(dP_dDM) < 1e-10:
283
+ raise ValidationError("Derivative too small for DM calculation")
284
+
285
+ # Newton-Raphson update
286
+ dm = dm - price_diff / dP_dDM
287
+
288
+ # Sanity check on DM
289
+ if dm < -0.10 or dm > 0.50:
290
+ dm = max(-0.10, min(0.50, dm))
291
+
292
+ raise ValidationError(
293
+ f"Discount margin did not converge after {max_iterations} iterations"
294
+ )
295
+
296
+ def price_from_yield(
297
+ self,
298
+ frn: FloatingRateBond,
299
+ ytm: float,
300
+ valuation_date: Optional[datetime] = None,
301
+ settlement_date: Optional[datetime] = None,
302
+ clean_price: bool = True,
303
+ assumed_index_rate: Optional[float] = None,
304
+ ) -> float:
305
+ """
306
+ Calculate FRN price given yield to maturity.
307
+
308
+ This is the inverse of yield_to_maturity - given a yield, calculate
309
+ the corresponding FRN price by projecting cashflows with an assumed
310
+ constant index rate and discounting with the given yield.
311
+
312
+ Args:
313
+ frn: Floating rate bond
314
+ ytm: Yield to maturity (annualized, continuously compounded)
315
+ valuation_date: Valuation date
316
+ settlement_date: Settlement date
317
+ clean_price: Whether to return clean price (default: True)
318
+ assumed_index_rate: Assumed constant index rate for projection
319
+ (if None, uses current fixing or forward rate)
320
+
321
+ Returns:
322
+ FRN price (clean or dirty based on clean_price flag)
323
+ """
324
+ if valuation_date is None:
325
+ valuation_date = self.pricing_env.valuation_date
326
+
327
+ if settlement_date is None:
328
+ settlement_date = valuation_date
329
+
330
+ # Check if bond has matured
331
+ if frn.is_expired(valuation_date):
332
+ return 0.0
333
+
334
+ # Update forward rates in the FRN
335
+ frn.update_forward_rates(self.pricing_env.rate_curve, valuation_date)
336
+
337
+ # Determine the assumed index rate for projection
338
+ if assumed_index_rate is None:
339
+ current_rate = frn.get_current_coupon_rate(valuation_date)
340
+ if current_rate is not None:
341
+ assumed_index_rate = current_rate - frn.spread
342
+ else:
343
+ assumed_index_rate = self.pricing_env.rate_curve.get_rate(0.25)
344
+
345
+ # Get future floating cashflows
346
+ floating_cfs = frn.get_floating_cashflows(settlement_date)
347
+
348
+ if not floating_cfs:
349
+ return 0.0
350
+
351
+ # Build projected cashflows and discount with given yield
352
+ dirty_price = 0.0
353
+ last_cf_time = 0.0
354
+ last_coupon = 0.0
355
+
356
+ for cf in floating_cfs:
357
+ time_to_payment = (cf.payment_date - valuation_date).days / 365.0
358
+ if time_to_payment <= 0:
359
+ continue
360
+
361
+ # Calculate coupon amount using assumed rate
362
+ if cf.is_projected:
363
+ total_rate = assumed_index_rate + frn.spread
364
+ if cf.rate_cap is not None:
365
+ total_rate = min(total_rate, cf.rate_cap)
366
+ if cf.rate_floor is not None:
367
+ total_rate = max(total_rate, cf.rate_floor)
368
+ coupon_amount = cf.notional * total_rate * cf.day_count_fraction
369
+ else:
370
+ coupon_amount = cf.amount
371
+
372
+ # Discount with given yield
373
+ df = math.exp(-ytm * time_to_payment)
374
+ dirty_price += coupon_amount * df
375
+
376
+ last_cf_time = time_to_payment
377
+ last_coupon = coupon_amount
378
+
379
+ # Add principal at maturity
380
+ time_to_maturity = (frn.maturity_date - valuation_date).days / 365.0
381
+ if time_to_maturity > 0:
382
+ df = math.exp(-ytm * time_to_maturity)
383
+ dirty_price += frn.denominator * df
384
+
385
+ if clean_price:
386
+ accrued = frn.calculate_accrued_interest(settlement_date)
387
+ return dirty_price - accrued
388
+
389
+ return dirty_price
390
+
391
+ def dirty_price_from_yield(
392
+ self,
393
+ frn: FloatingRateBond,
394
+ ytm: float,
395
+ valuation_date: Optional[datetime] = None,
396
+ settlement_date: Optional[datetime] = None,
397
+ assumed_index_rate: Optional[float] = None,
398
+ ) -> float:
399
+ """
400
+ Calculate dirty price given yield to maturity.
401
+
402
+ Args:
403
+ frn: Floating rate bond
404
+ ytm: Yield to maturity (annualized, continuously compounded)
405
+ valuation_date: Valuation date
406
+ settlement_date: Settlement date
407
+ assumed_index_rate: Assumed constant index rate for projection
408
+
409
+ Returns:
410
+ Dirty price
411
+ """
412
+ return self.price_from_yield(
413
+ frn,
414
+ ytm,
415
+ valuation_date,
416
+ settlement_date,
417
+ clean_price=False,
418
+ assumed_index_rate=assumed_index_rate,
419
+ )
420
+
421
+ def clean_price_from_yield(
422
+ self,
423
+ frn: FloatingRateBond,
424
+ ytm: float,
425
+ valuation_date: Optional[datetime] = None,
426
+ settlement_date: Optional[datetime] = None,
427
+ assumed_index_rate: Optional[float] = None,
428
+ ) -> float:
429
+ """
430
+ Calculate clean price given yield to maturity.
431
+
432
+ Args:
433
+ frn: Floating rate bond
434
+ ytm: Yield to maturity (annualized, continuously compounded)
435
+ valuation_date: Valuation date
436
+ settlement_date: Settlement date
437
+ assumed_index_rate: Assumed constant index rate for projection
438
+
439
+ Returns:
440
+ Clean price
441
+ """
442
+ return self.price_from_yield(
443
+ frn,
444
+ ytm,
445
+ valuation_date,
446
+ settlement_date,
447
+ clean_price=True,
448
+ assumed_index_rate=assumed_index_rate,
449
+ )
450
+
451
+ def yield_to_maturity(
452
+ self,
453
+ frn: FloatingRateBond,
454
+ market_price: float,
455
+ valuation_date: Optional[datetime] = None,
456
+ settlement_date: Optional[datetime] = None,
457
+ clean_price: bool = True,
458
+ assumed_index_rate: Optional[float] = None,
459
+ max_iterations: int = 100,
460
+ tolerance: float = 1e-8,
461
+ ) -> float:
462
+ """
463
+ Calculate Yield to Maturity for an FRN.
464
+
465
+ For FRNs, YTM is calculated by assuming the floating rate remains
466
+ constant at either:
467
+ 1. The provided assumed_index_rate
468
+ 2. The current/latest fixing rate
469
+ 3. The current forward rate from the curve
470
+
471
+ Then finds the single discount rate that equates PV to market price.
472
+
473
+ Args:
474
+ frn: Floating rate bond
475
+ market_price: Market price (clean or dirty based on clean_price flag)
476
+ valuation_date: Valuation date
477
+ settlement_date: Settlement date
478
+ clean_price: Whether price is clean price (default: True)
479
+ assumed_index_rate: Assumed constant index rate for projection
480
+ (if None, uses current fixing or forward rate)
481
+ max_iterations: Maximum iterations for solver
482
+ tolerance: Convergence tolerance
483
+
484
+ Returns:
485
+ Yield to maturity (annualized, continuously compounded)
486
+
487
+ Raises:
488
+ ValidationError: If convergence fails
489
+ """
490
+ if valuation_date is None:
491
+ valuation_date = self.pricing_env.valuation_date
492
+
493
+ if settlement_date is None:
494
+ settlement_date = valuation_date
495
+
496
+ if market_price <= 0:
497
+ raise ValidationError(f"Price must be positive, got {market_price}")
498
+
499
+ # Convert clean price to dirty price if needed
500
+ if clean_price:
501
+ accrued = frn.calculate_accrued_interest(settlement_date)
502
+ target_price = market_price + accrued
503
+ else:
504
+ target_price = market_price
505
+
506
+ # Update forward rates in the FRN
507
+ frn.update_forward_rates(self.pricing_env.rate_curve, valuation_date)
508
+
509
+ # Determine the assumed index rate for projection
510
+ if assumed_index_rate is None:
511
+ # Try to get from latest fixing or current period
512
+ current_rate = frn.get_current_coupon_rate(valuation_date)
513
+ if current_rate is not None:
514
+ # Current coupon includes spread, so extract index rate
515
+ assumed_index_rate = current_rate - frn.spread
516
+ else:
517
+ # Use forward rate at short tenor
518
+ assumed_index_rate = self.pricing_env.rate_curve.get_rate(0.25)
519
+
520
+ # Get future floating cashflows and build projected cashflows
521
+ floating_cfs = frn.get_floating_cashflows(settlement_date)
522
+
523
+ if not floating_cfs:
524
+ raise ValidationError("No future cashflows to calculate yield")
525
+
526
+ # Build list of (time, amount) for all cashflows
527
+ cashflow_data = []
528
+ for cf in floating_cfs:
529
+ time_to_payment = (cf.payment_date - valuation_date).days / 365.0
530
+ if time_to_payment <= 0:
531
+ continue
532
+
533
+ # Calculate coupon amount using assumed rate
534
+ if cf.is_projected:
535
+ # Use assumed index rate + spread
536
+ total_rate = assumed_index_rate + frn.spread
537
+ # Apply cap/floor if present
538
+ if cf.rate_cap is not None:
539
+ total_rate = min(total_rate, cf.rate_cap)
540
+ if cf.rate_floor is not None:
541
+ total_rate = max(total_rate, cf.rate_floor)
542
+ coupon_amount = cf.notional * total_rate * cf.day_count_fraction
543
+ else:
544
+ # Use actual fixing
545
+ coupon_amount = cf.amount
546
+
547
+ cashflow_data.append((time_to_payment, coupon_amount))
548
+
549
+ # Add principal at maturity
550
+ time_to_maturity = (frn.maturity_date - valuation_date).days / 365.0
551
+ if time_to_maturity > 0:
552
+ # Find the last cashflow time and add principal there
553
+ if cashflow_data:
554
+ last_time = cashflow_data[-1][0]
555
+ last_coupon = cashflow_data[-1][1]
556
+ cashflow_data[-1] = (last_time, last_coupon + frn.denominator)
557
+ else:
558
+ cashflow_data.append((time_to_maturity, frn.denominator))
559
+
560
+ if not cashflow_data:
561
+ raise ValidationError("No valid cashflows for YTM calculation")
562
+
563
+ # Initial guess: use current rate plus spread
564
+ ytm = assumed_index_rate + frn.spread
565
+
566
+ # Newton-Raphson iteration
567
+ for iteration in range(max_iterations):
568
+ # Calculate price and duration at current yield
569
+ pv = 0.0
570
+ duration = 0.0
571
+
572
+ for t, amount in cashflow_data:
573
+ df = math.exp(-ytm * t)
574
+ pv += amount * df
575
+ duration += amount * t * df
576
+
577
+ # Check convergence
578
+ price_diff = pv - target_price
579
+
580
+ if abs(price_diff) < tolerance:
581
+ return ytm
582
+
583
+ # Newton-Raphson update
584
+ # f(y) = PV(y) - target_price
585
+ # f'(y) = -duration
586
+ if abs(duration) < 1e-10:
587
+ raise ValidationError("Duration too small for yield calculation")
588
+
589
+ ytm = ytm - price_diff / (-duration)
590
+
591
+ # Sanity check on yield
592
+ if ytm < -0.20 or ytm > 1.0:
593
+ ytm = max(-0.20, min(1.0, ytm))
594
+
595
+ raise ValidationError(
596
+ f"Yield to maturity did not converge after {max_iterations} iterations"
597
+ )
598
+
599
+ def simple_margin(
600
+ self,
601
+ frn: FloatingRateBond,
602
+ market_price: float,
603
+ valuation_date: Optional[datetime] = None,
604
+ clean_price: bool = True,
605
+ ) -> float:
606
+ """
607
+ Calculate Simple Margin.
608
+
609
+ Simple Margin is an approximation:
610
+ SM = (100 - Price) / WAL + Quoted Spread
611
+
612
+ Where WAL is weighted average life.
613
+
614
+ Args:
615
+ frn: Floating rate bond
616
+ market_price: Market price (as % of par, e.g., 99.5)
617
+ valuation_date: Valuation date
618
+ clean_price: Whether price is clean price
619
+
620
+ Returns:
621
+ Simple margin (annualized)
622
+ """
623
+ if valuation_date is None:
624
+ valuation_date = self.pricing_env.valuation_date
625
+
626
+ # Calculate weighted average life
627
+ wal = self.weighted_average_life(frn, valuation_date)
628
+
629
+ if wal <= 0:
630
+ return frn.spread
631
+
632
+ # Convert price to percentage of par
633
+ if clean_price:
634
+ price_pct = (market_price / frn.denominator) * 100
635
+ else:
636
+ accrued = frn.calculate_accrued_interest(valuation_date)
637
+ clean = market_price - accrued
638
+ price_pct = (clean / frn.denominator) * 100
639
+
640
+ # Simple margin formula
641
+ # (100 - Price) / WAL gives annualized capital gain/loss
642
+ capital_component = (100.0 - price_pct) / (100.0 * wal)
643
+
644
+ return capital_component + frn.spread
645
+
646
+ def weighted_average_life(
647
+ self, frn: FloatingRateBond, valuation_date: Optional[datetime] = None
648
+ ) -> float:
649
+ """
650
+ Calculate Weighted Average Life (WAL).
651
+
652
+ WAL = sum(t_i * P_i) / sum(P_i)
653
+
654
+ Where t_i is time to principal payment and P_i is principal amount.
655
+ For bullet FRNs, this equals time to maturity.
656
+
657
+ Args:
658
+ frn: Floating rate bond
659
+ valuation_date: Valuation date
660
+
661
+ Returns:
662
+ Weighted average life in years
663
+ """
664
+ if valuation_date is None:
665
+ valuation_date = self.pricing_env.valuation_date
666
+
667
+ # For bullet FRNs (single principal payment at maturity)
668
+ time_to_maturity = (frn.maturity_date - valuation_date).days / 365.0
669
+
670
+ return max(0.0, time_to_maturity)
671
+
672
+ def effective_duration(
673
+ self,
674
+ frn: FloatingRateBond,
675
+ valuation_date: Optional[datetime] = None,
676
+ settlement_date: Optional[datetime] = None,
677
+ rate_bump: float = 0.0001,
678
+ ) -> float:
679
+ """
680
+ Calculate effective duration (sensitivity to parallel rate shift).
681
+
682
+ For FRNs, effective duration is typically very low since cashflows
683
+ reset to market rates. The duration is mainly from the time to
684
+ next reset.
685
+
686
+ Args:
687
+ frn: Floating rate bond
688
+ valuation_date: Valuation date
689
+ settlement_date: Settlement date
690
+ rate_bump: Size of rate bump (default: 1bp)
691
+
692
+ Returns:
693
+ Effective duration
694
+ """
695
+ if valuation_date is None:
696
+ valuation_date = self.pricing_env.valuation_date
697
+
698
+ if settlement_date is None:
699
+ settlement_date = valuation_date
700
+
701
+ base_price = self.price(frn, valuation_date, settlement_date)
702
+
703
+ if base_price <= 0:
704
+ return 0.0
705
+
706
+ # Price with rates bumped up
707
+ from quantark.param.rrf import FlatRateCurve
708
+
709
+ original_curve = self.pricing_env.rate_curve
710
+
711
+ # Create bumped curve (simple flat bump)
712
+ base_rate = original_curve.get_rate(1.0)
713
+
714
+ up_curve = FlatRateCurve(rate=base_rate + rate_bump)
715
+ down_curve = FlatRateCurve(rate=base_rate - rate_bump)
716
+
717
+ # Price with up curve
718
+ self.pricing_env.rate_curve = up_curve
719
+ price_up = self.price(frn, valuation_date, settlement_date)
720
+
721
+ # Price with down curve
722
+ self.pricing_env.rate_curve = down_curve
723
+ price_down = self.price(frn, valuation_date, settlement_date)
724
+
725
+ # Restore original curve
726
+ self.pricing_env.rate_curve = original_curve
727
+
728
+ # Effective duration = -(P+ - P-) / (2 * dY * P0)
729
+ duration = -(price_up - price_down) / (2 * rate_bump * base_price)
730
+
731
+ return duration
732
+
733
+ def spread_duration(
734
+ self,
735
+ frn: FloatingRateBond,
736
+ valuation_date: Optional[datetime] = None,
737
+ settlement_date: Optional[datetime] = None,
738
+ spread_bump: float = 0.0001,
739
+ ) -> float:
740
+ """
741
+ Calculate spread duration (sensitivity to credit spread changes).
742
+
743
+ Spread duration measures the price sensitivity to changes in the
744
+ discount margin / credit spread. For FRNs, this is approximately
745
+ equal to the weighted average life.
746
+
747
+ Args:
748
+ frn: Floating rate bond
749
+ valuation_date: Valuation date
750
+ settlement_date: Settlement date
751
+ spread_bump: Size of spread bump (default: 1bp)
752
+
753
+ Returns:
754
+ Spread duration
755
+ """
756
+ if valuation_date is None:
757
+ valuation_date = self.pricing_env.valuation_date
758
+
759
+ if settlement_date is None:
760
+ settlement_date = valuation_date
761
+
762
+ base_price = self.price(frn, valuation_date, settlement_date)
763
+
764
+ if base_price <= 0:
765
+ return 0.0
766
+
767
+ # Price with spread bumped up and down
768
+ price_up = self.price(
769
+ frn, valuation_date, settlement_date, spread_adjustment=spread_bump
770
+ )
771
+ price_down = self.price(
772
+ frn, valuation_date, settlement_date, spread_adjustment=-spread_bump
773
+ )
774
+
775
+ # Spread duration = -(P+ - P-) / (2 * dS * P0)
776
+ spread_dur = -(price_up - price_down) / (2 * spread_bump * base_price)
777
+
778
+ return spread_dur
779
+
780
+ def dv01(
781
+ self,
782
+ frn: FloatingRateBond,
783
+ valuation_date: Optional[datetime] = None,
784
+ settlement_date: Optional[datetime] = None,
785
+ ) -> float:
786
+ """
787
+ Calculate DV01 (dollar value of one basis point).
788
+
789
+ Args:
790
+ frn: Floating rate bond
791
+ valuation_date: Valuation date
792
+ settlement_date: Settlement date
793
+
794
+ Returns:
795
+ DV01 (price change per basis point)
796
+ """
797
+ eff_dur = self.effective_duration(frn, valuation_date, settlement_date)
798
+ price = self.price(frn, valuation_date, settlement_date)
799
+
800
+ return eff_dur * price * 0.0001
801
+
802
+ def cs01(
803
+ self,
804
+ frn: FloatingRateBond,
805
+ valuation_date: Optional[datetime] = None,
806
+ settlement_date: Optional[datetime] = None,
807
+ ) -> float:
808
+ """
809
+ Calculate CS01 (credit spread 01 - price change per 1bp spread change).
810
+
811
+ Args:
812
+ frn: Floating rate bond
813
+ valuation_date: Valuation date
814
+ settlement_date: Settlement date
815
+
816
+ Returns:
817
+ CS01
818
+ """
819
+ spread_dur = self.spread_duration(frn, valuation_date, settlement_date)
820
+ price = self.price(frn, valuation_date, settlement_date)
821
+
822
+ return spread_dur * price * 0.0001
823
+
824
+ def full_analysis(
825
+ self,
826
+ frn: FloatingRateBond,
827
+ market_price: Optional[float] = None,
828
+ valuation_date: Optional[datetime] = None,
829
+ settlement_date: Optional[datetime] = None,
830
+ clean_price: bool = True,
831
+ assumed_index_rate: Optional[float] = None,
832
+ ) -> FRNPricingResults:
833
+ """
834
+ Perform full analysis of an FRN.
835
+
836
+ Args:
837
+ frn: Floating rate bond
838
+ market_price: Market price (if provided, calculates margins and YTM)
839
+ valuation_date: Valuation date
840
+ settlement_date: Settlement date
841
+ clean_price: Whether market_price is clean
842
+ assumed_index_rate: Index rate to assume for YTM calculation
843
+
844
+ Returns:
845
+ FRNPricingResults with all metrics
846
+ """
847
+ if valuation_date is None:
848
+ valuation_date = self.pricing_env.valuation_date
849
+
850
+ if settlement_date is None:
851
+ settlement_date = valuation_date
852
+
853
+ dirty = self.dirty_price(frn, valuation_date, settlement_date)
854
+ accrued = frn.calculate_accrued_interest(settlement_date)
855
+ clean = dirty - accrued
856
+
857
+ # Calculate margins and YTM if market price provided
858
+ dm = None
859
+ sm = None
860
+ ytm = None
861
+ index_rate_used = assumed_index_rate
862
+
863
+ if market_price is not None:
864
+ try:
865
+ dm = self.discount_margin(
866
+ frn, market_price, valuation_date, settlement_date, clean_price
867
+ )
868
+ except ValidationError:
869
+ pass
870
+
871
+ sm = self.simple_margin(frn, market_price, valuation_date, clean_price)
872
+
873
+ try:
874
+ ytm = self.yield_to_maturity(
875
+ frn,
876
+ market_price,
877
+ valuation_date,
878
+ settlement_date,
879
+ clean_price,
880
+ assumed_index_rate,
881
+ )
882
+ # Determine the index rate that was actually used
883
+ if index_rate_used is None:
884
+ current_coupon = frn.get_current_coupon_rate(valuation_date)
885
+ if current_coupon is not None:
886
+ index_rate_used = current_coupon - frn.spread
887
+ else:
888
+ index_rate_used = self.pricing_env.rate_curve.get_rate(0.25)
889
+ except ValidationError:
890
+ pass
891
+
892
+ # Risk metrics
893
+ eff_dur = self.effective_duration(frn, valuation_date, settlement_date)
894
+ spread_dur = self.spread_duration(frn, valuation_date, settlement_date)
895
+ wal = self.weighted_average_life(frn, valuation_date)
896
+ current_coupon = frn.get_current_coupon_rate(valuation_date)
897
+
898
+ return FRNPricingResults(
899
+ dirty_price=dirty,
900
+ clean_price=clean,
901
+ accrued_interest=accrued,
902
+ discount_margin=dm,
903
+ simple_margin=sm,
904
+ yield_to_maturity=ytm,
905
+ effective_duration=eff_dur,
906
+ spread_duration=spread_dur,
907
+ weighted_average_life=wal,
908
+ current_coupon=current_coupon,
909
+ assumed_index_rate=index_rate_used,
910
+ )
911
+
912
+ def __repr__(self):
913
+ return f"FRNDiscountEngine(valuation_date={self.pricing_env.valuation_date.date()})"