ccxt-gateway 1.8.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.
- ccxt_gateway-1.8.0/.gitignore +44 -0
- ccxt_gateway-1.8.0/LICENSE +21 -0
- ccxt_gateway-1.8.0/PKG-INFO +124 -0
- ccxt_gateway-1.8.0/README.md +90 -0
- ccxt_gateway-1.8.0/config/default.yaml +49 -0
- ccxt_gateway-1.8.0/pyproject.toml +84 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/__init__.py +8 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/api/__init__.py +1 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/api/admin.py +267 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/api/rest.py +246 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/api/websocket.py +209 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/config.py +119 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/core/__init__.py +1 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/core/process_manager.py +380 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/core/protocol.py +120 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/core/zmq_broker.py +188 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/daemon_gateway.py +154 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/exchange/__init__.py +1 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/exchange/hotfixes.py +79 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/exchange/methods.py +67 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/exchange/subprocess.py +593 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/main.py +215 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/models/__init__.py +1 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/utils/__init__.py +1 -0
- ccxt_gateway-1.8.0/src/ccxt_gateway/utils/updates.py +153 -0
- ccxt_gateway-1.8.0/tests/conftest.py +37 -0
- ccxt_gateway-1.8.0/tests/test_100.py +14 -0
- ccxt_gateway-1.8.0/tests/test_admin_api.py +269 -0
- ccxt_gateway-1.8.0/tests/test_api_fixed.py +113 -0
- ccxt_gateway-1.8.0/tests/test_comprehensive.py +405 -0
- ccxt_gateway-1.8.0/tests/test_coverage_100.py +395 -0
- ccxt_gateway-1.8.0/tests/test_easiest_gaps.py +131 -0
- ccxt_gateway-1.8.0/tests/test_easy_gaps.py +50 -0
- ccxt_gateway-1.8.0/tests/test_edge2.py +50 -0
- ccxt_gateway-1.8.0/tests/test_edge_cases.py +78 -0
- ccxt_gateway-1.8.0/tests/test_final_100.py +174 -0
- ccxt_gateway-1.8.0/tests/test_final_gaps.py +168 -0
- ccxt_gateway-1.8.0/tests/test_final_gaps2.py +11 -0
- ccxt_gateway-1.8.0/tests/test_integration_proper.py +63 -0
- ccxt_gateway-1.8.0/tests/test_loops2.py +95 -0
- ccxt_gateway-1.8.0/tests/test_main_and_pm.py +183 -0
- ccxt_gateway-1.8.0/tests/test_methods.py +109 -0
- ccxt_gateway-1.8.0/tests/test_more2.py +236 -0
- ccxt_gateway-1.8.0/tests/test_more_coverage.py +224 -0
- ccxt_gateway-1.8.0/tests/test_more_coverage2.py +59 -0
- ccxt_gateway-1.8.0/tests/test_more_gaps.py +120 -0
- ccxt_gateway-1.8.0/tests/test_pm_coverage.py +253 -0
- ccxt_gateway-1.8.0/tests/test_push1.py +27 -0
- ccxt_gateway-1.8.0/tests/test_push2.py +44 -0
- ccxt_gateway-1.8.0/tests/test_push3.py +34 -0
- ccxt_gateway-1.8.0/tests/test_push4.py +14 -0
- ccxt_gateway-1.8.0/tests/test_push5.py +38 -0
- ccxt_gateway-1.8.0/tests/test_push6.py +54 -0
- ccxt_gateway-1.8.0/tests/test_push7.py +73 -0
- ccxt_gateway-1.8.0/tests/test_push_higher.py +178 -0
- ccxt_gateway-1.8.0/tests/test_remaining.py +185 -0
- ccxt_gateway-1.8.0/tests/test_rest_api.py +306 -0
- ccxt_gateway-1.8.0/tests/test_subprocess_basic.py +310 -0
- ccxt_gateway-1.8.0/tests/test_updates_more.py +161 -0
- ccxt_gateway-1.8.0/tests/test_websocket.py +103 -0
- ccxt_gateway-1.8.0/tests/test_zmq_simple.py +148 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
env/
|
|
28
|
+
|
|
29
|
+
# IDEs
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
*~
|
|
35
|
+
|
|
36
|
+
# OS
|
|
37
|
+
.DS_Store
|
|
38
|
+
Thumbs.db
|
|
39
|
+
|
|
40
|
+
# Project specific
|
|
41
|
+
.env
|
|
42
|
+
logs/
|
|
43
|
+
*.log
|
|
44
|
+
.cache/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 ccxt-gateway contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ccxt-gateway
|
|
3
|
+
Version: 1.8.0
|
|
4
|
+
Summary: High-performance self-hosted gateway for 100+ crypto exchanges via CCXT
|
|
5
|
+
Author: ccxt-gateway contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: aiofiles>=24.1.0
|
|
10
|
+
Requires-Dist: ccxt[pro]>=4.4.0
|
|
11
|
+
Requires-Dist: fastapi>=0.115.0
|
|
12
|
+
Requires-Dist: httpx>=0.27.0
|
|
13
|
+
Requires-Dist: orjson>=3.10.0
|
|
14
|
+
Requires-Dist: packaging>=24.0
|
|
15
|
+
Requires-Dist: psutil>=6.0.0
|
|
16
|
+
Requires-Dist: pydantic-settings>=2.14.2
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
18
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
19
|
+
Requires-Dist: pyzmq>=26.0.0
|
|
20
|
+
Requires-Dist: setuptools>=83.0.0
|
|
21
|
+
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
22
|
+
Requires-Dist: uvloop>=0.20.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: httpx>=0.27.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: types-requests>=2.31.0; extra == 'dev'
|
|
31
|
+
Provides-Extra: server
|
|
32
|
+
Requires-Dist: granian>=1.4.0; extra == 'server'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# ccxt-gateway
|
|
36
|
+
|
|
37
|
+
High-performance, self-hosted gateway for 100+ cryptocurrency exchanges via CCXT.
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **High Performance**: Built with FastAPI, uvloop, and orjson for maximum throughput
|
|
42
|
+
- **Rock Solid Stability**: Multi-process design with automatic restart on failures
|
|
43
|
+
- **Memory Management**: Automatic subprocess restart when memory limits exceeded
|
|
44
|
+
- **WebSocket Support**: Proxy CCXT watch* functions via WebSocket connections
|
|
45
|
+
- **Auto-Update**: Automatic CCXT version management
|
|
46
|
+
- **Unified API**: Single REST and WebSocket API for all supported exchanges
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
### Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Clone the repository
|
|
54
|
+
git clone https://github.com/yourusername/ccxt-gateway.git
|
|
55
|
+
cd ccxt-gateway
|
|
56
|
+
|
|
57
|
+
# Install dependencies
|
|
58
|
+
pip install -e ".[server]"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Running
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Start the gateway
|
|
65
|
+
ccxt-gateway
|
|
66
|
+
|
|
67
|
+
# Or directly with Python
|
|
68
|
+
python -m ccxt_gateway.main
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The gateway will start on `http://0.0.0.0:8000` by default.
|
|
72
|
+
|
|
73
|
+
### Basic Usage
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Create a Binance exchange instance
|
|
77
|
+
curl -X POST "http://localhost:8000/exchanges/my-binance?exchange_name=binance"
|
|
78
|
+
|
|
79
|
+
# Get ticker for BTC/USDT
|
|
80
|
+
curl "http://localhost:8000/exchanges/my-binance/fetch_ticker?symbol=BTC/USDT"
|
|
81
|
+
|
|
82
|
+
# Delete the exchange instance
|
|
83
|
+
curl -X DELETE "http://localhost:8000/exchanges/my-binance"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
Configuration is loaded from (in order):
|
|
89
|
+
1. Environment variables (prefixed with `CCXT_GATEWAY_`)
|
|
90
|
+
2. `.env` file
|
|
91
|
+
3. `config/default.yaml`
|
|
92
|
+
|
|
93
|
+
See `config/default.yaml` for all available options.
|
|
94
|
+
|
|
95
|
+
## API Documentation
|
|
96
|
+
|
|
97
|
+
Once running, visit:
|
|
98
|
+
- Swagger UI: `http://localhost:8000/docs`
|
|
99
|
+
- ReDoc: `http://localhost:8000/redoc`
|
|
100
|
+
|
|
101
|
+
## Architecture
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
Client -> FastAPI -> ZMQ Broker -> Exchange Subprocess (CCXT)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Each exchange runs in its own subprocess, communicating via ZeroMQ for isolation and stability.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Install dev dependencies
|
|
113
|
+
pip install -e ".[dev]"
|
|
114
|
+
|
|
115
|
+
# Run tests
|
|
116
|
+
pytest
|
|
117
|
+
|
|
118
|
+
# Format code
|
|
119
|
+
black src/
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ccxt-gateway
|
|
2
|
+
|
|
3
|
+
High-performance, self-hosted gateway for 100+ cryptocurrency exchanges via CCXT.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **High Performance**: Built with FastAPI, uvloop, and orjson for maximum throughput
|
|
8
|
+
- **Rock Solid Stability**: Multi-process design with automatic restart on failures
|
|
9
|
+
- **Memory Management**: Automatic subprocess restart when memory limits exceeded
|
|
10
|
+
- **WebSocket Support**: Proxy CCXT watch* functions via WebSocket connections
|
|
11
|
+
- **Auto-Update**: Automatic CCXT version management
|
|
12
|
+
- **Unified API**: Single REST and WebSocket API for all supported exchanges
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Clone the repository
|
|
20
|
+
git clone https://github.com/yourusername/ccxt-gateway.git
|
|
21
|
+
cd ccxt-gateway
|
|
22
|
+
|
|
23
|
+
# Install dependencies
|
|
24
|
+
pip install -e ".[server]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Running
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Start the gateway
|
|
31
|
+
ccxt-gateway
|
|
32
|
+
|
|
33
|
+
# Or directly with Python
|
|
34
|
+
python -m ccxt_gateway.main
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The gateway will start on `http://0.0.0.0:8000` by default.
|
|
38
|
+
|
|
39
|
+
### Basic Usage
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Create a Binance exchange instance
|
|
43
|
+
curl -X POST "http://localhost:8000/exchanges/my-binance?exchange_name=binance"
|
|
44
|
+
|
|
45
|
+
# Get ticker for BTC/USDT
|
|
46
|
+
curl "http://localhost:8000/exchanges/my-binance/fetch_ticker?symbol=BTC/USDT"
|
|
47
|
+
|
|
48
|
+
# Delete the exchange instance
|
|
49
|
+
curl -X DELETE "http://localhost:8000/exchanges/my-binance"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
Configuration is loaded from (in order):
|
|
55
|
+
1. Environment variables (prefixed with `CCXT_GATEWAY_`)
|
|
56
|
+
2. `.env` file
|
|
57
|
+
3. `config/default.yaml`
|
|
58
|
+
|
|
59
|
+
See `config/default.yaml` for all available options.
|
|
60
|
+
|
|
61
|
+
## API Documentation
|
|
62
|
+
|
|
63
|
+
Once running, visit:
|
|
64
|
+
- Swagger UI: `http://localhost:8000/docs`
|
|
65
|
+
- ReDoc: `http://localhost:8000/redoc`
|
|
66
|
+
|
|
67
|
+
## Architecture
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Client -> FastAPI -> ZMQ Broker -> Exchange Subprocess (CCXT)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Each exchange runs in its own subprocess, communicating via ZeroMQ for isolation and stability.
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Install dev dependencies
|
|
79
|
+
pip install -e ".[dev]"
|
|
80
|
+
|
|
81
|
+
# Run tests
|
|
82
|
+
pytest
|
|
83
|
+
|
|
84
|
+
# Format code
|
|
85
|
+
black src/
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ccxt-gateway default configuration
|
|
2
|
+
|
|
3
|
+
server:
|
|
4
|
+
host: "0.0.0.0"
|
|
5
|
+
port: 8999
|
|
6
|
+
debug: false
|
|
7
|
+
use_ssl: false
|
|
8
|
+
ssl_cert: null
|
|
9
|
+
ssl_key: null
|
|
10
|
+
# Use granian for higher performance (pip install ccxt-gateway[server])
|
|
11
|
+
use_granian: false
|
|
12
|
+
|
|
13
|
+
zmq:
|
|
14
|
+
# Main broker address (for main process to communicate with subprocesses)
|
|
15
|
+
broker_address: "tcp://127.0.0.1:5557"
|
|
16
|
+
# Subprocess connect to this address
|
|
17
|
+
subprocess_connect: "tcp://127.0.0.1:5557"
|
|
18
|
+
# High water mark for ZMQ (prevent memory issues)
|
|
19
|
+
high_water_mark: 1000
|
|
20
|
+
# Timeout for request-reply (ms)
|
|
21
|
+
timeout_ms: 30000
|
|
22
|
+
|
|
23
|
+
process_manager:
|
|
24
|
+
# Maximum RSS per exchange subprocess (in MB)
|
|
25
|
+
max_rss_mb: 512
|
|
26
|
+
# Interval to check memory usage (seconds)
|
|
27
|
+
check_interval: 30
|
|
28
|
+
# Auto-restart subprocesses on memory limit or crash
|
|
29
|
+
auto_restart: true
|
|
30
|
+
# Maximum restarts per hour per exchange
|
|
31
|
+
max_restarts_per_hour: 5
|
|
32
|
+
# Subprocess startup timeout (seconds)
|
|
33
|
+
startup_timeout: 30
|
|
34
|
+
|
|
35
|
+
update:
|
|
36
|
+
# Check for ccxt updates interval (hours, 0 to disable)
|
|
37
|
+
check_interval_hours: 24
|
|
38
|
+
# Auto-update ccxt when new version available
|
|
39
|
+
auto_update: false
|
|
40
|
+
# PyPI package name for ccxt
|
|
41
|
+
pypi_package: "ccxt"
|
|
42
|
+
|
|
43
|
+
exchanges:
|
|
44
|
+
# Default settings for exchange instances
|
|
45
|
+
default_enable_rate_limit: true
|
|
46
|
+
# Timeout for exchange API calls (ms)
|
|
47
|
+
default_timeout: 30000
|
|
48
|
+
# Enable verbose logging per exchange
|
|
49
|
+
default_verbose: false
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ccxt-gateway"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "High-performance self-hosted gateway for 100+ crypto exchanges via CCXT"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "ccxt-gateway contributors"}
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastapi>=0.115.0",
|
|
13
|
+
"uvicorn[standard]>=0.30.0",
|
|
14
|
+
"pyzmq>=26.0.0",
|
|
15
|
+
"pydantic-settings>=2.14.2",
|
|
16
|
+
"psutil>=6.0.0",
|
|
17
|
+
"ccxt[pro]>=4.4.0",
|
|
18
|
+
"setuptools>=83.0.0",
|
|
19
|
+
"uvloop>=0.20.0",
|
|
20
|
+
"orjson>=3.10.0",
|
|
21
|
+
"aiofiles>=24.1.0",
|
|
22
|
+
"httpx>=0.27.0",
|
|
23
|
+
"python-dotenv>=1.0.0",
|
|
24
|
+
"pyyaml>=6.0.0",
|
|
25
|
+
"packaging>=24.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
server = [
|
|
30
|
+
"granian>=1.4.0",
|
|
31
|
+
]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8.0.0",
|
|
34
|
+
"pytest-asyncio>=0.23.0",
|
|
35
|
+
"pytest-cov>=5.0.0",
|
|
36
|
+
"mypy>=1.10.0",
|
|
37
|
+
"httpx>=0.27.0",
|
|
38
|
+
"types-pyyaml>=6.0.0",
|
|
39
|
+
"types-requests>=2.31.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.mypy]
|
|
43
|
+
python_version = "3.14"
|
|
44
|
+
warn_return_any = true
|
|
45
|
+
warn_unused_configs = true
|
|
46
|
+
disallow_untyped_defs = true
|
|
47
|
+
disallow_incomplete_defs = true
|
|
48
|
+
check_untyped_defs = true
|
|
49
|
+
ignore_missing_imports = true
|
|
50
|
+
strict = true
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
asyncio_mode = "strict"
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
|
|
56
|
+
[build-system]
|
|
57
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
58
|
+
build-backend = "hatchling.build"
|
|
59
|
+
|
|
60
|
+
[tool.hatch.version]
|
|
61
|
+
source = "vcs"
|
|
62
|
+
raw-options = { search_parent_directories = true }
|
|
63
|
+
|
|
64
|
+
[dependency-groups]
|
|
65
|
+
dev = [
|
|
66
|
+
"pytest>=9.0.3",
|
|
67
|
+
"pytest-asyncio>=1.3.0",
|
|
68
|
+
"pytest-cov>=7.1.0",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.hatch.build.targets.wheel]
|
|
72
|
+
packages = ["src/ccxt_gateway"]
|
|
73
|
+
|
|
74
|
+
[tool.hatch.build.targets.sdist]
|
|
75
|
+
include = [
|
|
76
|
+
"src/ccxt_gateway/**/*",
|
|
77
|
+
"config/**/*",
|
|
78
|
+
"scripts/**/*",
|
|
79
|
+
"tests/**/*",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
[project.scripts]
|
|
83
|
+
ccxt-gateway = "ccxt_gateway.main:main"
|
|
84
|
+
ccxt-gateway-daemon = "ccxt_gateway.daemon_gateway:main"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""ccxt-gateway: High-performance self-hosted gateway for 100+ crypto exchanges via CCXT."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version as _metadata_version
|
|
5
|
+
|
|
6
|
+
__version__ = _metadata_version("ccxt-gateway")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API package for ccxt-gateway."""
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Admin API endpoints for ccxt-gateway."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
8
|
+
|
|
9
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
router: APIRouter = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_process_manager(request: Request) -> Any:
|
|
15
|
+
"""Dependency to get process manager."""
|
|
16
|
+
process_manager = getattr(request.app.state, "process_manager", None)
|
|
17
|
+
|
|
18
|
+
if process_manager is None:
|
|
19
|
+
raise HTTPException(status_code=503, detail="Process manager not initialized")
|
|
20
|
+
return process_manager
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_broker(request: Request) -> Any:
|
|
24
|
+
"""Dependency to get ZMQ broker."""
|
|
25
|
+
broker = getattr(request.app.state, "broker", None)
|
|
26
|
+
|
|
27
|
+
if broker is None:
|
|
28
|
+
raise HTTPException(status_code=503, detail="ZMQ broker not initialized")
|
|
29
|
+
return broker
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get("/exchanges")
|
|
33
|
+
async def list_exchanges(
|
|
34
|
+
process_manager: Any = Depends(get_process_manager),
|
|
35
|
+
) -> List[Dict[str, Any]]:
|
|
36
|
+
"""List all exchange instances."""
|
|
37
|
+
result: List[Dict[str, Any]] = []
|
|
38
|
+
for exchange_id, proc in process_manager.processes.items():
|
|
39
|
+
proc.update_memory()
|
|
40
|
+
result.append(
|
|
41
|
+
{
|
|
42
|
+
"exchange_id": exchange_id,
|
|
43
|
+
"exchange_name": proc.exchange_name,
|
|
44
|
+
"pid": proc.pid,
|
|
45
|
+
"running": proc.is_running,
|
|
46
|
+
"rss_mb": round(proc.rss_mb, 2),
|
|
47
|
+
"restart_count": proc.restart_count,
|
|
48
|
+
"uptime_seconds": round(time.time() - proc.started_at, 2)
|
|
49
|
+
if proc.started_at
|
|
50
|
+
else None,
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.get("/exchanges/{exchange_id}")
|
|
57
|
+
async def get_exchange_details(
|
|
58
|
+
exchange_id: str,
|
|
59
|
+
process_manager: Any = Depends(get_process_manager),
|
|
60
|
+
) -> Dict[str, Any]:
|
|
61
|
+
"""Get details of a specific exchange instance."""
|
|
62
|
+
if exchange_id not in process_manager.processes:
|
|
63
|
+
raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
|
|
64
|
+
|
|
65
|
+
proc: Any = process_manager.processes[exchange_id]
|
|
66
|
+
proc.update_memory()
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"exchange_id": exchange_id,
|
|
70
|
+
"exchange_name": proc.exchange_name,
|
|
71
|
+
"pid": proc.pid,
|
|
72
|
+
"running": proc.is_running,
|
|
73
|
+
"rss_mb": round(proc.rss_mb, 2),
|
|
74
|
+
"restart_count": proc.restart_count,
|
|
75
|
+
"last_restart": proc.last_restart,
|
|
76
|
+
"uptime_seconds": round(time.time() - proc.started_at, 2)
|
|
77
|
+
if proc.started_at
|
|
78
|
+
else None,
|
|
79
|
+
"config": proc.config,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.post("/exchanges/{exchange_id}/restart")
|
|
84
|
+
async def restart_exchange(
|
|
85
|
+
exchange_id: str,
|
|
86
|
+
process_manager: Any = Depends(get_process_manager),
|
|
87
|
+
) -> Dict[str, Any]:
|
|
88
|
+
"""Manually restart an exchange instance."""
|
|
89
|
+
if exchange_id not in process_manager.processes:
|
|
90
|
+
raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
|
|
91
|
+
|
|
92
|
+
await process_manager._restart_exchange(exchange_id)
|
|
93
|
+
|
|
94
|
+
return {"status": "restart initiated", "exchange_id": exchange_id}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.get("/stats")
|
|
98
|
+
async def get_stats(
|
|
99
|
+
process_manager: Any = Depends(get_process_manager),
|
|
100
|
+
broker: Any = Depends(get_broker),
|
|
101
|
+
) -> Dict[str, Any]:
|
|
102
|
+
"""Get gateway statistics."""
|
|
103
|
+
total_exchanges: int = len(process_manager.processes)
|
|
104
|
+
running: int = sum(1 for p in process_manager.processes.values() if p.is_running)
|
|
105
|
+
total_memory: float = sum(p.rss_mb for p in process_manager.processes.values())
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"total_exchanges": total_exchanges,
|
|
109
|
+
"running_exchanges": running,
|
|
110
|
+
"stopped_exchanges": total_exchanges - running,
|
|
111
|
+
"total_memory_mb": round(total_memory, 2),
|
|
112
|
+
"pending_requests": len(broker.pending_requests)
|
|
113
|
+
if hasattr(broker, "pending_requests")
|
|
114
|
+
else 0,
|
|
115
|
+
"registered_exchanges": len(broker.exchange_identities)
|
|
116
|
+
if hasattr(broker, "exchange_identities")
|
|
117
|
+
else 0,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@router.get("/info")
|
|
122
|
+
async def get_info(request: Request) -> Dict[str, Any]:
|
|
123
|
+
"""Get gateway server info."""
|
|
124
|
+
import time
|
|
125
|
+
start_time: float = getattr(request.app.state, "start_time", time.time())
|
|
126
|
+
uptime: float = time.time() - start_time
|
|
127
|
+
return {
|
|
128
|
+
"result": {
|
|
129
|
+
"status": "running",
|
|
130
|
+
"version": "0.1.0",
|
|
131
|
+
"uptime_seconds": round(uptime, 2),
|
|
132
|
+
"python_version": __import__("sys").version,
|
|
133
|
+
},
|
|
134
|
+
"error": None,
|
|
135
|
+
"error_code": None,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@router.get("/memory")
|
|
140
|
+
async def get_memory(
|
|
141
|
+
process_manager: Any = Depends(get_process_manager),
|
|
142
|
+
) -> Dict[str, Any]:
|
|
143
|
+
"""Get memory usage."""
|
|
144
|
+
total_memory: float = sum(p.rss_mb for p in process_manager.processes.values())
|
|
145
|
+
return {
|
|
146
|
+
"result": {
|
|
147
|
+
"total_memory_mb": round(total_memory, 2),
|
|
148
|
+
"exchange_count": len(process_manager.processes),
|
|
149
|
+
},
|
|
150
|
+
"error": None,
|
|
151
|
+
"error_code": None,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@router.get("/exchange_names")
|
|
156
|
+
async def get_exchange_names() -> Dict[str, Any]:
|
|
157
|
+
"""Get all CCXT exchange names."""
|
|
158
|
+
try:
|
|
159
|
+
import ccxt
|
|
160
|
+
names = sorted(ccxt.exchanges)
|
|
161
|
+
return {
|
|
162
|
+
"result": names,
|
|
163
|
+
"error": None,
|
|
164
|
+
"error_code": None,
|
|
165
|
+
}
|
|
166
|
+
except Exception as e:
|
|
167
|
+
return {
|
|
168
|
+
"result": None,
|
|
169
|
+
"error": str(e),
|
|
170
|
+
"error_code": "EXCHANGE_NAMES_FAILED",
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@router.post("/update/ccxt")
|
|
175
|
+
async def trigger_ccxt_update(
|
|
176
|
+
broker: Any = Depends(get_broker),
|
|
177
|
+
) -> Dict[str, Any]:
|
|
178
|
+
"""Manually trigger CCXT update."""
|
|
179
|
+
from ccxt_gateway.utils.updates import update_ccxt, check_update
|
|
180
|
+
|
|
181
|
+
update_available: bool
|
|
182
|
+
current: Optional[str]
|
|
183
|
+
latest: Optional[str]
|
|
184
|
+
update_available, current, latest = await check_update()
|
|
185
|
+
|
|
186
|
+
if not update_available:
|
|
187
|
+
return {
|
|
188
|
+
"status": "no update available",
|
|
189
|
+
"current_version": current,
|
|
190
|
+
"latest_version": latest,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
success: bool
|
|
194
|
+
message: str
|
|
195
|
+
success, message = await update_ccxt()
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"status": "success" if success else "failed",
|
|
199
|
+
"message": message,
|
|
200
|
+
"previous_version": current,
|
|
201
|
+
"new_version": latest,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@router.get("/update/check")
|
|
206
|
+
async def check_ccxt_update(
|
|
207
|
+
broker: Any = Depends(get_broker),
|
|
208
|
+
) -> Dict[str, Any]:
|
|
209
|
+
"""Check if CCXT update is available."""
|
|
210
|
+
from ccxt_gateway.utils.updates import check_update
|
|
211
|
+
|
|
212
|
+
update_available: bool
|
|
213
|
+
current: Optional[str]
|
|
214
|
+
latest: Optional[str]
|
|
215
|
+
update_available, current, latest = await check_update()
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"update_available": update_available,
|
|
219
|
+
"current_version": current,
|
|
220
|
+
"latest_version": latest,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@router.get("/errors")
|
|
225
|
+
async def list_errors(
|
|
226
|
+
broker: Any = Depends(get_broker),
|
|
227
|
+
) -> Dict[str, Any]:
|
|
228
|
+
"""List all ccxt error names."""
|
|
229
|
+
try:
|
|
230
|
+
import ccxt
|
|
231
|
+
from ccxt.base.errors import ExchangeError, NetworkError, AuthenticationError, PermissionError, RequestTimeout, ExchangeNotAvailable, NotSupported, OperationFailed, BaseError
|
|
232
|
+
|
|
233
|
+
error_names = [
|
|
234
|
+
"BaseError",
|
|
235
|
+
"ExchangeError",
|
|
236
|
+
"NetworkError",
|
|
237
|
+
"AuthenticationError",
|
|
238
|
+
"PermissionError",
|
|
239
|
+
"RequestTimeout",
|
|
240
|
+
"ExchangeNotAvailable",
|
|
241
|
+
"NotSupported",
|
|
242
|
+
"OperationFailed",
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"result": error_names,
|
|
247
|
+
"error": None,
|
|
248
|
+
"error_code": None,
|
|
249
|
+
}
|
|
250
|
+
except Exception as e:
|
|
251
|
+
return {
|
|
252
|
+
"result": None,
|
|
253
|
+
"error": str(e),
|
|
254
|
+
"error_code": "ERROR_LIST_FAILED",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@router.post("/shutdown")
|
|
259
|
+
async def shutdown_server(request: Request) -> Dict[str, str]:
|
|
260
|
+
"""Gracefully shut down the gateway: closes all exchange subprocesses, then exits."""
|
|
261
|
+
logger.info("Shutdown requested via /admin/shutdown")
|
|
262
|
+
import asyncio, sys
|
|
263
|
+
async def _delayed_shutdown():
|
|
264
|
+
await asyncio.sleep(0.2)
|
|
265
|
+
sys.exit(0)
|
|
266
|
+
asyncio.create_task(_delayed_shutdown())
|
|
267
|
+
return {"status": "shutting_down"}
|