mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-07-28 02:47:47 +00:00
Add Railway deployment config for 24/7 cloud paper trading
- Dockerfile: add templates copy, ENV PORT, update CMD to live engine - Create src/live/health.py: threaded HTTP health server (/health, /state) - Wire health server into src/live/run.py before engine loop - Dashboard: add unauthenticated /health endpoint, use PORT env var - Create railway.toml with Dockerfile builder and health check config - docker-compose.yml: rename service to live-engine, add PORT env vars Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -12,8 +12,10 @@ RUN pip install --no-cache-dir -r requirements.docker.txt
|
||||
COPY config/system.yaml config/system.yaml
|
||||
COPY src/ src/
|
||||
COPY sql/ sql/
|
||||
COPY templates/ templates/
|
||||
|
||||
RUN mkdir -p logs models
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
CMD ["python", "src/order_executor.py"]
|
||||
ENV PORT=8080
|
||||
CMD ["python", "src/live/run.py"]
|
||||
|
||||
+8
-2
@@ -1,13 +1,17 @@
|
||||
services:
|
||||
fx-quant:
|
||||
live-engine:
|
||||
build: .
|
||||
container_name: fx-quant
|
||||
container_name: fx-quant-engine
|
||||
env_file:
|
||||
- config/.env
|
||||
environment:
|
||||
- PORT=8080
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- ./logs:/app/logs
|
||||
- ./models:/app/models
|
||||
ports:
|
||||
- "8080:8080"
|
||||
restart: unless-stopped
|
||||
|
||||
dashboard:
|
||||
@@ -16,6 +20,8 @@ services:
|
||||
command: python src/dashboard.py
|
||||
env_file:
|
||||
- config/.env
|
||||
environment:
|
||||
- PORT=5000
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- ./logs:/app/logs:ro
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[build]
|
||||
builder = "dockerfile"
|
||||
dockerfilePath = "Dockerfile"
|
||||
|
||||
[deploy]
|
||||
healthcheckPath = "/health"
|
||||
healthcheckTimeout = 30
|
||||
restartPolicyType = "on_failure"
|
||||
restartPolicyMaxRetries = 5
|
||||
+6
-1
@@ -161,6 +161,11 @@ def validate_config(cfg):
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes
|
||||
# ---------------------------------------------------------------------------
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@requires_auth
|
||||
def index():
|
||||
@@ -589,4 +594,4 @@ def _parse_int_list(s):
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=False)
|
||||
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=False)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Lightweight threaded HTTP health server for Railway liveness checks.
|
||||
|
||||
Runs as a daemon thread so it never blocks the main trading engine loop.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
_start_time = time.time()
|
||||
|
||||
STATE_FILE = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||
"logs", "live_state.json",
|
||||
)
|
||||
|
||||
|
||||
class _HealthHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == "/health":
|
||||
body = json.dumps({
|
||||
"status": "ok",
|
||||
"uptime_seconds": round(time.time() - _start_time, 1),
|
||||
})
|
||||
self._respond(200, body)
|
||||
elif self.path == "/state":
|
||||
try:
|
||||
with open(STATE_FILE) as f:
|
||||
body = f.read()
|
||||
except FileNotFoundError:
|
||||
body = json.dumps({"error": "state file not found"})
|
||||
self._respond(200, body)
|
||||
else:
|
||||
self._respond(404, json.dumps({"error": "not found"}))
|
||||
|
||||
def _respond(self, code: int, body: str):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(body.encode())
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass # suppress request logs to keep engine output clean
|
||||
|
||||
|
||||
def start_health_server():
|
||||
port = int(os.environ.get("PORT", 8080))
|
||||
server = HTTPServer(("0.0.0.0", port), _HealthHandler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
print(f"Health server listening on :{port}")
|
||||
@@ -89,6 +89,9 @@ def build_slots(config: dict) -> list[StrategySlot]:
|
||||
|
||||
|
||||
def main():
|
||||
from src.live.health import start_health_server
|
||||
start_health_server()
|
||||
|
||||
config = load_config()
|
||||
if not config:
|
||||
print("ERROR: No phase2 config found in config/system.yaml")
|
||||
|
||||
Reference in New Issue
Block a user