classifiedai 0.1.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.
Files changed (40) hide show
  1. classifiedai-0.1.0/PKG-INFO +228 -0
  2. classifiedai-0.1.0/README.md +209 -0
  3. classifiedai-0.1.0/classified/__init__.py +0 -0
  4. classifiedai-0.1.0/classified/api/__init__.py +1 -0
  5. classifiedai-0.1.0/classified/api/app.py +96 -0
  6. classifiedai-0.1.0/classified/api/models.py +42 -0
  7. classifiedai-0.1.0/classified/api/routes.py +110 -0
  8. classifiedai-0.1.0/classified/api/server.py +431 -0
  9. classifiedai-0.1.0/classified/api/stream.py +190 -0
  10. classifiedai-0.1.0/classified/api/test_api.py +104 -0
  11. classifiedai-0.1.0/classified/api/test_stream.py +51 -0
  12. classifiedai-0.1.0/classified/exchange/__init__.py +17 -0
  13. classifiedai-0.1.0/classified/exchange/base.py +66 -0
  14. classifiedai-0.1.0/classified/exchange/ccxt_adapter.py +220 -0
  15. classifiedai-0.1.0/classified/exchange/market_stream.py +292 -0
  16. classifiedai-0.1.0/classified/exchange/test_ccxt_adapter.py +56 -0
  17. classifiedai-0.1.0/classified/exchange/test_ccxt_adapter_current_ticker.py +22 -0
  18. classifiedai-0.1.0/classified/market_data/__init__.py +0 -0
  19. classifiedai-0.1.0/classified/market_data/cache.py +187 -0
  20. classifiedai-0.1.0/classified/market_data/context.py +346 -0
  21. classifiedai-0.1.0/classified/market_data/data_requests.py +92 -0
  22. classifiedai-0.1.0/classified/market_data/freshness.py +135 -0
  23. classifiedai-0.1.0/classified/market_data/manager.py +340 -0
  24. classifiedai-0.1.0/classified/market_data/models.py +111 -0
  25. classifiedai-0.1.0/classified/market_data/normalizer.py +217 -0
  26. classifiedai-0.1.0/classified/market_data/test_cache.py +110 -0
  27. classifiedai-0.1.0/classified/market_data/test_cache_integration.py +152 -0
  28. classifiedai-0.1.0/classified/market_data/test_context.py +55 -0
  29. classifiedai-0.1.0/classified/market_data/test_freshness.py +64 -0
  30. classifiedai-0.1.0/classified/market_data/test_manager.py +58 -0
  31. classifiedai-0.1.0/classified/market_data/test_multi_request.py +80 -0
  32. classifiedai-0.1.0/classified/market_data/test_normalizer.py +144 -0
  33. classifiedai-0.1.0/classified/market_data/test_request.py +58 -0
  34. classifiedai-0.1.0/classifiedai.egg-info/PKG-INFO +228 -0
  35. classifiedai-0.1.0/classifiedai.egg-info/SOURCES.txt +38 -0
  36. classifiedai-0.1.0/classifiedai.egg-info/dependency_links.txt +1 -0
  37. classifiedai-0.1.0/classifiedai.egg-info/requires.txt +13 -0
  38. classifiedai-0.1.0/classifiedai.egg-info/top_level.txt +1 -0
  39. classifiedai-0.1.0/pyproject.toml +29 -0
  40. classifiedai-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: classifiedai
3
+ Version: 0.1.0
4
+ Summary: Classified trading analysis platform
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: ccxt
8
+ Requires-Dist: pandas
9
+ Requires-Dist: numpy
10
+ Requires-Dist: aiohttp
11
+ Requires-Dist: python-dotenv
12
+ Requires-Dist: requests
13
+ Requires-Dist: matplotlib
14
+ Requires-Dist: mplfinance
15
+ Requires-Dist: ta
16
+ Requires-Dist: websockets
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest; extra == "test"
19
+
20
+ # CLASSIFIED
21
+
22
+ ### Market Data Infrastructure · Real-Time Streaming · Multi-Timeframe Intelligence
23
+
24
+ **A unified market-data infrastructure package for exchange connectivity, normalized OHLCV data, multi-timeframe requests, and HTTP/WebSocket delivery.**
25
+
26
+ CLASSIFIED provides shared market-data infrastructure for charting applications, analytical engines, and specialist trading systems. It centralizes exchange connectivity and market-data handling so independent consumers can work with a consistent foundation rather than implementing their own exchange integrations.
27
+
28
+ The package is published under the distribution name `classifiedai`, while its Python import namespace remains `classified`.
29
+
30
+ > **Core principle:** One market-data foundation. Multiple independent consumers. A consistent data contract.
31
+
32
+ ---
33
+
34
+ ## Features
35
+
36
+ - **Exchange connectivity** — integrate with supported exchanges through CCXT.
37
+ - **Normalized OHLCV data** — work with Open, High, Low, Close, and Volume candle data.
38
+ - **Multi-timeframe requests** — request market data at different candle intervals.
39
+ - **Historical market data** — retrieve candles for charting and analysis.
40
+ - **Adaptive candle requirements** — centralize candle-count requirements for data consumers where supported by the active implementation.
41
+ - **HTTP API** — expose market data through HTTP endpoints.
42
+ - **WebSocket streaming** — support persistent connections for live market-data delivery.
43
+ - **Shared infrastructure** — allow independent applications to reuse the same market-data foundation.
44
+
45
+ CLASSIFIED is infrastructure, not a trading strategy or trade-execution engine.
46
+
47
+ ---
48
+
49
+ ## Installation
50
+
51
+ Install the package from PyPI:
52
+
53
+ ```bash
54
+ python -m pip install classifiedai
55
+ ```
56
+
57
+ ### Requirements
58
+
59
+ - Python 3.12 or newer.
60
+ - Network access to the configured exchange where live exchange data is required.
61
+ - Appropriate exchange configuration for the selected data source.
62
+
63
+ ### Verify the installation
64
+
65
+ ```bash
66
+ python -c "import classified; print(classified.__file__)"
67
+ ```
68
+
69
+ The distribution name used by `pip` is `classifiedai`. The import namespace used in Python remains `classified`.
70
+
71
+ ### Install optional test dependencies
72
+
73
+ ```bash
74
+ python -m pip install "classifiedai[test]"
75
+ ```
76
+
77
+ This command is available when the published release includes the `test` optional dependency extra.
78
+
79
+ ---
80
+
81
+ ## Architecture
82
+
83
+ ```text
84
+ EXCHANGE DATA SOURCE
85
+ |
86
+ v
87
+ +----------------------+
88
+ | CLASSIFIED Core |
89
+ | |
90
+ | Exchange Connectivity|
91
+ | OHLCV Normalization |
92
+ | Multi-Timeframe Data |
93
+ | Candle Requirements |
94
+ | Cache & Freshness |
95
+ +----------+-----------+
96
+ |
97
+ v
98
+ +----------------------+
99
+ | API Gateway |
100
+ | |
101
+ | HTTP / REST |
102
+ | WebSocket Streaming |
103
+ +----------+-----------+
104
+ |
105
+ +------------+------------+
106
+ | | |
107
+ v v v
108
+ Visualization Analytical Other Data
109
+ Applications Systems Consumers
110
+ ```
111
+
112
+ *Conceptual architecture. The precise responsibilities and available capabilities depend on the installed implementation and its configuration.*
113
+
114
+ ---
115
+
116
+ ## Market Data
117
+
118
+ CLASSIFIED is designed to provide a common interface for market-data consumers that need historical candles, multiple timeframes, or continuous updates.
119
+
120
+ ### OHLCV data
121
+
122
+ OHLCV represents:
123
+
124
+ | Field | Meaning |
125
+ |---|---|
126
+ | Open | Opening price for a candle |
127
+ | High | Highest price reached during the candle |
128
+ | Low | Lowest price reached during the candle |
129
+ | Close | Closing price for a candle |
130
+ | Volume | Volume recorded during the candle |
131
+
132
+ ### Multi-timeframe access
133
+
134
+ Example candle intervals include:
135
+
136
+ | Timeframe | Typical analytical use |
137
+ |---|---|
138
+ | `4h` | Higher-timeframe market structure |
139
+ | `1h` | Intermediate market context |
140
+ | `15m` | Intraday price action |
141
+ | `5m` | Short-term price monitoring |
142
+
143
+ These are example intervals, not trading signals. Actual supported intervals and candle limits depend on the exchange and implementation.
144
+
145
+ ---
146
+
147
+ ## API Gateway
148
+
149
+ The gateway provides HTTP-based access and WebSocket streaming through an asynchronous server using `aiohttp`.
150
+
151
+ The documented local server address is:
152
+
153
+ ```text
154
+ http://127.0.0.1:8080
155
+ ```
156
+
157
+ The server must be started separately when using the gateway endpoints.
158
+
159
+ ### Start the gateway
160
+
161
+ From an environment where CLASSIFIED and its dependencies are installed, run:
162
+
163
+ ```bash
164
+ python -m classified.api.server
165
+ ```
166
+
167
+ The server is configured to listen on port `8080` in the documented setup.
168
+
169
+ ### 1. Health check
170
+
171
+ **Method:** `GET`
172
+
173
+ ```http
174
+ GET /health
175
+ ```
176
+
177
+ Local URL:
178
+
179
+ ```text
180
+ http://127.0.0.1:8080/health
181
+ ```
182
+
183
+ Use this endpoint to check whether the gateway is responding.
184
+
185
+ ### 2. Historical candle data
186
+
187
+ **Method:** `GET`
188
+
189
+ ```http
190
+ GET /api/v1/market-data/candles
191
+ ```
192
+
193
+ Query parameters:
194
+
195
+ | Parameter | Description | Example |
196
+ |---|---|---|
197
+ | `symbol` | Trading pair | `BTC/USDT` |
198
+ | `timeframe` | Candle interval | `5m` |
199
+ | `candle_count` | Requested candle count | `150` |
200
+
201
+ Example URL:
202
+
203
+ ```text
204
+ http://127.0.0.1:8080/api/v1/market-data/candles?symbol=BTC%2FUSDT&timeframe=5m&candle_count=150
205
+ ```
206
+
207
+ PowerShell example:
208
+
209
+ ```powershell
210
+ Invoke-RestMethod `
211
+ "http://127.0.0.1:8080/api/v1/market-data/candles?symbol=BTC%2FUSDT&timeframe=5m&candle_count=150"
212
+ ```
213
+
214
+ The endpoint is intended to supply historical candle data to charting and analytical consumers. Consult the active server implementation for the exact response schema and error behavior.
215
+
216
+ ### 3. Live market-data stream
217
+
218
+ **Protocol:** WebSocket
219
+
220
+ ```text
221
+ ws://127.0.0.1:8080/api/v1/market-data/stream
222
+ ```
223
+
224
+ Query parameters:
225
+
226
+ | Parameter | Description | Example |
227
+ |---|---|---|
228
+ | `symbol`
@@ -0,0 +1,209 @@
1
+ # CLASSIFIED
2
+
3
+ ### Market Data Infrastructure · Real-Time Streaming · Multi-Timeframe Intelligence
4
+
5
+ **A unified market-data infrastructure package for exchange connectivity, normalized OHLCV data, multi-timeframe requests, and HTTP/WebSocket delivery.**
6
+
7
+ CLASSIFIED provides shared market-data infrastructure for charting applications, analytical engines, and specialist trading systems. It centralizes exchange connectivity and market-data handling so independent consumers can work with a consistent foundation rather than implementing their own exchange integrations.
8
+
9
+ The package is published under the distribution name `classifiedai`, while its Python import namespace remains `classified`.
10
+
11
+ > **Core principle:** One market-data foundation. Multiple independent consumers. A consistent data contract.
12
+
13
+ ---
14
+
15
+ ## Features
16
+
17
+ - **Exchange connectivity** — integrate with supported exchanges through CCXT.
18
+ - **Normalized OHLCV data** — work with Open, High, Low, Close, and Volume candle data.
19
+ - **Multi-timeframe requests** — request market data at different candle intervals.
20
+ - **Historical market data** — retrieve candles for charting and analysis.
21
+ - **Adaptive candle requirements** — centralize candle-count requirements for data consumers where supported by the active implementation.
22
+ - **HTTP API** — expose market data through HTTP endpoints.
23
+ - **WebSocket streaming** — support persistent connections for live market-data delivery.
24
+ - **Shared infrastructure** — allow independent applications to reuse the same market-data foundation.
25
+
26
+ CLASSIFIED is infrastructure, not a trading strategy or trade-execution engine.
27
+
28
+ ---
29
+
30
+ ## Installation
31
+
32
+ Install the package from PyPI:
33
+
34
+ ```bash
35
+ python -m pip install classifiedai
36
+ ```
37
+
38
+ ### Requirements
39
+
40
+ - Python 3.12 or newer.
41
+ - Network access to the configured exchange where live exchange data is required.
42
+ - Appropriate exchange configuration for the selected data source.
43
+
44
+ ### Verify the installation
45
+
46
+ ```bash
47
+ python -c "import classified; print(classified.__file__)"
48
+ ```
49
+
50
+ The distribution name used by `pip` is `classifiedai`. The import namespace used in Python remains `classified`.
51
+
52
+ ### Install optional test dependencies
53
+
54
+ ```bash
55
+ python -m pip install "classifiedai[test]"
56
+ ```
57
+
58
+ This command is available when the published release includes the `test` optional dependency extra.
59
+
60
+ ---
61
+
62
+ ## Architecture
63
+
64
+ ```text
65
+ EXCHANGE DATA SOURCE
66
+ |
67
+ v
68
+ +----------------------+
69
+ | CLASSIFIED Core |
70
+ | |
71
+ | Exchange Connectivity|
72
+ | OHLCV Normalization |
73
+ | Multi-Timeframe Data |
74
+ | Candle Requirements |
75
+ | Cache & Freshness |
76
+ +----------+-----------+
77
+ |
78
+ v
79
+ +----------------------+
80
+ | API Gateway |
81
+ | |
82
+ | HTTP / REST |
83
+ | WebSocket Streaming |
84
+ +----------+-----------+
85
+ |
86
+ +------------+------------+
87
+ | | |
88
+ v v v
89
+ Visualization Analytical Other Data
90
+ Applications Systems Consumers
91
+ ```
92
+
93
+ *Conceptual architecture. The precise responsibilities and available capabilities depend on the installed implementation and its configuration.*
94
+
95
+ ---
96
+
97
+ ## Market Data
98
+
99
+ CLASSIFIED is designed to provide a common interface for market-data consumers that need historical candles, multiple timeframes, or continuous updates.
100
+
101
+ ### OHLCV data
102
+
103
+ OHLCV represents:
104
+
105
+ | Field | Meaning |
106
+ |---|---|
107
+ | Open | Opening price for a candle |
108
+ | High | Highest price reached during the candle |
109
+ | Low | Lowest price reached during the candle |
110
+ | Close | Closing price for a candle |
111
+ | Volume | Volume recorded during the candle |
112
+
113
+ ### Multi-timeframe access
114
+
115
+ Example candle intervals include:
116
+
117
+ | Timeframe | Typical analytical use |
118
+ |---|---|
119
+ | `4h` | Higher-timeframe market structure |
120
+ | `1h` | Intermediate market context |
121
+ | `15m` | Intraday price action |
122
+ | `5m` | Short-term price monitoring |
123
+
124
+ These are example intervals, not trading signals. Actual supported intervals and candle limits depend on the exchange and implementation.
125
+
126
+ ---
127
+
128
+ ## API Gateway
129
+
130
+ The gateway provides HTTP-based access and WebSocket streaming through an asynchronous server using `aiohttp`.
131
+
132
+ The documented local server address is:
133
+
134
+ ```text
135
+ http://127.0.0.1:8080
136
+ ```
137
+
138
+ The server must be started separately when using the gateway endpoints.
139
+
140
+ ### Start the gateway
141
+
142
+ From an environment where CLASSIFIED and its dependencies are installed, run:
143
+
144
+ ```bash
145
+ python -m classified.api.server
146
+ ```
147
+
148
+ The server is configured to listen on port `8080` in the documented setup.
149
+
150
+ ### 1. Health check
151
+
152
+ **Method:** `GET`
153
+
154
+ ```http
155
+ GET /health
156
+ ```
157
+
158
+ Local URL:
159
+
160
+ ```text
161
+ http://127.0.0.1:8080/health
162
+ ```
163
+
164
+ Use this endpoint to check whether the gateway is responding.
165
+
166
+ ### 2. Historical candle data
167
+
168
+ **Method:** `GET`
169
+
170
+ ```http
171
+ GET /api/v1/market-data/candles
172
+ ```
173
+
174
+ Query parameters:
175
+
176
+ | Parameter | Description | Example |
177
+ |---|---|---|
178
+ | `symbol` | Trading pair | `BTC/USDT` |
179
+ | `timeframe` | Candle interval | `5m` |
180
+ | `candle_count` | Requested candle count | `150` |
181
+
182
+ Example URL:
183
+
184
+ ```text
185
+ http://127.0.0.1:8080/api/v1/market-data/candles?symbol=BTC%2FUSDT&timeframe=5m&candle_count=150
186
+ ```
187
+
188
+ PowerShell example:
189
+
190
+ ```powershell
191
+ Invoke-RestMethod `
192
+ "http://127.0.0.1:8080/api/v1/market-data/candles?symbol=BTC%2FUSDT&timeframe=5m&candle_count=150"
193
+ ```
194
+
195
+ The endpoint is intended to supply historical candle data to charting and analytical consumers. Consult the active server implementation for the exact response schema and error behavior.
196
+
197
+ ### 3. Live market-data stream
198
+
199
+ **Protocol:** WebSocket
200
+
201
+ ```text
202
+ ws://127.0.0.1:8080/api/v1/market-data/stream
203
+ ```
204
+
205
+ Query parameters:
206
+
207
+ | Parameter | Description | Example |
208
+ |---|---|---|
209
+ | `symbol`
File without changes
@@ -0,0 +1 @@
1
+ """Classified market-data transport APIs."""
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from aiohttp import web
6
+
7
+ from classified.exchange.ccxt_adapter import CCXTExchangeAdapter
8
+ from classified.market_data.manager import MarketDataManager
9
+
10
+ from .routes import register_routes
11
+ from .stream import create_stream_registry
12
+
13
+
14
+ def _env_bool(name: str, default: bool = False) -> bool:
15
+ value = os.getenv(name)
16
+ if value is None:
17
+ return default
18
+ return value.strip().lower() in {"1", "true", "yes", "on"}
19
+
20
+
21
+ def create_manager() -> MarketDataManager:
22
+ exchange_id = os.getenv("CLASSIFIED_EXCHANGE_ID", "bingx").strip().lower()
23
+ adapter = CCXTExchangeAdapter(
24
+ exchange_id,
25
+ api_key=os.getenv("CLASSIFIED_EXCHANGE_API_KEY") or None,
26
+ secret=os.getenv("CLASSIFIED_EXCHANGE_SECRET") or None,
27
+ password=os.getenv("CLASSIFIED_EXCHANGE_PASSWORD") or None,
28
+ sandbox=_env_bool("CLASSIFIED_EXCHANGE_SANDBOX", False),
29
+ enable_rate_limit=True,
30
+ timeout=int(os.getenv("CLASSIFIED_EXCHANGE_TIMEOUT_MS", "30000")),
31
+ )
32
+ return MarketDataManager(adapter)
33
+
34
+
35
+ def _cors_middleware() -> web.middleware:
36
+ raw = os.getenv(
37
+ "CLASSIFIED_API_CORS_ORIGINS",
38
+ "http://localhost:5173,http://127.0.0.1:5173",
39
+ )
40
+ allowed_origins = frozenset(
41
+ origin.strip() for origin in raw.split(",") if origin.strip()
42
+ )
43
+
44
+ @web.middleware
45
+ async def middleware(request: web.Request, handler):
46
+ if request.method == "OPTIONS":
47
+ response = web.Response(status=204)
48
+ else:
49
+ response = await handler(request)
50
+
51
+ origin = request.headers.get("Origin")
52
+ if origin in allowed_origins:
53
+ response.headers["Access-Control-Allow-Origin"] = origin
54
+ response.headers["Vary"] = "Origin"
55
+ response.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS"
56
+ response.headers["Access-Control-Allow-Headers"] = "Content-Type"
57
+
58
+ return response
59
+
60
+ return middleware
61
+
62
+
63
+ async def _on_startup(app: web.Application) -> None:
64
+ manager: MarketDataManager = app["market_data_manager"]
65
+ manager.connect()
66
+
67
+
68
+ async def _on_cleanup(app: web.Application) -> None:
69
+ registry = app.get("market_stream_registry")
70
+ if registry is not None:
71
+ await registry.close()
72
+
73
+ manager: MarketDataManager = app["market_data_manager"]
74
+ manager.close()
75
+
76
+
77
+ def create_app(manager: MarketDataManager | None = None) -> web.Application:
78
+ resolved_manager = manager or create_manager()
79
+ app = web.Application(middlewares=[_cors_middleware()])
80
+ register_routes(app, resolved_manager)
81
+ app["market_stream_registry"] = create_stream_registry(
82
+ resolved_manager.exchange_name
83
+ )
84
+ app.on_startup.append(_on_startup)
85
+ app.on_cleanup.append(_on_cleanup)
86
+ return app
87
+
88
+
89
+ def main() -> None:
90
+ host = os.getenv("CLASSIFIED_API_HOST", "127.0.0.1")
91
+ port = int(os.getenv("CLASSIFIED_API_PORT", "8080"))
92
+ web.run_app(create_app(), host=host, port=port)
93
+
94
+
95
+ if __name__ == "__main__":
96
+ main()
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import timezone
4
+ from typing import Any
5
+
6
+ from classified.market_data.manager import MarketDataResult
7
+
8
+
9
+ def candle_to_dict(candle: Any) -> dict[str, Any]:
10
+ """Serialize one Classified Candle for external clients."""
11
+ timestamp = candle.timestamp
12
+
13
+ if timestamp.tzinfo is None or timestamp.utcoffset() is None:
14
+ timestamp = timestamp.replace(tzinfo=timezone.utc)
15
+ else:
16
+ timestamp = timestamp.astimezone(timezone.utc)
17
+
18
+ epoch_seconds = int(timestamp.timestamp())
19
+
20
+ return {
21
+ "timestamp": epoch_seconds,
22
+ "timestampMs": epoch_seconds * 1000,
23
+ "open": candle.open,
24
+ "high": candle.high,
25
+ "low": candle.low,
26
+ "close": candle.close,
27
+ "volume": candle.volume,
28
+ }
29
+
30
+
31
+ def market_data_result_to_dict(result: MarketDataResult) -> dict[str, Any]:
32
+ """Serialize a market-data result without exposing internal objects."""
33
+ return {
34
+ "symbol": result.symbol,
35
+ "timeframe": result.timeframe,
36
+ "purpose": result.request.purpose,
37
+ "requestedCandleCount": result.request.candle_count,
38
+ "candleCount": result.candle_count,
39
+ "fromCache": result.from_cache,
40
+ "refreshed": result.refreshed,
41
+ "candles": [candle_to_dict(candle) for candle in result.candles],
42
+ }
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ from aiohttp import web
4
+
5
+ from classified.market_data.data_requests import MarketDataRequest
6
+ from classified.market_data.manager import (
7
+ MarketDataManager,
8
+ MarketDataUnavailableError,
9
+ )
10
+
11
+ from .models import market_data_result_to_dict
12
+ from .stream import market_stream
13
+
14
+ SUPPORTED_TIMEFRAMES: frozenset[str] = frozenset(
15
+ {
16
+ "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h",
17
+ "6h", "8h", "12h", "1d", "3d", "1w", "1M",
18
+ }
19
+ )
20
+
21
+ SUPPORTED_PURPOSES: frozenset[str] = frozenset(
22
+ {
23
+ "higher_timeframe", "structure", "setup", "execution",
24
+ "indicator", "liquidity", "general",
25
+ }
26
+ )
27
+
28
+
29
+ def _error(message: str, status: int) -> web.Response:
30
+ return web.json_response(
31
+ {"error": {"message": message, "status": status}},
32
+ status=status,
33
+ )
34
+
35
+
36
+ def _positive_int(value: str | None, *, name: str, default: int) -> int:
37
+ if value is None or value == "":
38
+ return default
39
+ try:
40
+ parsed = int(value)
41
+ except ValueError as exc:
42
+ raise ValueError(f"{name} must be an integer.") from exc
43
+ if parsed < 1:
44
+ raise ValueError(f"{name} must be at least 1.")
45
+ return parsed
46
+
47
+
48
+ def register_routes(app: web.Application, manager: MarketDataManager) -> None:
49
+ app["market_data_manager"] = manager
50
+ app.router.add_get("/health", health)
51
+ app.router.add_get("/api/v1/market-data/candles", candles)
52
+ app.router.add_get("/api/v1/market-data/stream", market_stream)
53
+
54
+
55
+ async def health(request: web.Request) -> web.Response:
56
+ manager: MarketDataManager = request.app["market_data_manager"]
57
+ return web.json_response(
58
+ {
59
+ "status": "ok",
60
+ "service": "classified",
61
+ "exchange": manager.exchange_name,
62
+ "exchangeConnected": manager.is_connected(),
63
+ "liveMarketStream": True,
64
+ }
65
+ )
66
+
67
+
68
+ async def candles(request: web.Request) -> web.Response:
69
+ manager: MarketDataManager = request.app["market_data_manager"]
70
+ symbol = request.query.get("symbol", "").strip()
71
+ timeframe = request.query.get("timeframe", "").strip()
72
+ purpose = request.query.get("purpose", "general").strip()
73
+
74
+ if not symbol:
75
+ return _error("Query parameter 'symbol' is required.", 400)
76
+ if not timeframe:
77
+ return _error("Query parameter 'timeframe' is required.", 400)
78
+ if timeframe not in SUPPORTED_TIMEFRAMES:
79
+ return _error("Unsupported timeframe.", 400)
80
+ if purpose not in SUPPORTED_PURPOSES:
81
+ return _error("Unsupported purpose.", 400)
82
+
83
+ try:
84
+ candle_count = _positive_int(
85
+ request.query.get("candle_count"),
86
+ name="candle_count",
87
+ default=150,
88
+ )
89
+ except ValueError as exc:
90
+ return _error(str(exc), 400)
91
+
92
+ if candle_count > 1000:
93
+ return _error("candle_count cannot exceed 1000.", 400)
94
+
95
+ try:
96
+ market_request = MarketDataRequest(
97
+ symbol=symbol,
98
+ timeframe=timeframe, # type: ignore[arg-type]
99
+ candle_count=candle_count,
100
+ purpose=purpose, # type: ignore[arg-type]
101
+ )
102
+ result = manager.fulfill_request(market_request)
103
+ except MarketDataUnavailableError as exc:
104
+ return _error(str(exc), 503)
105
+ except ValueError as exc:
106
+ return _error(str(exc), 400)
107
+ except Exception as exc:
108
+ return _error(f"Unexpected market-data error: {exc}", 500)
109
+
110
+ return web.json_response({"data": market_data_result_to_dict(result)})