py-beacon-kit 0.2.0__tar.gz → 0.3.0__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.
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/.gitignore +3 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/CHANGELOG.md +52 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/PKG-INFO +1 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/__init__.py +1 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/analysis/attribution.py +22 -6
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/backtest/__init__.py +2 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/backtest/engine.py +1 -2
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/backtest/pricing.py +1 -2
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/backtest/result.py +22 -31
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/backtest/rules.py +7 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/changelog.py +17 -6
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/__init__.py +1 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/adjustment.py +2 -2
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/corporate_actions.py +18 -5
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/fetcher.py +43 -6
- py_beacon_kit-0.3.0/src/beacon/data/prices.py +152 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/store.py +26 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/derivatives/pricing.py +6 -4
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/derivatives/swaps.py +7 -3
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/exceptions.py +10 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/expressions/catalogue.py +0 -7
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/expressions/namespaces.py +11 -7
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/expressions/resolve.py +61 -31
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/expressions/stubs.py +0 -4
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/expressions/validation.py +48 -17
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/__init__.py +2 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/cache.py +10 -2
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/calculation/calculator.py +9 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/calculation/market_values.py +12 -7
- py_beacon_kit-0.3.0/src/beacon/index/calculation/price_gaps.py +88 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/calculation/total_return.py +4 -3
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/constructor.py +24 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/expression_rules.py +7 -3
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/methodology.py +45 -5
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/requirements.py +10 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/result.py +30 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/plot/accessors.py +26 -12
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/portfolio/reporting.py +32 -16
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/report/blocks.py +15 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/__main__.py +7 -2
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/app.py +9 -3
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/config.py +17 -7
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/errors.py +6 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/jobs.py +62 -5
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/methods.py +45 -4
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/optimisation.py +26 -16
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/risk.py +19 -16
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/beacon.py +19 -16
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/data.py +62 -15
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/derivatives.py +22 -3
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/jobs.py +6 -3
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/optimise.py +8 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/reports.py +14 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/risk.py +25 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/synthetic.py +23 -2
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/schemas.py +53 -3
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/store_schemas.py +5 -3
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/views.py +38 -14
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/weights.py +31 -25
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/testing/dataset.py +5 -2
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/universe.py +7 -0
- py_beacon_kit-0.3.0/tests/baseline/test_annual_returns_dark.png +0 -0
- py_beacon_kit-0.3.0/tests/baseline/test_annual_returns_light.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_adjustment.py +18 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_api_contract.py +8 -3
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_attribution.py +20 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_changelog.py +34 -2
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_corporate_actions.py +11 -0
- py_beacon_kit-0.3.0/tests/test_currency_conversion.py +183 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_derivatives_pricing.py +10 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_derivatives_swaps.py +11 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_docs_style.py +32 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_expression_rules.py +30 -5
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_expression_validation.py +55 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_fx_data.py +20 -2
- py_beacon_kit-0.3.0/tests/test_fx_pairs.py +67 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_calculator_integration.py +85 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_definition.py +13 -5
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_schedule.py +10 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_job_persistence.py +73 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_plot.py +69 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_plot_images.py +2 -2
- py_beacon_kit-0.3.0/tests/test_reporting.py +69 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server.py +10 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_cors.py +16 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_data_router.py +13 -2
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_data_stores.py +67 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_derivatives.py +11 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_features.py +4 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_jobs.py +10 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_reports.py +13 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_synthetic.py +33 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_views.py +28 -1
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_spec_conformance.py +47 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_total_return.py +20 -0
- py_beacon_kit-0.2.0/tests/baseline/test_annual_returns_dark.png +0 -0
- py_beacon_kit-0.2.0/tests/baseline/test_annual_returns_light.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/LICENSE.txt +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/README.md +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/01_index_and_backtest.ipynb +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/02_backtest_analysis.ipynb +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/03_index_futures.ipynb +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/04_optimised_index.ipynb +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/05_optimised_backtest.ipynb +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/README.md +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/data/templates/equities_corp_actions_data.xlsx +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/data/templates/equities_fundamental_data.xlsx +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/data/templates/equities_market_data.xlsx +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/data/templates/equities_reference_data.xlsx +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/examples/data/templates/fx_rates_data.xlsx +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/pyproject.toml +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/_optional.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/analysis/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/analysis/concentration.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/analysis/etf/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/analysis/etf/analytics.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/analysis/liquidity.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/analysis/relative.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/analysis/risk.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/asset/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/asset/base.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/asset/bond.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/asset/commodity.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/asset/equity.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/asset/view.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/backtest/asset_view.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/backtest/main.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/catalogue.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/base.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/features.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/free_float.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/fx.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/identifiers.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/importing.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/ingest.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/layout.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/loader.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/postgres.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/data/session.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/derivatives/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/derivatives/base.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/derivatives/curves.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/derivatives/forwards.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/derivatives/futures.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/derivatives/term_structure.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/environment/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/environment/config.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/expressions/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/expressions/core.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/fund/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/fund/base.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/fund/etf.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/asset_view.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/calculation/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/calculation/corporate_actions.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/calculation/deletions.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/calculation/selection.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/capping.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/chaining.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/context.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/derived.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/feature_rules.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/index/schedule.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/optimise/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/optimise/config.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/optimise/constraints.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/optimise/frontier.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/optimise/result.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/optimise/solver.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/plot/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/plot/base.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/plot/comparison.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/plot/style.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/portfolio/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/portfolio/asset_view.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/portfolio/base.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/portfolio/history.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/py.typed +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/report/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/report/pdf.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/risk/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/risk/contribution.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/risk/covariance.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/risk/factors.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/risk/model.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/active_data.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/backtests.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/benchmarks.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/constraints.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/data_stores.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/definitions.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/derivatives.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/documents.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/preview.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/reference.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/reports.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/coverage.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/importing.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/indices.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/refresh.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/stores.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/universes.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/routers/watchlists.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/runs.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/security.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/serialisation.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/store.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/server/types.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/sources.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/__main__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/dataset.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/extend.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/features.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/fx.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/listings.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/prices.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/profiles.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/regimes.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/regions.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/returns.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/state.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/synthetic/universe.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/testing/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/testing/weights.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/tokens/__init__.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/src/beacon/tokens/colors.json +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_compare_dark.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_compare_light.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_contributions_dark.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_contributions_light.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_correlation_dark.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_correlation_light.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_exposures_dark.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_exposures_light.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_frontier_dark.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_frontier_light.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_level_dark.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_level_light.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_performance_dark.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_performance_light.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_weights_dark.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/baseline/test_weights_light.png +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/conftest.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/docstyle.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_asset.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_asset_view.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_backtest_engine.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_backtest_engine_integration.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_backtest_fills.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_backtest_main.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_backtest_modifier.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_backtest_pricing.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_backtest_result.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_benchmark.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_canonical_dataset_integration.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_capping.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_catalogue.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_chaining.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_concentration.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_constraint_catalogue.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_corporate_action_kind.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_coverage_fields.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_daily_weights.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_data_fetcher.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_data_import.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_data_store.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_data_tables.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_derivatives_base.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_derivatives_curves.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_derivatives_etf_future.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_derivatives_futures.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_derived_index.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_docs_examples.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_effective_lag.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_etf_tracking.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_examples.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_expression_namespaces.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_expression_schema.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_expressions.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_factors.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_feature_rules.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_features.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_free_float_backfill.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_frontier.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_fund.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_fx_routes.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_identifier_search.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_asset_view.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_batch_reads.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_cache.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_calculator.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_currency.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_eligibility.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_result.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_selection.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_index_weighting.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_ingest.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_integration.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_look_ahead.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_optimise.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_optional_dependencies.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_portfolio.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_portfolio_asset_view.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_postgres_store.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_properties.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_read_volume.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_readme.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_reference_currency.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_reference_dimensions.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_reference_profile.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_report.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_requirements.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_risk_contribution.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_risk_model.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_backtest.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_document_faults.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_import.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_indices.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_listing_discovery.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_optimise.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_optimised_indices.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_preview.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_preview_document.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_preview_optimised.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_reference_batch.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_refresh.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_risk.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_store.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_universes.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_server_weights_rows.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_session_agreement.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_splits.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_stale_prices.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_survivorship.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_synthetic.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_synthetic_extend.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_synthetic_features.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_testing_dataset.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_tokens.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_universe_batch_reads.py +0 -0
- {py_beacon_kit-0.2.0 → py_beacon_kit-0.3.0}/tests/test_universe_filters.py +0 -0
|
@@ -8,6 +8,56 @@ Before 1.0, a breaking change raises the middle number, as in 0.1 to 0.2.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.3.0] - 2026-09-30
|
|
12
|
+
|
|
13
|
+
Money in one currency across multi-currency indices, prices carried over gaps, expressions that refuse fields the data does not have, and engine state that survives. Two breaking changes: `data.actions` is gone from expressions, and some requests the engine cannot answer as put answer 422 instead of 404.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `DataFetcher.fetch_prices` reads prices for several instruments at once, converted into one currency day by day under the dataset's FX policy.
|
|
18
|
+
- `IndexResult.price_gaps` and `beacon.index.PriceGap`: the days a held name had no bar and was valued at its last close. `PriceGap` is still importable from `beacon.backtest`.
|
|
19
|
+
- The asset view reports `currency`, which its returns, beta and tracking error are measured in (the index's), and `price_currency`, which its price series is in (the name's own).
|
|
20
|
+
- A feature import's response says whether the rows were `saved` into the served store, and gives the new `data_version`.
|
|
21
|
+
- `BacktestResult.get_annual_returns()`: calendar-year returns that compound to the whole run's return.
|
|
22
|
+
- `TotalReturnSwap` takes `underlying_type` (`INDEX`, the default, `ETF` or `EQUITY`). It was always `INDEX`.
|
|
23
|
+
- Each release from `GET /changelog` carries its `summary`, the prose between its heading and its first section.
|
|
24
|
+
- A risk model request takes an optional `currency`, and a risk model and an optimisation run report the currency they were measured in.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Requests that cannot be answered as put answer 422 `INVALID_ARGUMENT` instead of 404 `DATA_NOT_FOUND`: an unknown price interval, an adjusted series without `CLOSE`, and an empty or oversized feature batch.
|
|
29
|
+
- An expression with a field the data does not have is refused with `ExpressionError` by `universe.where`, `ExpressionScreen` and an index's `ExpressionRule`, naming the field and suggesting close matches. Before, it selected nothing.
|
|
30
|
+
- `IndexDefinition` refuses an unknown calendar or rebalancing frequency when it is built, naming the valid values and suggesting close calendar codes. Before, the mistake surfaced only when rebalance dates were first computed.
|
|
31
|
+
- The API schema states more of what it accepts: `since` on `/changelog` is a dotted version, and `/beacon/compare` needs at least two `ids`, each a valid identifier. These are now refused as `VALIDATION_ERROR` by the schema check. The boolean fields of a synthetic-data or import request no longer accept `0`, `1` or strings.
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
|
|
35
|
+
- `data.actions` in expressions. No expression could read its fields, so every name was missing a value; it now raises `UnknownDatasetError`, and the field catalogue and `GET /data/fields` no longer list an `actions` namespace. Read corporate actions from the fetcher's `corporate_actions`.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Money is compared in one currency wherever names are compared or added up. `LiquidityRule`'s traded-value floor, the engine's risk models, optimisation runs, the weights pane (drift, risk contributions, active risk), attribution and the asset view all read each name's prices in its own currency, so a multi-currency index mixed yen with dollars and left exchange-rate returns and risk out. They now convert into the index's currency.
|
|
40
|
+
- An index values a held name with no bar on a session at its last close, as the backtest engine does, and lists the day in `IndexResult.price_gaps`. It used to value the name at zero, so the level dipped by the name's weight for the day and recovered when the bar returned.
|
|
41
|
+
- An expression's `market_cap` inside an index is in the index's currency, as `MarketCapRule`'s bounds are. Outside an index it stays in USD.
|
|
42
|
+
- `POST /data/features` now saves the rows into the served store when it is a folder, so they survive a restart, and changes `data_version` and sends a `data.freshness` event for `features`. Before, a client caching on `data_version` missed the change and a restart lost the rows.
|
|
43
|
+
- The engine always keeps the saved results its views read: the latest backtest of each index, every optimisation run and the latest estimate of each risk model. The limit of 50 saved jobs now applies only to the rest, so a run of loads, refreshes or renders no longer makes those views answer 404.
|
|
44
|
+
- `data.market.free_float` and `free_float_market_cap` in an expression carry the free float forward as far as the data's `free_float_backfill_days`, as every other read does, rather than 10 days.
|
|
45
|
+
- Attribution for a capped index no longer fails when its window starts after the index's first rebalance. The cap drag is measured over the same periods as the contributions.
|
|
46
|
+
- A cancelled dividend is no longer reinvested by a total-return index, counted in the trailing dividend and yield, or applied to adjusted closes; a cancelled split is no longer applied to adjusted closes either.
|
|
47
|
+
- The engine checks for a token before it loads any data, so a missing token is reported at once rather than after a large store has loaded.
|
|
48
|
+
- `http://127.0.0.1` and `http://[::1]` on any port are allowed origins, as `http://localhost` already was.
|
|
49
|
+
- A wrong or missing token on the event socket closes it with code 1008 and a reason, as intended, instead of refusing the handshake with an HTTP 403 a browser cannot read.
|
|
50
|
+
- `BacktestPlots.annual_returns` measures each year from the previous year's close, and the first from the initial capital. It measured each year from its own first close, so every year after the first lost its first day's return.
|
|
51
|
+
- A chart drawn before `beacon.plot.use()` gets the light colours on matplotlib's white background, not the dark mode's.
|
|
52
|
+
- `OptimisationPlots.frontier` starts the capital market line at the rate the frontier was traced at, unless another is passed, so the line is tangent.
|
|
53
|
+
- The Excel reports accept a `pathlib.Path` as well as a string, and the holdings report writes `valuation_date` into its sheet instead of only logging it.
|
|
54
|
+
- `futures_roll_return` uses 365-day years, like every other year fraction in the derivatives. It used 365.25, so its rates came out smaller than the rest by a factor of 365/365.25.
|
|
55
|
+
- The sample dataset in `beacon.testing.dataset` stores its GBPUSD rate in `RATE`, so `fx_pairs` and the coverage view list it. A pair stored without `RATE` still converts but now logs a warning that it is not listed.
|
|
56
|
+
- A report template with an unknown page setting answers 422 instead of 500, and so does a futures or swap price whose inputs overflow.
|
|
57
|
+
- A plain `OPTIONS` request answers 405 with an `Allow` header listing every method its path supports. It listed only one route's methods on a path with several, such as `/data/features`. CORS preflights are unchanged.
|
|
58
|
+
- The nightly API fuzz run passes again: it leaves out operations that start heavy work, and skips the check that a schema-valid request is accepted only where a rule spans fields or depends on the data.
|
|
59
|
+
- Generating or extending synthetic data from an isolated engine (`python -I`, as the Beacon app runs it) keeps the child process isolated too, so it cannot load packages from the user's own site-packages.
|
|
60
|
+
|
|
11
61
|
## [0.2.0] - 2026-09-26
|
|
12
62
|
|
|
13
63
|
Choose where the engine's data comes from: named stores, synthetic data you can extend to today, CSV and Excel import, and read-only Postgres. Also documentation at pybeacon.dev, and fixes to index levels and backtests around splits and delistings.
|
|
@@ -78,7 +128,8 @@ The first release.
|
|
|
78
128
|
- An index or backtest never uses a price, rate or free float dated after the day it is working on.
|
|
79
129
|
- Requires Python 3.11 or later.
|
|
80
130
|
|
|
81
|
-
[Unreleased]: https://github.com/karanbh01/py-beacon/compare/v0.
|
|
131
|
+
[Unreleased]: https://github.com/karanbh01/py-beacon/compare/v0.3.0...HEAD
|
|
132
|
+
[0.3.0]: https://github.com/karanbh01/py-beacon/compare/v0.2.0...v0.3.0
|
|
82
133
|
[0.2.0]: https://github.com/karanbh01/py-beacon/compare/v0.1.1...v0.2.0
|
|
83
134
|
[0.1.1]: https://github.com/karanbh01/py-beacon/compare/v0.1.0...v0.1.1
|
|
84
135
|
[0.1.0]: https://github.com/karanbh01/py-beacon/releases/tag/v0.1.0
|
|
@@ -350,7 +350,8 @@ def cost_drag(total_costs: float,
|
|
|
350
350
|
|
|
351
351
|
def cap_drag(capped_weights: dict[pd.Timestamp, dict[str, float]],
|
|
352
352
|
uncapped_weights: dict[pd.Timestamp, dict[str, float]],
|
|
353
|
-
prices: pd.DataFrame
|
|
353
|
+
prices: pd.DataFrame,
|
|
354
|
+
window: pd.Index | None = None) -> float:
|
|
354
355
|
"""What capping cost, or gained, over the window.
|
|
355
356
|
|
|
356
357
|
The capped index's return minus the return of the same methodology left
|
|
@@ -365,27 +366,42 @@ def cap_drag(capped_weights: dict[pd.Timestamp, dict[str, float]],
|
|
|
365
366
|
Args:
|
|
366
367
|
capped_weights: Rebalance date -> capped weights.
|
|
367
368
|
uncapped_weights: Rebalance date -> weights before capping.
|
|
368
|
-
prices: Constituent prices
|
|
369
|
+
prices: Constituent prices, from the first rebalance on. Weights
|
|
370
|
+
drift from the rebalance before each date, so the prices have to
|
|
371
|
+
reach back to it even when *window* starts later.
|
|
372
|
+
window: The dates to measure over, as for :func:`attribute`: the
|
|
373
|
+
return of each date after the first. None measures over every
|
|
374
|
+
date in *prices*.
|
|
369
375
|
|
|
370
376
|
Returns:
|
|
371
377
|
float: Capped total return minus uncapped total return.
|
|
372
378
|
"""
|
|
373
379
|
asset_returns = prices.pct_change()
|
|
374
380
|
|
|
375
|
-
capped_return = _path_return(capped_weights, prices, asset_returns)
|
|
376
|
-
uncapped_return = _path_return(uncapped_weights, prices, asset_returns
|
|
381
|
+
capped_return = _path_return(capped_weights, prices, asset_returns, window)
|
|
382
|
+
uncapped_return = _path_return(uncapped_weights, prices, asset_returns,
|
|
383
|
+
window)
|
|
377
384
|
|
|
378
385
|
return capped_return - uncapped_return
|
|
379
386
|
|
|
380
387
|
|
|
381
388
|
def _path_return(snapshots: dict[pd.Timestamp, dict[str, float]],
|
|
382
389
|
prices: pd.DataFrame,
|
|
383
|
-
asset_returns: pd.DataFrame
|
|
390
|
+
asset_returns: pd.DataFrame,
|
|
391
|
+
window: pd.Index | None = None) -> float:
|
|
384
392
|
"""Compounded return of an index following *snapshots*."""
|
|
393
|
+
# BN-258: drifted over all of *prices*, then cut to the window. Cutting
|
|
394
|
+
# the prices first left a rebalance before the window with no price to
|
|
395
|
+
# drift from, and the lookup raised KeyError.
|
|
385
396
|
weights = drifted_weights(snapshots, prices)
|
|
397
|
+
|
|
398
|
+
if window is not None:
|
|
399
|
+
weights = weights.loc[weights.index.intersection(window)]
|
|
400
|
+
|
|
386
401
|
assets = sorted(set(weights.columns) & set(asset_returns.columns))
|
|
387
402
|
|
|
388
|
-
periods = (weights[assets].shift(1)
|
|
403
|
+
periods = (weights[assets].shift(1)
|
|
404
|
+
* asset_returns[assets].reindex(weights.index)).sum(axis=1)
|
|
389
405
|
periods = periods.loc[weights[assets].shift(1).dropna(how="all").index]
|
|
390
406
|
|
|
391
407
|
return float((1.0 + periods).prod() - 1.0)
|
|
@@ -13,11 +13,12 @@ and tracking against the target index. `BacktestModifier` subclasses such as
|
|
|
13
13
|
`DriftThresholdModifier` can skip a rebalance or adjust its trades, and
|
|
14
14
|
`BacktestAssetView` gives one asset's story through a backtest.
|
|
15
15
|
"""
|
|
16
|
+
from ..index.result import PriceGap
|
|
16
17
|
from ..portfolio.base import TradeInstruction
|
|
17
18
|
from .asset_view import BacktestAssetView
|
|
18
19
|
from .engine import BacktestEngine
|
|
19
20
|
from .main import Backtest
|
|
20
|
-
from .result import BacktestResult,
|
|
21
|
+
from .result import BacktestResult, RebalancePricing, UnfilledOrder
|
|
21
22
|
from .rules import BacktestModifier, DriftThresholdModifier
|
|
22
23
|
|
|
23
24
|
__all__ = [
|
|
@@ -9,7 +9,7 @@ import pandas as pd
|
|
|
9
9
|
from ..data.corporate_actions import ratios_between
|
|
10
10
|
from ..data.fetcher import DataFetcher
|
|
11
11
|
from ..index.requirements import require_price_column
|
|
12
|
-
from ..index.result import IndexResult
|
|
12
|
+
from ..index.result import IndexResult, PriceGap
|
|
13
13
|
from ..index.schedule import SESSION_UNIT, sessions
|
|
14
14
|
from ..portfolio.base import CASH_TOLERANCE as PORTFOLIO_CASH_TOLERANCE
|
|
15
15
|
|
|
@@ -22,7 +22,6 @@ from .result import (
|
|
|
22
22
|
BacktestResult,
|
|
23
23
|
Book,
|
|
24
24
|
IndexBooks,
|
|
25
|
-
PriceGap,
|
|
26
25
|
RebalancePricing,
|
|
27
26
|
UnfilledOrder,
|
|
28
27
|
)
|
|
@@ -31,9 +31,8 @@ import pandas as pd
|
|
|
31
31
|
|
|
32
32
|
from ..data.fetcher import DataFetcher
|
|
33
33
|
from ..exceptions import CalculationError
|
|
34
|
-
from ..index.result import IndexResult
|
|
34
|
+
from ..index.result import IndexResult, PriceGap
|
|
35
35
|
from ..index.schedule import is_session
|
|
36
|
-
from .result import PriceGap
|
|
37
36
|
|
|
38
37
|
logger = logging.getLogger(__name__)
|
|
39
38
|
|
|
@@ -47,7 +47,7 @@ import pandas as pd
|
|
|
47
47
|
|
|
48
48
|
from ..analysis.relative import RelativeMetrics, relative_metrics
|
|
49
49
|
from ..data.fetcher import DataFetcher
|
|
50
|
-
from ..index.result import IndexResult
|
|
50
|
+
from ..index.result import IndexResult, PriceGap
|
|
51
51
|
from ..plot.base import PlotAccessor
|
|
52
52
|
from ..portfolio.base import Portfolio
|
|
53
53
|
from .asset_view import BacktestAssetView
|
|
@@ -78,36 +78,8 @@ class UnfilledOrder:
|
|
|
78
78
|
shortfall_value: float
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
class PriceGap:
|
|
84
|
-
"""A day a name should have traded on and had no bar.
|
|
85
|
-
|
|
86
|
-
The engine prices from the last session on or before the date it is
|
|
87
|
-
marking. Two different things can put it there, and only one of them is a
|
|
88
|
-
fault: a day the index's **calendar says was closed** is a market that was
|
|
89
|
-
shut, and the previous session's price is what the position was genuinely
|
|
90
|
-
worth through it, so no gap is recorded, because nothing is missing. A day
|
|
91
|
-
the calendar says was **open** is data that is missing something, and the
|
|
92
|
-
price carried forward is a stale quote.
|
|
93
|
-
|
|
94
|
-
Carrying it forward is standard practice and beats refusing: a backtest
|
|
95
|
-
over five hundred names must not die because one of them had one bad day.
|
|
96
|
-
Doing it silently is not: the mark is not what that day's market said, so
|
|
97
|
-
it is published here rather than absorbed, the way `unfilled` publishes the
|
|
98
|
-
legs a rebalance could not fill.
|
|
99
|
-
|
|
100
|
-
Attributes:
|
|
101
|
-
date: The simulated day whose bar was missing.
|
|
102
|
-
asset_id: The name with no bar.
|
|
103
|
-
priced_from: The session the carried price actually came from, always
|
|
104
|
-
earlier than *date*.
|
|
105
|
-
"""
|
|
106
|
-
date: pd.Timestamp
|
|
107
|
-
asset_id: str
|
|
108
|
-
priced_from: pd.Timestamp
|
|
109
|
-
|
|
110
|
-
|
|
81
|
+
# RebalancePricing is BN-183. PriceGap, also BN-183, moved to the index
|
|
82
|
+
# layer with BN-251, when the index started recording gaps too.
|
|
111
83
|
@dataclass(frozen=True)
|
|
112
84
|
class RebalancePricing:
|
|
113
85
|
"""What one rebalance priced from.
|
|
@@ -369,6 +341,25 @@ class BacktestResult:
|
|
|
369
341
|
|
|
370
342
|
return returns.dropna()
|
|
371
343
|
|
|
344
|
+
def get_annual_returns(self) -> pd.Series:
|
|
345
|
+
"""The portfolio's calendar-year returns.
|
|
346
|
+
|
|
347
|
+
Each year runs from the previous year's last close, and the first from
|
|
348
|
+
the initial capital, so the years compound to the whole run's return.
|
|
349
|
+
A partial first or last year covers only its days.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
pd.Series: Return by calendar year, indexed by the year as an int.
|
|
353
|
+
"""
|
|
354
|
+
returns = self.get_returns()
|
|
355
|
+
|
|
356
|
+
if returns.empty:
|
|
357
|
+
return pd.Series(dtype=float)
|
|
358
|
+
|
|
359
|
+
yearly = (1.0 + returns).groupby(returns.index.year).prod() - 1.0
|
|
360
|
+
|
|
361
|
+
return yearly.astype(float)
|
|
362
|
+
|
|
372
363
|
def get_tracking_error(self) -> float | None:
|
|
373
364
|
"""Calculate annualised tracking error against the tracked index.
|
|
374
365
|
|
|
@@ -14,6 +14,7 @@ import pandas as pd
|
|
|
14
14
|
from ..data.fetcher import DataFetcher
|
|
15
15
|
from ..expressions.core import Expression
|
|
16
16
|
from ..expressions.resolve import resolve
|
|
17
|
+
from ..expressions.validation import require_valid
|
|
17
18
|
from ..portfolio.base import Portfolio, TradeInstruction
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
@@ -147,12 +148,18 @@ class ExpressionScreen(BacktestModifier):
|
|
|
147
148
|
fetcher: Data to resolve against.
|
|
148
149
|
on_missing: Whether a name with no value passes. Excluded by default,
|
|
149
150
|
matching the index rules.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
ExpressionError: If the expression names a field the data does not
|
|
154
|
+
have.
|
|
150
155
|
"""
|
|
151
156
|
|
|
152
157
|
def __init__(self,
|
|
153
158
|
expression: "Expression",
|
|
154
159
|
fetcher: "DataFetcher",
|
|
155
160
|
on_missing: bool = False):
|
|
161
|
+
require_valid(expression, fetcher, "ExpressionScreen")
|
|
162
|
+
|
|
156
163
|
self.expression = expression
|
|
157
164
|
self.fetcher = fetcher
|
|
158
165
|
self.on_missing = on_missing
|
|
@@ -17,7 +17,7 @@ the emphasis and code spans in them survive to whatever renders them.
|
|
|
17
17
|
# BN-222. `CHANGELOG.md` at the repository root is the one file anybody
|
|
18
18
|
# edits; the wheel's copy comes from `force-include` in pyproject.toml.
|
|
19
19
|
import re
|
|
20
|
-
from dataclasses import dataclass, field
|
|
20
|
+
from dataclasses import dataclass, field, replace
|
|
21
21
|
from importlib import resources
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
@@ -43,21 +43,25 @@ class ChangelogSection:
|
|
|
43
43
|
class ChangelogEntry:
|
|
44
44
|
"""One release: its version, its date, and what changed in it.
|
|
45
45
|
|
|
46
|
-
`date` is None for the Unreleased entry, which has none.
|
|
46
|
+
`date` is None for the Unreleased entry, which has none. `summary` is the
|
|
47
|
+
prose between the release heading and its first section, its lines
|
|
48
|
+
joined with spaces, or None when there is none.
|
|
47
49
|
"""
|
|
48
50
|
version: str
|
|
49
51
|
date: str | None
|
|
50
52
|
sections: list[ChangelogSection] = field(default_factory=list)
|
|
53
|
+
summary: str | None = None
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
def parse(text: str) -> list[ChangelogEntry]:
|
|
54
57
|
"""Read a Keep a Changelog document into entries, newest first.
|
|
55
58
|
|
|
56
|
-
Prose between a release heading and its first section
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
Prose between a release heading and its first section is the entry's
|
|
60
|
+
summary, not an item. A list item that wraps onto indented lines is
|
|
61
|
+
joined back into one string.
|
|
59
62
|
"""
|
|
60
63
|
entries: list[ChangelogEntry] = []
|
|
64
|
+
prose: list[list[str]] = []
|
|
61
65
|
section: ChangelogSection | None = None
|
|
62
66
|
|
|
63
67
|
for line in text.splitlines():
|
|
@@ -66,6 +70,7 @@ def parse(text: str) -> list[ChangelogEntry]:
|
|
|
66
70
|
if release is not None:
|
|
67
71
|
entries.append(ChangelogEntry(version=release["version"],
|
|
68
72
|
date=release["date"]))
|
|
73
|
+
prose.append([])
|
|
69
74
|
section = None
|
|
70
75
|
continue
|
|
71
76
|
|
|
@@ -79,9 +84,15 @@ def parse(text: str) -> list[ChangelogEntry]:
|
|
|
79
84
|
entries[-1].sections.append(section)
|
|
80
85
|
continue
|
|
81
86
|
|
|
87
|
+
# BN-274: the summary used to be skipped.
|
|
88
|
+
if section is None and line.strip():
|
|
89
|
+
prose[-1].append(line.strip())
|
|
90
|
+
continue
|
|
91
|
+
|
|
82
92
|
section = _add_line(section, line)
|
|
83
93
|
|
|
84
|
-
return
|
|
94
|
+
return [replace(entry, summary=" ".join(lines) or None)
|
|
95
|
+
for entry, lines in zip(entries, prose, strict=True)]
|
|
85
96
|
|
|
86
97
|
|
|
87
98
|
def _add_line(section: ChangelogSection | None,
|
|
@@ -25,7 +25,7 @@ __all__ = [
|
|
|
25
25
|
# anybody who guessed the other import at the right one. Without it the error
|
|
26
26
|
# is "module 'beacon.data' has no attribute 'market'", which is true and tells
|
|
27
27
|
# nobody what to do about it.
|
|
28
|
-
_EXPRESSION_ROOTS = ("market", "reference"
|
|
28
|
+
_EXPRESSION_ROOTS = ("market", "reference")
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def __getattr__(name: str) -> Any:
|
|
@@ -47,7 +47,7 @@ import logging
|
|
|
47
47
|
import numpy as np
|
|
48
48
|
import pandas as pd
|
|
49
49
|
|
|
50
|
-
from .corporate_actions import CASH_ACTIONS, RATIO_ACTIONS
|
|
50
|
+
from .corporate_actions import CASH_ACTIONS, RATIO_ACTIONS, without_cancelled
|
|
51
51
|
|
|
52
52
|
logger = logging.getLogger(__name__)
|
|
53
53
|
|
|
@@ -90,7 +90,7 @@ def _factors(closes: pd.Series,
|
|
|
90
90
|
if actions is None or actions.empty:
|
|
91
91
|
return factors
|
|
92
92
|
|
|
93
|
-
for _, action in actions.iterrows():
|
|
93
|
+
for _, action in without_cancelled(actions).iterrows():
|
|
94
94
|
ex_date = pd.Timestamp(action["EX_DATE"])
|
|
95
95
|
action_type = str(action["TYPE"])
|
|
96
96
|
|
|
@@ -123,6 +123,22 @@ def kind_of(action_type: str) -> Kind:
|
|
|
123
123
|
return STRUCTURAL
|
|
124
124
|
|
|
125
125
|
|
|
126
|
+
def without_cancelled(actions: pd.DataFrame) -> pd.DataFrame:
|
|
127
|
+
"""*actions* less any whose status is cancelled.
|
|
128
|
+
|
|
129
|
+
A cancelled action never happened, so nothing that applies actions (a
|
|
130
|
+
split to units held, a dividend to a total return, either to an adjusted
|
|
131
|
+
close) or sums them (a trailing dividend) may count it. Rows with no
|
|
132
|
+
status, or a frame with no status column, are kept.
|
|
133
|
+
"""
|
|
134
|
+
# BN-261: the ratio schedule already did this; the dividend readers and
|
|
135
|
+
# the adjusted closes did not.
|
|
136
|
+
if actions.empty or STATUS_COLUMN not in actions.columns:
|
|
137
|
+
return actions
|
|
138
|
+
|
|
139
|
+
return actions[actions[STATUS_COLUMN].map(status_of) != CANCELLED]
|
|
140
|
+
|
|
141
|
+
|
|
126
142
|
def status_of(value: object) -> Status | None:
|
|
127
143
|
"""Normalise a stored status, or None if it is not one this vocabulary has.
|
|
128
144
|
|
|
@@ -372,7 +388,7 @@ class CorporateActions:
|
|
|
372
388
|
end = pd.Timestamp(as_of)
|
|
373
389
|
start = end - TRAILING_YEAR
|
|
374
390
|
|
|
375
|
-
subset = self.get(identifier, types=types)
|
|
391
|
+
subset = without_cancelled(self.get(identifier, types=types))
|
|
376
392
|
if subset.empty:
|
|
377
393
|
return subset
|
|
378
394
|
|
|
@@ -434,10 +450,7 @@ class CorporateActions:
|
|
|
434
450
|
multiplier would erase or invert a position.
|
|
435
451
|
"""
|
|
436
452
|
frame = self._df
|
|
437
|
-
ratios = frame[frame["TYPE"].isin(RATIO_ACTIONS)]
|
|
438
|
-
|
|
439
|
-
if STATUS_COLUMN in ratios.columns:
|
|
440
|
-
ratios = ratios[ratios[STATUS_COLUMN].map(status_of) != CANCELLED]
|
|
453
|
+
ratios = without_cancelled(frame[frame["TYPE"].isin(RATIO_ACTIONS)])
|
|
441
454
|
|
|
442
455
|
schedule: dict[pd.Timestamp, dict[str, float]] = {}
|
|
443
456
|
|
|
@@ -21,6 +21,7 @@ from .free_float import (
|
|
|
21
21
|
carried_forward,
|
|
22
22
|
validated_window,
|
|
23
23
|
)
|
|
24
|
+
from .prices import prices_in
|
|
24
25
|
from .session import SessionPanel
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
@@ -269,6 +270,8 @@ class DataFetcher:
|
|
|
269
270
|
self._fx_series: dict[tuple[str, str], pd.Series] = {}
|
|
270
271
|
# How each cached pair was found: direct, inverse, or a cross (BN-235).
|
|
271
272
|
self._fx_routes: dict[tuple[str, str], str | None] = {}
|
|
273
|
+
# Pairs already warned about being stored without RATE (BN-226).
|
|
274
|
+
self._unlisted_pairs: set[str] = set()
|
|
272
275
|
# Each name's observed free floats, blanks dropped, read once when a
|
|
273
276
|
# blank day first needs carrying over (BN-219). Cleared with the FX
|
|
274
277
|
# series on a merge, for the same reason.
|
|
@@ -968,6 +971,26 @@ class DataFetcher:
|
|
|
968
971
|
val = df[column].iloc[0]
|
|
969
972
|
return float(val) if pd.notna(val) else None
|
|
970
973
|
|
|
974
|
+
def fetch_prices(self,
|
|
975
|
+
identifiers: list[str],
|
|
976
|
+
start_date: str | None = None,
|
|
977
|
+
end_date: str | None = None,
|
|
978
|
+
currency: str | None = None,
|
|
979
|
+
column: str = "CLOSE") -> pd.DataFrame:
|
|
980
|
+
"""Prices by date and instrument, converted into *currency*.
|
|
981
|
+
|
|
982
|
+
The read to use whenever prices of several instruments are compared
|
|
983
|
+
or added up: each is converted from its own currency day by day under
|
|
984
|
+
this fetcher's FX policy. With ``currency=None`` nothing is converted.
|
|
985
|
+
See `beacon.data.prices`.
|
|
986
|
+
|
|
987
|
+
Raises:
|
|
988
|
+
CalculationError: If an instrument's currency has no rate into
|
|
989
|
+
*currency*.
|
|
990
|
+
"""
|
|
991
|
+
return prices_in(self, identifiers, start_date, end_date, currency,
|
|
992
|
+
column)
|
|
993
|
+
|
|
971
994
|
def fetch_fx_rates(self,
|
|
972
995
|
from_currency: str,
|
|
973
996
|
to_currency: str,
|
|
@@ -977,10 +1000,12 @@ class DataFetcher:
|
|
|
977
1000
|
"""Return the stored FX rate series converting *from_currency* into *to_currency*.
|
|
978
1001
|
|
|
979
1002
|
The pair is looked up as a market-data identifier named
|
|
980
|
-
``f"{from_currency}{to_currency}"`` (upper-cased)
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1003
|
+
``f"{from_currency}{to_currency}"`` (upper-cased), and its *column*
|
|
1004
|
+
read. A pair should carry `RATE`, which is also what :attr:`fx_pairs`
|
|
1005
|
+
lists pairs by. One stored without it is read from its first data
|
|
1006
|
+
column, with a warning, and is not listed. Returns an empty Series if
|
|
1007
|
+
that exact pair is not stored: this does not invert or cross rates.
|
|
1008
|
+
:meth:`fx_rate_on` and :meth:`fx_rates_on` do.
|
|
984
1009
|
"""
|
|
985
1010
|
pair = f"{from_currency}{to_currency}".upper()
|
|
986
1011
|
if pair not in self._market.identifiers:
|
|
@@ -988,8 +1013,20 @@ class DataFetcher:
|
|
|
988
1013
|
df = self._market.get(pair, start_date, end_date)
|
|
989
1014
|
if df.empty:
|
|
990
1015
|
return pd.Series(dtype=float)
|
|
991
|
-
|
|
992
|
-
|
|
1016
|
+
|
|
1017
|
+
if column in df.columns and df[column].notna().any():
|
|
1018
|
+
return df[column]
|
|
1019
|
+
|
|
1020
|
+
# BN-226: kept for stores written without RATE, but said, because
|
|
1021
|
+
# such a pair converts and `fx_pairs` does not list it.
|
|
1022
|
+
fallback = next(name for name in df.columns if name != column)
|
|
1023
|
+
if pair not in self._unlisted_pairs:
|
|
1024
|
+
self._unlisted_pairs.add(pair)
|
|
1025
|
+
logger.warning("%s has no %s values, so it is read from %s and "
|
|
1026
|
+
"is not listed among the currency pairs. Store its "
|
|
1027
|
+
"rate in %s.", pair, column, fallback, column)
|
|
1028
|
+
|
|
1029
|
+
return df[fallback]
|
|
993
1030
|
|
|
994
1031
|
def fx_route(self,
|
|
995
1032
|
from_currency: str,
|