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.

Files changed (149) hide show
  1. cadlag_ce-1.0.4/LICENSE +17 -0
  2. cadlag_ce-1.0.4/PKG-INFO +60 -0
  3. cadlag_ce-1.0.4/README.md +17 -0
  4. cadlag_ce-1.0.4/cadlag_ce/__init__.py +10 -0
  5. cadlag_ce-1.0.4/cadlag_ce/api/__init__.py +1 -0
  6. cadlag_ce-1.0.4/cadlag_ce/api/routes/__init__.py +43 -0
  7. cadlag_ce-1.0.4/cadlag_ce/api/routes/audit.py +63 -0
  8. cadlag_ce-1.0.4/cadlag_ce/api/routes/backtest.py +288 -0
  9. cadlag_ce-1.0.4/cadlag_ce/api/routes/brokers.py +105 -0
  10. cadlag_ce-1.0.4/cadlag_ce/api/routes/config.py +40 -0
  11. cadlag_ce-1.0.4/cadlag_ce/api/routes/data.py +285 -0
  12. cadlag_ce-1.0.4/cadlag_ce/api/routes/datasets.py +167 -0
  13. cadlag_ce-1.0.4/cadlag_ce/api/routes/deployments.py +168 -0
  14. cadlag_ce-1.0.4/cadlag_ce/api/routes/experiments.py +117 -0
  15. cadlag_ce-1.0.4/cadlag_ce/api/routes/feature_sets.py +162 -0
  16. cadlag_ce-1.0.4/cadlag_ce/api/routes/health.py +18 -0
  17. cadlag_ce-1.0.4/cadlag_ce/api/routes/live.py +197 -0
  18. cadlag_ce-1.0.4/cadlag_ce/api/routes/models.py +192 -0
  19. cadlag_ce-1.0.4/cadlag_ce/api/routes/pipeline.py +300 -0
  20. cadlag_ce-1.0.4/cadlag_ce/api/routes/risk.py +103 -0
  21. cadlag_ce-1.0.4/cadlag_ce/api/routes/strategies.py +177 -0
  22. cadlag_ce-1.0.4/cadlag_ce/api/routes/validation.py +163 -0
  23. cadlag_ce-1.0.4/cadlag_ce/api/ws/__init__.py +95 -0
  24. cadlag_ce-1.0.4/cadlag_ce/api/ws/audit.py +55 -0
  25. cadlag_ce-1.0.4/cadlag_ce/api/ws/backtest.py +91 -0
  26. cadlag_ce-1.0.4/cadlag_ce/api/ws/data.py +128 -0
  27. cadlag_ce-1.0.4/cadlag_ce/api/ws/deployment.py +84 -0
  28. cadlag_ce-1.0.4/cadlag_ce/api/ws/live.py +81 -0
  29. cadlag_ce-1.0.4/cadlag_ce/api/ws/pipeline.py +48 -0
  30. cadlag_ce-1.0.4/cadlag_ce/api/ws/sweep.py +85 -0
  31. cadlag_ce-1.0.4/cadlag_ce/cli/__init__.py +2 -0
  32. cadlag_ce-1.0.4/cadlag_ce/cli/maintenance.py +658 -0
  33. cadlag_ce-1.0.4/cadlag_ce/core/__init__.py +16 -0
  34. cadlag_ce-1.0.4/cadlag_ce/core/accounting.py +15 -0
  35. cadlag_ce-1.0.4/cadlag_ce/core/broker_types.py +297 -0
  36. cadlag_ce-1.0.4/cadlag_ce/core/capabilities.py +141 -0
  37. cadlag_ce-1.0.4/cadlag_ce/core/capability_keys.py +49 -0
  38. cadlag_ce-1.0.4/cadlag_ce/core/config.py +268 -0
  39. cadlag_ce-1.0.4/cadlag_ce/core/domain.py +1124 -0
  40. cadlag_ce-1.0.4/cadlag_ce/core/exceptions.py +171 -0
  41. cadlag_ce-1.0.4/cadlag_ce/core/extension_manifest.py +41 -0
  42. cadlag_ce-1.0.4/cadlag_ce/core/extensions.py +332 -0
  43. cadlag_ce-1.0.4/cadlag_ce/core/license_guards.py +58 -0
  44. cadlag_ce-1.0.4/cadlag_ce/core/logging.py +170 -0
  45. cadlag_ce-1.0.4/cadlag_ce/core/order_fsm.py +159 -0
  46. cadlag_ce-1.0.4/cadlag_ce/core/parameter_timeline.py +158 -0
  47. cadlag_ce-1.0.4/cadlag_ce/core/protocols.py +751 -0
  48. cadlag_ce-1.0.4/cadlag_ce/core/version_binding.py +308 -0
  49. cadlag_ce-1.0.4/cadlag_ce/data/__init__.py +5 -0
  50. cadlag_ce-1.0.4/cadlag_ce/data/adapters/__init__.py +1 -0
  51. cadlag_ce-1.0.4/cadlag_ce/data/adapters/basic_polling_adapter.py +146 -0
  52. cadlag_ce-1.0.4/cadlag_ce/data/adapters/broker_bar_source.py +137 -0
  53. cadlag_ce-1.0.4/cadlag_ce/data/broker_circuit_breaker.py +95 -0
  54. cadlag_ce-1.0.4/cadlag_ce/data/broker_provider_registry.py +47 -0
  55. cadlag_ce-1.0.4/cadlag_ce/data/data_service.py +409 -0
  56. cadlag_ce-1.0.4/cadlag_ce/data/dataset_snapshot.py +69 -0
  57. cadlag_ce-1.0.4/cadlag_ce/data/feature_store.py +246 -0
  58. cadlag_ce-1.0.4/cadlag_ce/data/gateway_registry.py +124 -0
  59. cadlag_ce-1.0.4/cadlag_ce/data/ingest_adapter.py +256 -0
  60. cadlag_ce-1.0.4/cadlag_ce/data/lakehouse_catalog.py +55 -0
  61. cadlag_ce-1.0.4/cadlag_ce/data/metrics.py +14 -0
  62. cadlag_ce-1.0.4/cadlag_ce/data/mt5/__init__.py +23 -0
  63. cadlag_ce-1.0.4/cadlag_ce/data/mt5/bridge_client.py +219 -0
  64. cadlag_ce-1.0.4/cadlag_ce/data/mt5/connection.py +207 -0
  65. cadlag_ce-1.0.4/cadlag_ce/data/mt5/data_gateway.py +148 -0
  66. cadlag_ce-1.0.4/cadlag_ce/data/mt5/order_gateway.py +96 -0
  67. cadlag_ce-1.0.4/cadlag_ce/data/mt5/timezone_verify.py +77 -0
  68. cadlag_ce-1.0.4/cadlag_ce/data/mt5/translator.py +417 -0
  69. cadlag_ce-1.0.4/cadlag_ce/data/parquet_writer.py +212 -0
  70. cadlag_ce-1.0.4/cadlag_ce/data/phantom/__init__.py +21 -0
  71. cadlag_ce-1.0.4/cadlag_ce/data/phantom/clock.py +136 -0
  72. cadlag_ce-1.0.4/cadlag_ce/data/phantom/config.py +214 -0
  73. cadlag_ce-1.0.4/cadlag_ce/data/phantom/data_gateway.py +346 -0
  74. cadlag_ce-1.0.4/cadlag_ce/data/phantom/order_gateway.py +389 -0
  75. cadlag_ce-1.0.4/cadlag_ce/data/phantom/sessions.py +301 -0
  76. cadlag_ce-1.0.4/cadlag_ce/data/phantom/synthesis.py +140 -0
  77. cadlag_ce-1.0.4/cadlag_ce/data/query_engine.py +291 -0
  78. cadlag_ce-1.0.4/cadlag_ce/data/questdb_init.py +67 -0
  79. cadlag_ce-1.0.4/cadlag_ce/data/questdb_writer.py +159 -0
  80. cadlag_ce-1.0.4/cadlag_ce/data/types.py +69 -0
  81. cadlag_ce-1.0.4/cadlag_ce/execution/__init__.py +71 -0
  82. cadlag_ce-1.0.4/cadlag_ce/execution/adapters/__init__.py +15 -0
  83. cadlag_ce-1.0.4/cadlag_ce/execution/adapters/backtrader_adapter.py +419 -0
  84. cadlag_ce-1.0.4/cadlag_ce/execution/adapters/base.py +89 -0
  85. cadlag_ce-1.0.4/cadlag_ce/execution/adapters/broker_order_adapter.py +174 -0
  86. cadlag_ce-1.0.4/cadlag_ce/execution/adapters/paper_order_adapter.py +159 -0
  87. cadlag_ce-1.0.4/cadlag_ce/execution/backtest_result_repository.py +912 -0
  88. cadlag_ce-1.0.4/cadlag_ce/execution/deployment_binding.py +538 -0
  89. cadlag_ce-1.0.4/cadlag_ce/execution/deployment_entitlement.py +88 -0
  90. cadlag_ce-1.0.4/cadlag_ce/execution/fill_simulator.py +94 -0
  91. cadlag_ce-1.0.4/cadlag_ce/execution/live_chain.py +595 -0
  92. cadlag_ce-1.0.4/cadlag_ce/execution/live_deployment_repository.py +544 -0
  93. cadlag_ce-1.0.4/cadlag_ce/execution/live_order_persister.py +409 -0
  94. cadlag_ce-1.0.4/cadlag_ce/execution/live_runtime_manager.py +1434 -0
  95. cadlag_ce-1.0.4/cadlag_ce/execution/model_runtime.py +472 -0
  96. cadlag_ce-1.0.4/cadlag_ce/execution/optimizer.py +293 -0
  97. cadlag_ce-1.0.4/cadlag_ce/execution/order_identity.py +81 -0
  98. cadlag_ce-1.0.4/cadlag_ce/execution/order_router.py +275 -0
  99. cadlag_ce-1.0.4/cadlag_ce/execution/portfolio_book.py +784 -0
  100. cadlag_ce-1.0.4/cadlag_ce/execution/prediction_cache.py +467 -0
  101. cadlag_ce-1.0.4/cadlag_ce/execution/risk_engine.py +389 -0
  102. cadlag_ce-1.0.4/cadlag_ce/execution/router.py +538 -0
  103. cadlag_ce-1.0.4/cadlag_ce/execution/rule_engine.py +195 -0
  104. cadlag_ce-1.0.4/cadlag_ce/execution/simplified_portfolio.py +447 -0
  105. cadlag_ce-1.0.4/cadlag_ce/execution/simulation_engine.py +825 -0
  106. cadlag_ce-1.0.4/cadlag_ce/execution/strategy_adapter.py +474 -0
  107. cadlag_ce-1.0.4/cadlag_ce/execution/trading_statistics.py +201 -0
  108. cadlag_ce-1.0.4/cadlag_ce/pipeline/__init__.py +6 -0
  109. cadlag_ce-1.0.4/cadlag_ce/pipeline/input_shaper.py +154 -0
  110. cadlag_ce-1.0.4/cadlag_ce/pipeline/orchestrator.py +425 -0
  111. cadlag_ce-1.0.4/cadlag_ce/pipeline/stage_executor.py +131 -0
  112. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/__init__.py +1 -0
  113. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/base.py +17 -0
  114. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/compare_baseline.py +80 -0
  115. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/compute_trading_metrics.py +48 -0
  116. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/dataset_freeze.py +150 -0
  117. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/dataset_split.py +153 -0
  118. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/feature_materialize.py +239 -0
  119. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/generate_reports.py +245 -0
  120. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/load_dataset.py +82 -0
  121. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/predictive_validation.py +288 -0
  122. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/run_backtest.py +235 -0
  123. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/schema_validation.py +163 -0
  124. cadlag_ce-1.0.4/cadlag_ce/pipeline/stages/trainer_execution.py +284 -0
  125. cadlag_ce-1.0.4/cadlag_ce/pipeline/trainer_adapter.py +154 -0
  126. cadlag_ce-1.0.4/cadlag_ce/pipeline/training_pipeline.py +223 -0
  127. cadlag_ce-1.0.4/cadlag_ce/pipeline/types.py +254 -0
  128. cadlag_ce-1.0.4/cadlag_ce/pipeline/validation_pipeline.py +196 -0
  129. cadlag_ce-1.0.4/cadlag_ce/registry/__init__.py +163 -0
  130. cadlag_ce-1.0.4/cadlag_ce/registry/audit.py +156 -0
  131. cadlag_ce-1.0.4/cadlag_ce/registry/config_service.py +152 -0
  132. cadlag_ce-1.0.4/cadlag_ce/registry/dataset_registry.py +734 -0
  133. cadlag_ce-1.0.4/cadlag_ce/registry/experiment_tracker.py +401 -0
  134. cadlag_ce-1.0.4/cadlag_ce/registry/feature_registry.py +380 -0
  135. cadlag_ce-1.0.4/cadlag_ce/registry/model_registry.py +521 -0
  136. cadlag_ce-1.0.4/cadlag_ce/registry/model_watcher.py +221 -0
  137. cadlag_ce-1.0.4/cadlag_ce/registry/registry.py +108 -0
  138. cadlag_ce-1.0.4/cadlag_ce/registry/risk_policy.py +333 -0
  139. cadlag_ce-1.0.4/cadlag_ce/registry/strategy_registry.py +500 -0
  140. cadlag_ce-1.0.4/cadlag_ce/registry/validation_registry.py +514 -0
  141. cadlag_ce-1.0.4/cadlag_ce/server.py +536 -0
  142. cadlag_ce-1.0.4/cadlag_ce.egg-info/PKG-INFO +60 -0
  143. cadlag_ce-1.0.4/cadlag_ce.egg-info/SOURCES.txt +147 -0
  144. cadlag_ce-1.0.4/cadlag_ce.egg-info/dependency_links.txt +1 -0
  145. cadlag_ce-1.0.4/cadlag_ce.egg-info/entry_points.txt +2 -0
  146. cadlag_ce-1.0.4/cadlag_ce.egg-info/requires.txt +31 -0
  147. cadlag_ce-1.0.4/cadlag_ce.egg-info/top_level.txt +1 -0
  148. cadlag_ce-1.0.4/pyproject.toml +91 -0
  149. cadlag_ce-1.0.4/setup.cfg +4 -0
@@ -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/
@@ -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,10 @@
1
+ # Copyright (c) 2026 Cadlag. Licensed under BSL 1.1.
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("cadlag-ce")
7
+ except PackageNotFoundError:
8
+ __version__ = "0+unknown"
9
+
10
+ __all__ = ["__version__"]
@@ -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))