cadlag-ce 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cadlag-ce might be problematic. Click here for more details.
- cadlag_ce-1.0.4/LICENSE +17 -0
- cadlag_ce-1.0.4/PKG-INFO +60 -0
- cadlag_ce-1.0.4/README.md +17 -0
- cadlag_ce-1.0.4/cadlag_ce/__init__.py +10 -0
- cadlag_ce-1.0.4/cadlag_ce/api/__init__.py +1 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/__init__.py +43 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/audit.py +63 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/backtest.py +288 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/brokers.py +105 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/config.py +40 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/data.py +285 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/datasets.py +167 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/deployments.py +168 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/experiments.py +117 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/feature_sets.py +162 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/health.py +18 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/live.py +197 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/models.py +192 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/pipeline.py +300 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/risk.py +103 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/strategies.py +177 -0
- cadlag_ce-1.0.4/cadlag_ce/api/routes/validation.py +163 -0
- cadlag_ce-1.0.4/cadlag_ce/api/ws/__init__.py +95 -0
- cadlag_ce-1.0.4/cadlag_ce/api/ws/audit.py +55 -0
- cadlag_ce-1.0.4/cadlag_ce/api/ws/backtest.py +91 -0
- cadlag_ce-1.0.4/cadlag_ce/api/ws/data.py +128 -0
- cadlag_ce-1.0.4/cadlag_ce/api/ws/deployment.py +84 -0
- cadlag_ce-1.0.4/cadlag_ce/api/ws/live.py +81 -0
- cadlag_ce-1.0.4/cadlag_ce/api/ws/pipeline.py +48 -0
- cadlag_ce-1.0.4/cadlag_ce/api/ws/sweep.py +85 -0
- cadlag_ce-1.0.4/cadlag_ce/cli/__init__.py +2 -0
- cadlag_ce-1.0.4/cadlag_ce/cli/maintenance.py +658 -0
- cadlag_ce-1.0.4/cadlag_ce/core/__init__.py +16 -0
- cadlag_ce-1.0.4/cadlag_ce/core/accounting.py +15 -0
- cadlag_ce-1.0.4/cadlag_ce/core/broker_types.py +297 -0
- cadlag_ce-1.0.4/cadlag_ce/core/capabilities.py +141 -0
- cadlag_ce-1.0.4/cadlag_ce/core/capability_keys.py +49 -0
- cadlag_ce-1.0.4/cadlag_ce/core/config.py +268 -0
- cadlag_ce-1.0.4/cadlag_ce/core/domain.py +1124 -0
- cadlag_ce-1.0.4/cadlag_ce/core/exceptions.py +171 -0
- cadlag_ce-1.0.4/cadlag_ce/core/extension_manifest.py +41 -0
- cadlag_ce-1.0.4/cadlag_ce/core/extensions.py +332 -0
- cadlag_ce-1.0.4/cadlag_ce/core/license_guards.py +58 -0
- cadlag_ce-1.0.4/cadlag_ce/core/logging.py +170 -0
- cadlag_ce-1.0.4/cadlag_ce/core/order_fsm.py +159 -0
- cadlag_ce-1.0.4/cadlag_ce/core/parameter_timeline.py +158 -0
- cadlag_ce-1.0.4/cadlag_ce/core/protocols.py +751 -0
- cadlag_ce-1.0.4/cadlag_ce/core/version_binding.py +308 -0
- cadlag_ce-1.0.4/cadlag_ce/data/__init__.py +5 -0
- cadlag_ce-1.0.4/cadlag_ce/data/adapters/__init__.py +1 -0
- cadlag_ce-1.0.4/cadlag_ce/data/adapters/basic_polling_adapter.py +146 -0
- cadlag_ce-1.0.4/cadlag_ce/data/adapters/broker_bar_source.py +137 -0
- cadlag_ce-1.0.4/cadlag_ce/data/broker_circuit_breaker.py +95 -0
- cadlag_ce-1.0.4/cadlag_ce/data/broker_provider_registry.py +47 -0
- cadlag_ce-1.0.4/cadlag_ce/data/data_service.py +409 -0
- cadlag_ce-1.0.4/cadlag_ce/data/dataset_snapshot.py +69 -0
- cadlag_ce-1.0.4/cadlag_ce/data/feature_store.py +246 -0
- cadlag_ce-1.0.4/cadlag_ce/data/gateway_registry.py +124 -0
- cadlag_ce-1.0.4/cadlag_ce/data/ingest_adapter.py +256 -0
- cadlag_ce-1.0.4/cadlag_ce/data/lakehouse_catalog.py +55 -0
- cadlag_ce-1.0.4/cadlag_ce/data/metrics.py +14 -0
- cadlag_ce-1.0.4/cadlag_ce/data/mt5/__init__.py +23 -0
- cadlag_ce-1.0.4/cadlag_ce/data/mt5/bridge_client.py +219 -0
- cadlag_ce-1.0.4/cadlag_ce/data/mt5/connection.py +207 -0
- cadlag_ce-1.0.4/cadlag_ce/data/mt5/data_gateway.py +148 -0
- cadlag_ce-1.0.4/cadlag_ce/data/mt5/order_gateway.py +96 -0
- cadlag_ce-1.0.4/cadlag_ce/data/mt5/timezone_verify.py +77 -0
- cadlag_ce-1.0.4/cadlag_ce/data/mt5/translator.py +417 -0
- cadlag_ce-1.0.4/cadlag_ce/data/parquet_writer.py +212 -0
- cadlag_ce-1.0.4/cadlag_ce/data/phantom/__init__.py +21 -0
- cadlag_ce-1.0.4/cadlag_ce/data/phantom/clock.py +136 -0
- cadlag_ce-1.0.4/cadlag_ce/data/phantom/config.py +214 -0
- cadlag_ce-1.0.4/cadlag_ce/data/phantom/data_gateway.py +346 -0
- cadlag_ce-1.0.4/cadlag_ce/data/phantom/order_gateway.py +389 -0
- cadlag_ce-1.0.4/cadlag_ce/data/phantom/sessions.py +301 -0
- cadlag_ce-1.0.4/cadlag_ce/data/phantom/synthesis.py +140 -0
- cadlag_ce-1.0.4/cadlag_ce/data/query_engine.py +291 -0
- cadlag_ce-1.0.4/cadlag_ce/data/questdb_init.py +67 -0
- cadlag_ce-1.0.4/cadlag_ce/data/questdb_writer.py +159 -0
- cadlag_ce-1.0.4/cadlag_ce/data/types.py +69 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/__init__.py +71 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/adapters/__init__.py +15 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/adapters/backtrader_adapter.py +419 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/adapters/base.py +89 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/adapters/broker_order_adapter.py +174 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/adapters/paper_order_adapter.py +159 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/backtest_result_repository.py +912 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/deployment_binding.py +538 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/deployment_entitlement.py +88 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/fill_simulator.py +94 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/live_chain.py +595 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/live_deployment_repository.py +544 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/live_order_persister.py +409 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/live_runtime_manager.py +1434 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/model_runtime.py +472 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/optimizer.py +293 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/order_identity.py +81 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/order_router.py +275 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/portfolio_book.py +784 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/prediction_cache.py +467 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/risk_engine.py +389 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/router.py +538 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/rule_engine.py +195 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/simplified_portfolio.py +447 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/simulation_engine.py +825 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/strategy_adapter.py +474 -0
- cadlag_ce-1.0.4/cadlag_ce/execution/trading_statistics.py +201 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/__init__.py +6 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/input_shaper.py +154 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/orchestrator.py +425 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stage_executor.py +131 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/__init__.py +1 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/base.py +17 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/compare_baseline.py +80 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/compute_trading_metrics.py +48 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/dataset_freeze.py +150 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/dataset_split.py +153 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/feature_materialize.py +239 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/generate_reports.py +245 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/load_dataset.py +82 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/predictive_validation.py +288 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/run_backtest.py +235 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/schema_validation.py +163 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/trainer_execution.py +284 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/trainer_adapter.py +154 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/training_pipeline.py +223 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/types.py +254 -0
- cadlag_ce-1.0.4/cadlag_ce/pipeline/validation_pipeline.py +196 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/__init__.py +163 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/audit.py +156 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/config_service.py +152 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/dataset_registry.py +734 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/experiment_tracker.py +401 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/feature_registry.py +380 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/model_registry.py +521 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/model_watcher.py +221 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/registry.py +108 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/risk_policy.py +333 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/strategy_registry.py +500 -0
- cadlag_ce-1.0.4/cadlag_ce/registry/validation_registry.py +514 -0
- cadlag_ce-1.0.4/cadlag_ce/server.py +536 -0
- cadlag_ce-1.0.4/cadlag_ce.egg-info/PKG-INFO +60 -0
- cadlag_ce-1.0.4/cadlag_ce.egg-info/SOURCES.txt +147 -0
- cadlag_ce-1.0.4/cadlag_ce.egg-info/dependency_links.txt +1 -0
- cadlag_ce-1.0.4/cadlag_ce.egg-info/entry_points.txt +2 -0
- cadlag_ce-1.0.4/cadlag_ce.egg-info/requires.txt +31 -0
- cadlag_ce-1.0.4/cadlag_ce.egg-info/top_level.txt +1 -0
- cadlag_ce-1.0.4/pyproject.toml +91 -0
- cadlag_ce-1.0.4/setup.cfg +4 -0
cadlag_ce-1.0.4/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cadlag
|
|
4
|
+
|
|
5
|
+
Parameters
|
|
6
|
+
|
|
7
|
+
Licensor: Cadlag
|
|
8
|
+
Licensed Work: Cadlag CE v1.0
|
|
9
|
+
Additional Use Grant: You may use the Licensed Work for any purpose,
|
|
10
|
+
including production use, provided that you do not
|
|
11
|
+
offer the Licensed Work as a hosted service to third
|
|
12
|
+
parties.
|
|
13
|
+
Change Date: Four years from first public release
|
|
14
|
+
Change License: Apache License, Version 2.0
|
|
15
|
+
|
|
16
|
+
The full text of the Business Source License 1.1 is available at:
|
|
17
|
+
https://mariadb.com/bsl11/
|
cadlag_ce-1.0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cadlag-ce
|
|
3
|
+
Version: 1.0.4
|
|
4
|
+
Summary: Cadlag CE — Source-available algorithmic trading platform
|
|
5
|
+
Maintainer-email: Cadlag <info@cadlag.pro>
|
|
6
|
+
License: BSL-1.1
|
|
7
|
+
Project-URL: Homepage, https://cadlag.pro
|
|
8
|
+
Project-URL: Repository, https://github.com/cadlag-quant/cadlag
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: cadlag-sdk<2.0,>=1.0.4
|
|
13
|
+
Requires-Dist: fastapi>=0.109
|
|
14
|
+
Requires-Dist: uvicorn[standard]>=0.27
|
|
15
|
+
Requires-Dist: asyncpg>=0.29
|
|
16
|
+
Requires-Dist: alembic>=1.13
|
|
17
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
18
|
+
Requires-Dist: psycopg2-binary>=2.9
|
|
19
|
+
Requires-Dist: questdb>=1.1
|
|
20
|
+
Requires-Dist: pydantic>=2.5
|
|
21
|
+
Requires-Dist: structlog>=24.1
|
|
22
|
+
Requires-Dist: uuid-utils<1,>=0.14
|
|
23
|
+
Requires-Dist: polars>=0.20
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Requires-Dist: pyarrow>=14.0
|
|
26
|
+
Requires-Dist: duckdb>=0.9
|
|
27
|
+
Requires-Dist: watchdog>=3.0
|
|
28
|
+
Requires-Dist: pydantic-settings>=2.1
|
|
29
|
+
Requires-Dist: pyyaml>=6.0
|
|
30
|
+
Requires-Dist: packaging>=21.0
|
|
31
|
+
Requires-Dist: httpx>=0.26
|
|
32
|
+
Requires-Dist: pytz>=2023.3
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: httpx2>=2.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-xdist>=3.5; extra == "dev"
|
|
39
|
+
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
40
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
41
|
+
Requires-Dist: types-PyYAML>=6.0; extra == "dev"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# Cadlag CE
|
|
45
|
+
|
|
46
|
+
Source-available algorithmic trading platform for traceable, reproducible
|
|
47
|
+
quantitative research. Requires Python 3.11 or later.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python -m pip install cadlag-ce
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Import the server as `cadlag_ce.server:app`. A running deployment also needs
|
|
54
|
+
PostgreSQL, QuestDB, runtime configuration and database migrations. The public
|
|
55
|
+
repository includes `cadlag-docker/` for an evaluation deployment and `docs/` for
|
|
56
|
+
configuration and API documentation. Installing `cadlag-quant` includes the SDK and
|
|
57
|
+
Starter examples as well.
|
|
58
|
+
|
|
59
|
+
CE is licensed under BSL-1.1. The additional use grant excludes offering the
|
|
60
|
+
software as a hosted service to third parties. See `LICENSE` for the terms.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Cadlag CE
|
|
2
|
+
|
|
3
|
+
Source-available algorithmic trading platform for traceable, reproducible
|
|
4
|
+
quantitative research. Requires Python 3.11 or later.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
python -m pip install cadlag-ce
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Import the server as `cadlag_ce.server:app`. A running deployment also needs
|
|
11
|
+
PostgreSQL, QuestDB, runtime configuration and database migrations. The public
|
|
12
|
+
repository includes `cadlag-docker/` for an evaluation deployment and `docs/` for
|
|
13
|
+
configuration and API documentation. Installing `cadlag-quant` includes the SDK and
|
|
14
|
+
Starter examples as well.
|
|
15
|
+
|
|
16
|
+
CE is licensed under BSL-1.1. The additional use grant excludes offering the
|
|
17
|
+
software as a hosted service to third parties. See `LICENSE` for the terms.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cadlag. Licensed under BSL 1.1.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cadlag. Licensed under BSL 1.1.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import NoReturn, TypeVar
|
|
6
|
+
|
|
7
|
+
from cadlag_ce.core.exceptions import ConflictError, ResourceNotFoundError
|
|
8
|
+
|
|
9
|
+
_T = TypeVar("_T")
|
|
10
|
+
|
|
11
|
+
ROUTE_MODULES = (
|
|
12
|
+
"audit",
|
|
13
|
+
"backtest",
|
|
14
|
+
"brokers",
|
|
15
|
+
"capabilities",
|
|
16
|
+
"config",
|
|
17
|
+
"data",
|
|
18
|
+
"datasets",
|
|
19
|
+
"deployments",
|
|
20
|
+
"experiments",
|
|
21
|
+
"feature_sets",
|
|
22
|
+
"health",
|
|
23
|
+
"live",
|
|
24
|
+
"models",
|
|
25
|
+
"pipeline",
|
|
26
|
+
"risk",
|
|
27
|
+
"strategies",
|
|
28
|
+
"validation",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def ensure_module(module: _T | None, name: str) -> _T:
|
|
33
|
+
if module is None:
|
|
34
|
+
raise ResourceNotFoundError(f"{name} is not initialized")
|
|
35
|
+
return module
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def raise_registry_error(exc: Exception, *, not_found: bool | None = None) -> NoReturn:
|
|
39
|
+
message = str(exc)
|
|
40
|
+
message_lower = message.lower()
|
|
41
|
+
if not_found is True or (not_found is None and "not found" in message_lower):
|
|
42
|
+
raise ResourceNotFoundError(message) from exc
|
|
43
|
+
raise ConflictError(message) from exc
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cadlag. Licensed under BSL 1.1.
|
|
2
|
+
"""Audit log API routes."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Request
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
|
+
|
|
12
|
+
from cadlag_ce.api.routes import raise_registry_error
|
|
13
|
+
|
|
14
|
+
router = APIRouter(tags=["Audit"])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _RouteModel(BaseModel):
|
|
18
|
+
model_config = ConfigDict(from_attributes=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuditEventResponse(_RouteModel):
|
|
22
|
+
id: Any | None = None
|
|
23
|
+
entity_type: str
|
|
24
|
+
entity_id: str
|
|
25
|
+
action: str
|
|
26
|
+
actor: str
|
|
27
|
+
old_value: dict[str, Any] | None = None
|
|
28
|
+
new_value: dict[str, Any] | None = None
|
|
29
|
+
metadata: dict[str, Any] | None = None
|
|
30
|
+
created_at: Any | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _audit_log(request: Request) -> Any:
|
|
34
|
+
return request.app.state.registry.audit
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("", response_model=list[AuditEventResponse])
|
|
38
|
+
async def query_audit_events(
|
|
39
|
+
request: Request,
|
|
40
|
+
entity_type: str | None = None,
|
|
41
|
+
entity_id: str | None = None,
|
|
42
|
+
action: str | None = None,
|
|
43
|
+
since: str | None = None,
|
|
44
|
+
limit: int = 100,
|
|
45
|
+
) -> list[Any]:
|
|
46
|
+
return cast(
|
|
47
|
+
"list[Any]",
|
|
48
|
+
await _audit_log(request).query(
|
|
49
|
+
entity_type=entity_type,
|
|
50
|
+
entity_id=entity_id,
|
|
51
|
+
action=action,
|
|
52
|
+
since=since,
|
|
53
|
+
limit=limit,
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@router.get("/{event_id}", response_model=AuditEventResponse)
|
|
59
|
+
async def get_audit_event(event_id: str, request: Request) -> Any:
|
|
60
|
+
try:
|
|
61
|
+
return await _audit_log(request).get_by_id(UUID(event_id))
|
|
62
|
+
except ValueError as exc:
|
|
63
|
+
raise_registry_error(exc, not_found=True)
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cadlag. Licensed under BSL 1.1.
|
|
2
|
+
"""Backtest execution API routes."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Literal, cast
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Request, Response, status
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
11
|
+
|
|
12
|
+
from cadlag_ce.api.routes import raise_registry_error
|
|
13
|
+
from cadlag_ce.core.domain import BacktestResult, ExecutionConfig, FillSimConfig, VersionBindings
|
|
14
|
+
from cadlag_ce.core.exceptions import ConflictError, ResourceNotFoundError
|
|
15
|
+
|
|
16
|
+
router = APIRouter(tags=["Backtesting"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _RouteModel(BaseModel):
|
|
20
|
+
model_config = ConfigDict(from_attributes=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BacktestRequestSchema(_RouteModel):
|
|
24
|
+
model_config = ConfigDict(extra="forbid")
|
|
25
|
+
|
|
26
|
+
mode: Literal["simulation", "research", "sweep", "backtrader"] = "simulation"
|
|
27
|
+
promotion_grade: bool = False
|
|
28
|
+
use_ml: bool = True
|
|
29
|
+
use_fill_simulator: bool = True
|
|
30
|
+
use_platform_portfolio: bool = False
|
|
31
|
+
strategy_version_id: UUID | None = None
|
|
32
|
+
strategy_name: str | None = None
|
|
33
|
+
model_version_ids: list[str] = Field(default_factory=list)
|
|
34
|
+
model_version_id: UUID | None = None
|
|
35
|
+
symbol: str = "EURUSD"
|
|
36
|
+
interval: str = "D1"
|
|
37
|
+
start_date: Any | None = None
|
|
38
|
+
end_date: Any | None = None
|
|
39
|
+
dataset_version_id: UUID | None = None
|
|
40
|
+
feature_set_version_id: UUID | None = None
|
|
41
|
+
initial_cash: float | None = None
|
|
42
|
+
optimizer_class: str | None = None
|
|
43
|
+
parameter_search_space: dict[str, Any] | None = None
|
|
44
|
+
risk_profile_id: UUID | None = None
|
|
45
|
+
fill_sim_config: FillSimConfig | None = None
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="before")
|
|
48
|
+
@classmethod
|
|
49
|
+
def _reject_internal_identity(cls, value: Any) -> Any:
|
|
50
|
+
if isinstance(value, dict) and ({"run_id", "sweep_parent_run_id"} & set(value)):
|
|
51
|
+
raise ValueError("run_id and sweep_parent_run_id are server-assigned and are not accepted")
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
def to_config(self) -> ExecutionConfig:
|
|
55
|
+
return ExecutionConfig(**self.model_dump())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CompareSchema(_RouteModel):
|
|
59
|
+
run_ids: list[UUID]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BacktestResultResponse(_RouteModel):
|
|
63
|
+
run_id: UUID | None = None
|
|
64
|
+
mode: str
|
|
65
|
+
promotion_grade: bool
|
|
66
|
+
total_pnl: float
|
|
67
|
+
sharpe_ratio: float
|
|
68
|
+
max_drawdown: float
|
|
69
|
+
profit_factor: float | None = None
|
|
70
|
+
win_rate: float
|
|
71
|
+
total_trades: int
|
|
72
|
+
initial_cash: float
|
|
73
|
+
symbol: str
|
|
74
|
+
interval: str
|
|
75
|
+
start_date: Any
|
|
76
|
+
end_date: Any
|
|
77
|
+
use_platform_portfolio: bool
|
|
78
|
+
bindings: VersionBindings
|
|
79
|
+
source_providers: list[str] = Field(default_factory=list)
|
|
80
|
+
orders: list[dict[str, Any]] | None = None
|
|
81
|
+
fills: list[dict[str, Any]] | None = None
|
|
82
|
+
equity_curve: list[dict[str, Any]] | None = None
|
|
83
|
+
artifact_uri: str | None = None
|
|
84
|
+
artifact_manifest: dict[str, Any] | None = None
|
|
85
|
+
sweep_metadata: dict[str, Any] | None = None
|
|
86
|
+
created_at: Any
|
|
87
|
+
duration_seconds: float
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SweepResultResponse(_RouteModel):
|
|
91
|
+
mode: str = "sweep"
|
|
92
|
+
parent_run_id: UUID
|
|
93
|
+
results: list[BacktestResultResponse]
|
|
94
|
+
best_result: BacktestResultResponse
|
|
95
|
+
total_combinations: int
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SweepHistoryResponse(_RouteModel):
|
|
99
|
+
parent_run_id: UUID
|
|
100
|
+
strategy_version_id: UUID | None = None
|
|
101
|
+
symbol: str
|
|
102
|
+
interval: str
|
|
103
|
+
start_date: Any
|
|
104
|
+
end_date: Any
|
|
105
|
+
total_combinations: int
|
|
106
|
+
optimizer_target: str | None = None
|
|
107
|
+
created_at: Any
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class SweepSummaryResponse(SweepHistoryResponse):
|
|
111
|
+
best_run_id: UUID
|
|
112
|
+
best_metrics: dict[str, Any]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ComparisonResponse(_RouteModel):
|
|
116
|
+
run_ids: list[UUID]
|
|
117
|
+
metrics: dict[str, dict[str, float | int | None]]
|
|
118
|
+
best_run_id: UUID | None = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _execution_router(request: Request) -> Any:
|
|
122
|
+
return request.app.state.execution_router
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _backtest_repo(request: Request) -> Any:
|
|
126
|
+
return request.app.state.backtest_result_repository
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@router.post("/run", response_model=BacktestResultResponse | SweepResultResponse)
|
|
130
|
+
async def run_backtest(schema: BacktestRequestSchema, request: Request) -> BacktestResultResponse | SweepResultResponse:
|
|
131
|
+
result = await _execution_router(request).run_backtest(schema.to_config())
|
|
132
|
+
if isinstance(result, list):
|
|
133
|
+
return await _sweep_response(result, request)
|
|
134
|
+
return await _result_response(result, request)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@router.get("/results", response_model=list[BacktestResultResponse])
|
|
138
|
+
async def list_results(
|
|
139
|
+
request: Request,
|
|
140
|
+
strategy_version_id: UUID | None = None,
|
|
141
|
+
limit: int = 100,
|
|
142
|
+
offset: int = 0,
|
|
143
|
+
) -> list[BacktestResultResponse]:
|
|
144
|
+
results = await _backtest_repo(request).list_results(strategy_version_id, limit=limit, offset=offset)
|
|
145
|
+
return [await _result_response(result, request) for result in results]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@router.get("/results/{run_id}", response_model=BacktestResultResponse)
|
|
149
|
+
async def get_result(run_id: UUID, request: Request) -> BacktestResultResponse:
|
|
150
|
+
result = await _backtest_repo(request).get_by_run_id(run_id)
|
|
151
|
+
if result is None:
|
|
152
|
+
raise ResourceNotFoundError(f"Backtest result '{run_id}' not found")
|
|
153
|
+
return await _result_response(result, request)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@router.delete("/results/{run_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
157
|
+
async def delete_result(run_id: UUID, request: Request) -> Response:
|
|
158
|
+
try:
|
|
159
|
+
await _backtest_repo(request).delete_result(run_id)
|
|
160
|
+
except ValueError as exc:
|
|
161
|
+
raise_registry_error(exc)
|
|
162
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@router.get("/sweeps", response_model=list[SweepHistoryResponse])
|
|
166
|
+
async def list_sweeps(
|
|
167
|
+
request: Request,
|
|
168
|
+
strategy_version_id: UUID | None = None,
|
|
169
|
+
limit: int = 100,
|
|
170
|
+
offset: int = 0,
|
|
171
|
+
) -> list[dict[str, Any]]:
|
|
172
|
+
return cast(
|
|
173
|
+
"list[dict[str, Any]]",
|
|
174
|
+
await _backtest_repo(request).list_sweeps(strategy_version_id, limit=limit, offset=offset),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@router.get("/sweeps/{parent_run_id}", response_model=SweepSummaryResponse)
|
|
179
|
+
async def get_sweep_summary(parent_run_id: UUID, request: Request) -> dict[str, Any]:
|
|
180
|
+
summary = await _backtest_repo(request).sweep_summary(parent_run_id)
|
|
181
|
+
if summary is None:
|
|
182
|
+
raise ResourceNotFoundError(f"Sweep parent '{parent_run_id}' not found")
|
|
183
|
+
return cast("dict[str, Any]", summary)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@router.get("/sweeps/{parent_run_id}/results", response_model=SweepResultResponse)
|
|
187
|
+
async def get_sweep_results(
|
|
188
|
+
parent_run_id: UUID,
|
|
189
|
+
request: Request,
|
|
190
|
+
include_artifacts: bool = False,
|
|
191
|
+
) -> SweepResultResponse:
|
|
192
|
+
results = await _backtest_repo(request).list_sweep_results(parent_run_id, include_artifacts=include_artifacts)
|
|
193
|
+
if not results:
|
|
194
|
+
raise ResourceNotFoundError(f"Sweep parent '{parent_run_id}' not found")
|
|
195
|
+
return await _sweep_response(results, request, parent_run_id=parent_run_id)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@router.delete("/sweeps/{parent_run_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
199
|
+
async def delete_sweep(parent_run_id: UUID, request: Request) -> Response:
|
|
200
|
+
try:
|
|
201
|
+
await _backtest_repo(request).delete_sweep(parent_run_id)
|
|
202
|
+
except ValueError as exc:
|
|
203
|
+
raise_registry_error(exc)
|
|
204
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@router.post("/compare", response_model=ComparisonResponse)
|
|
208
|
+
async def compare_results(schema: CompareSchema, request: Request) -> ComparisonResponse:
|
|
209
|
+
metrics: dict[str, dict[str, float | int | None]] = {}
|
|
210
|
+
best_run_id: UUID | None = None
|
|
211
|
+
best_sharpe: float | None = None
|
|
212
|
+
for run_id in schema.run_ids:
|
|
213
|
+
result = await _backtest_repo(request).get_by_run_id(run_id)
|
|
214
|
+
if result is None:
|
|
215
|
+
raise ResourceNotFoundError(f"Backtest result '{run_id}' not found")
|
|
216
|
+
metrics[str(run_id)] = {
|
|
217
|
+
"total_pnl": result.total_pnl,
|
|
218
|
+
"sharpe_ratio": result.sharpe_ratio,
|
|
219
|
+
"max_drawdown": result.max_drawdown,
|
|
220
|
+
"profit_factor": result.profit_factor,
|
|
221
|
+
"win_rate": result.win_rate,
|
|
222
|
+
"total_trades": result.total_trades,
|
|
223
|
+
}
|
|
224
|
+
if best_sharpe is None or result.sharpe_ratio > best_sharpe:
|
|
225
|
+
best_sharpe = result.sharpe_ratio
|
|
226
|
+
best_run_id = run_id
|
|
227
|
+
return ComparisonResponse(run_ids=schema.run_ids, metrics=metrics, best_run_id=best_run_id)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def _result_response(result: BacktestResult, request: Request) -> BacktestResultResponse:
|
|
231
|
+
source_providers = await _source_providers(result, request)
|
|
232
|
+
payload = result.model_dump(mode="python")
|
|
233
|
+
payload["source_providers"] = source_providers
|
|
234
|
+
payload["orders"] = _dump_model_list(result.orders)
|
|
235
|
+
payload["fills"] = _dump_model_list(result.fills)
|
|
236
|
+
payload["equity_curve"] = _dump_model_list(result.equity_curve)
|
|
237
|
+
return BacktestResultResponse.model_validate(payload)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
async def _sweep_response(
|
|
241
|
+
results: list[BacktestResult],
|
|
242
|
+
request: Request,
|
|
243
|
+
parent_run_id: UUID | None = None,
|
|
244
|
+
) -> SweepResultResponse:
|
|
245
|
+
if not results:
|
|
246
|
+
raise ConflictError("Sweep response requires at least one result")
|
|
247
|
+
responses = [await _result_response(result, request) for result in results]
|
|
248
|
+
resolved_parent = parent_run_id or _parent_run_id(results[0])
|
|
249
|
+
if resolved_parent is None:
|
|
250
|
+
raise ConflictError("Sweep result is missing parent_run_id metadata")
|
|
251
|
+
return SweepResultResponse(
|
|
252
|
+
parent_run_id=resolved_parent,
|
|
253
|
+
results=responses,
|
|
254
|
+
best_result=responses[0],
|
|
255
|
+
total_combinations=len(responses),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def _source_providers(result: BacktestResult, request: Request) -> list[str]:
|
|
260
|
+
dataset_version_id = result.bindings.dataset_version_id
|
|
261
|
+
dataset_registry = getattr(request.app.state.registry, "dataset", None)
|
|
262
|
+
if dataset_version_id is None or dataset_registry is None:
|
|
263
|
+
return []
|
|
264
|
+
try:
|
|
265
|
+
version = await dataset_registry.resolve_version(str(dataset_version_id))
|
|
266
|
+
except ValueError:
|
|
267
|
+
return []
|
|
268
|
+
return list(version.source_providers)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _parent_run_id(result: BacktestResult) -> UUID | None:
|
|
272
|
+
metadata = result.sweep_metadata or {}
|
|
273
|
+
value = metadata.get("parent_run_id")
|
|
274
|
+
if value is None:
|
|
275
|
+
return None
|
|
276
|
+
return value if isinstance(value, UUID) else UUID(str(value))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _dump_model_list(values: list[Any] | None) -> list[dict[str, Any]] | None:
|
|
280
|
+
if values is None:
|
|
281
|
+
return None
|
|
282
|
+
dumped: list[dict[str, Any]] = []
|
|
283
|
+
for value in values:
|
|
284
|
+
if hasattr(value, "model_dump"):
|
|
285
|
+
dumped.append(value.model_dump(mode="python"))
|
|
286
|
+
elif isinstance(value, dict):
|
|
287
|
+
dumped.append(value)
|
|
288
|
+
return dumped
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cadlag. Licensed under BSL 1.1.
|
|
2
|
+
|
|
3
|
+
"""Broker status API routes."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
from fastapi import APIRouter, Request
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from cadlag_ce.core.broker_types import GatewayCapabilities
|
|
15
|
+
from cadlag_ce.data.gateway_registry import GatewayRegistry
|
|
16
|
+
|
|
17
|
+
logger = structlog.get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
router = APIRouter(tags=["Brokers"])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BrokerCapabilitiesResponse(BaseModel):
|
|
23
|
+
"""Serialised form of GatewayCapabilities."""
|
|
24
|
+
|
|
25
|
+
supports_historical_bars: bool
|
|
26
|
+
supports_price_snapshot: bool
|
|
27
|
+
supports_streaming_prices: bool
|
|
28
|
+
supports_streaming_bars: bool
|
|
29
|
+
supported_source_types: list[str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BrokerStatusResponse(BaseModel):
|
|
33
|
+
"""Status of a single registered broker."""
|
|
34
|
+
|
|
35
|
+
broker_id: str
|
|
36
|
+
type: str
|
|
37
|
+
data_connected: bool
|
|
38
|
+
order_connected: bool
|
|
39
|
+
health: dict[str, object] | None = None
|
|
40
|
+
capabilities: BrokerCapabilitiesResponse | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _serialize_capabilities(caps: GatewayCapabilities) -> BrokerCapabilitiesResponse:
|
|
44
|
+
return BrokerCapabilitiesResponse(
|
|
45
|
+
supports_historical_bars=caps.supports_historical_bars,
|
|
46
|
+
supports_price_snapshot=caps.supports_price_snapshot,
|
|
47
|
+
supports_streaming_prices=caps.supports_streaming_prices,
|
|
48
|
+
supports_streaming_bars=caps.supports_streaming_bars,
|
|
49
|
+
supported_source_types=sorted(caps.supported_source_types),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.get("/brokers", response_model=list[BrokerStatusResponse])
|
|
54
|
+
async def list_brokers(request: Request) -> list[BrokerStatusResponse]:
|
|
55
|
+
"""Return the status of all registered brokers."""
|
|
56
|
+
|
|
57
|
+
registry: GatewayRegistry = request.app.state.gateway_registry
|
|
58
|
+
data_gateways = registry.list_data()
|
|
59
|
+
order_gateways = registry.list_order()
|
|
60
|
+
|
|
61
|
+
broker_ids = sorted(set(data_gateways) | set(order_gateways))
|
|
62
|
+
results: list[BrokerStatusResponse] = []
|
|
63
|
+
for broker_id in broker_ids:
|
|
64
|
+
dg = data_gateways.get(broker_id)
|
|
65
|
+
og = order_gateways.get(broker_id)
|
|
66
|
+
|
|
67
|
+
data_connected = False
|
|
68
|
+
health_dict: dict[str, object] | None = None
|
|
69
|
+
caps: BrokerCapabilitiesResponse | None = None
|
|
70
|
+
if dg is not None:
|
|
71
|
+
try:
|
|
72
|
+
health = await dg.health_check()
|
|
73
|
+
data_connected = health.connected
|
|
74
|
+
health_dict = {
|
|
75
|
+
"connected": health.connected,
|
|
76
|
+
"trade_allowed": health.trade_allowed,
|
|
77
|
+
"latency_ms": health.latency_ms,
|
|
78
|
+
}
|
|
79
|
+
except Exception:
|
|
80
|
+
logger.warning("health_check_failed", broker_id=broker_id)
|
|
81
|
+
caps = _serialize_capabilities(dg.get_capabilities())
|
|
82
|
+
|
|
83
|
+
order_connected = False
|
|
84
|
+
if og is not None:
|
|
85
|
+
try:
|
|
86
|
+
order_connected = True
|
|
87
|
+
except Exception:
|
|
88
|
+
logger.warning("order_gateway_check_failed", broker_id=broker_id)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
broker_type = registry.get_provider_type(broker_id)
|
|
92
|
+
except KeyError:
|
|
93
|
+
broker_type = broker_id
|
|
94
|
+
|
|
95
|
+
results.append(
|
|
96
|
+
BrokerStatusResponse(
|
|
97
|
+
broker_id=broker_id,
|
|
98
|
+
type=broker_type,
|
|
99
|
+
data_connected=data_connected,
|
|
100
|
+
order_connected=order_connected,
|
|
101
|
+
health=health_dict,
|
|
102
|
+
capabilities=caps,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
return results
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cadlag. Licensed under BSL 1.1.
|
|
2
|
+
"""Configuration API routes."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Request
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
11
|
+
from cadlag_ce.api.routes import ensure_module
|
|
12
|
+
|
|
13
|
+
router = APIRouter(tags=["Config"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _RouteModel(BaseModel):
|
|
17
|
+
model_config = ConfigDict(from_attributes=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConfigResponse(_RouteModel):
|
|
21
|
+
config: dict[str, Any]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigValueResponse(_RouteModel):
|
|
25
|
+
key_path: str
|
|
26
|
+
value: Any
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _config_service(request: Request) -> Any:
|
|
30
|
+
return ensure_module(request.app.state.registry.config, "Config service")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.get("", response_model=ConfigResponse)
|
|
34
|
+
async def get_config(request: Request) -> ConfigResponse:
|
|
35
|
+
return ConfigResponse(config=await _config_service(request).get_config())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.get("/{key_path:path}", response_model=ConfigValueResponse)
|
|
39
|
+
async def get_config_value(key_path: str, request: Request) -> ConfigValueResponse:
|
|
40
|
+
return ConfigValueResponse(key_path=key_path, value=await _config_service(request).get_value(key_path))
|