Kaynağa Gözat

initial commit - server scaffold

Lukas Goldschmidt 1 ay önce
işleme
7dbaa98994
14 değiştirilmiş dosya ile 642 ekleme ve 0 silme
  1. 22 0
      .gitignore
  2. 16 0
      PROJECT.md
  3. 14 0
      README.md
  4. 399 0
      Strategy_concepts_1.md
  5. 46 0
      killserver.sh
  6. 5 0
      requirements.txt
  7. 7 0
      restart.sh
  8. 15 0
      run.sh
  9. 1 0
      src/trader_mcp/__init__.py
  10. 29 0
      src/trader_mcp/dashboard.py
  11. 39 0
      src/trader_mcp/server.py
  12. 16 0
      tests.sh
  13. 1 0
      tests/__init__.py
  14. 32 0
      tests/test_smoke.py

+ 22 - 0
.gitignore

@@ -0,0 +1,22 @@
+# Python
+__pycache__/
+*.py[cod]
+*.pyo
+*.pyd
+*.egg-info/
+
+# Virtualenv
+.venv/
+venv/
+
+# FastAPI/uvicorn
+*.log
+logs/
+
+# Build
+build/
+dist/
+
+# SQLite
+*.db
+*.sqlite

+ 16 - 0
PROJECT.md

@@ -0,0 +1,16 @@
+# Trader MCP - Project
+
+## Purpose
+A minimal MCP server scaffold for trading helpers. Start with a small public surface and keep app logic isolated.
+
+## Architecture
+- FastAPI app
+- FastMCP mounted at `/mcp` using SSE at `/mcp/sse`
+- App-specific logic under `src/<app_name>/*`
+- State persistence via SQLite (add only as needed)
+- Logs and pid files under `./logs/`
+
+## Routes
+- `GET /` minimal landing page
+- `GET /health` liveness
+- `GET /mcp/sse` MCP SSE endpoint

+ 14 - 0
README.md

@@ -0,0 +1,14 @@
+# Trader MCP
+
+MCP server for trading-related helper functions (read-only by default where possible).
+
+## Endpoints
+- `GET /` - landing page
+- `GET /health` - lightweight health check
+- `GET /mcp/sse` - MCP SSE transport endpoint
+
+## MCP
+Implements a small, read-oriented MCP surface under `/mcp`.
+
+## Development
+See `run.sh` / `tests.sh` in this folder.

+ 399 - 0
Strategy_concepts_1.md

@@ -0,0 +1,399 @@
+# Strategy Engine Architecture – Concept Paper
+
+## 1. Overview
+
+This document defines a modular architecture for a strategy execution system consisting of:
+
+* A **host engine**
+* Pluggable **strategy modules**
+* Configurable **strategy instances**
+* A **dashboard UI**
+
+The system separates **definition**, **configuration**, **execution**, and **visualization** into clearly distinct layers.
+
+---
+
+## 2. Core Concepts
+
+### 2.1 Strategy Definition
+
+A strategy is a Python module that defines:
+
+* Logic (`on_tick`)
+* State initialization (`init`)
+* Runtime visualization (`render`)
+* Configuration schema (`CONFIG_SCHEMA`)
+
+Characteristics:
+
+* Stateless at definition level
+* Reusable across multiple instances
+* Stored as a single `.py` file (optionally + config)
+
+---
+
+### 2.2 Strategy Instance
+
+A strategy instance represents a **configured and uniquely identifiable deployment** of a strategy.
+
+#### Immutable Identity
+
+Defined at creation and never changed:
+
+* `id` (unique internal identifier)
+* `strategy_type`
+* `account`
+* `market` (e.g. BTC/USDT)
+
+Purpose:
+
+* Stable logging
+* Auditability
+* Reproducibility
+
+---
+
+#### Mutable Configuration
+
+User-editable and persisted:
+
+```json
+{
+  "risk": 0.01,
+  "window": 20,
+  "note": "test run"
+}
+```
+
+Characteristics:
+
+* Editable via UI
+* Stored in database
+* Survives restarts
+
+---
+
+#### Runtime State
+
+* Managed internally by the strategy
+* Not user-editable
+* Typically not persisted (initially)
+
+Example:
+
+```json
+{
+  "prices": [],
+  "position": 0
+}
+```
+
+---
+
+## 3. System Layers
+
+```text
+Strategy Definition → Strategy Instance → Runtime Execution → UI Rendering
+```
+
+---
+
+## 4. Modes of Operation
+
+Each instance has exactly one mode:
+
+```text
+mode ∈ { off, observe, active }
+```
+
+### Modes
+
+| Mode    | Loaded | Receives Ticks | Can Trade |
+| ------- | ------ | -------------- | --------- |
+| off     | ❌      | ❌              | ❌         |
+| observe | ✅      | ✅              | ❌         |
+| active  | ✅      | ✅              | ✅         |
+
+---
+
+### Design Principle
+
+* Strategies always execute the same logic
+* Capabilities are restricted externally via context
+* No internal branching based on mode
+
+---
+
+## 5. Persistence vs Runtime
+
+### Persistent Layer (Database)
+
+Source of truth:
+
+```json
+{
+  "id": "...",
+  "strategy_type": "...",
+  "account": "...",
+  "market": "...",
+  "mode": "observe",
+  "config": {...}
+}
+```
+
+---
+
+### Runtime Layer (Engine Memory)
+
+```python
+running_instances = {
+    "id": StrategyInstance(...)
+}
+```
+
+Derived from persistent state.
+
+---
+
+## 6. Reconciliation Model
+
+The engine continuously aligns runtime with the database.
+
+### Pseudocode
+
+```python
+def reconcile():
+    for record in db.instances:
+
+        if record.mode != "off" and record.id not in running:
+            load_instance(record)
+
+        if record.mode == "off" and record.id in running:
+            unload_instance(record.id)
+```
+
+---
+
+### Key Insight
+
+> The system is **declarative**:
+> The database defines the desired state, the engine enforces it.
+
+---
+
+## 7. Strategy SDK Contract
+
+### Base Structure
+
+```python
+class Strategy:
+    CONFIG_SCHEMA = {}
+
+    def __init__(self, context, config):
+        self.context = context
+        self.config = config
+        self.state = self.init()
+
+    def init(self):
+        return {}
+
+    def on_tick(self, tick):
+        pass
+
+    def render(self):
+        return {"widgets": []}
+```
+
+---
+
+### Context Interface
+
+Strategies interact only through context:
+
+```python
+context.get_price()
+context.place_order(...)
+context.get_orders()
+```
+
+---
+
+### Mode Enforcement
+
+```python
+def place_order(...):
+    if mode != "active":
+        raise Exception("Not allowed")
+```
+
+---
+
+## 8. UI Architecture
+
+### 8.1 Config UI
+
+Defined via `CONFIG_SCHEMA`
+
+Purpose:
+
+* Generate forms
+* Validate input
+
+Example:
+
+```json
+{
+  "risk": {
+    "type": "float",
+    "default": 0.01
+  }
+}
+```
+
+---
+
+### 8.2 Runtime UI
+
+Generated dynamically:
+
+```python
+def render(state):
+    return {
+        "widgets": [
+            {"type": "line_chart", "data": [...]},
+            {"type": "metric", "label": "PnL", "value": 123}
+        ]
+    }
+```
+
+---
+
+### Principle
+
+```text
+Config UI = user input
+Runtime UI = system output
+```
+
+---
+
+## 9. Dashboard Responsibilities
+
+### Strategy List View
+
+Displays:
+
+* Instance ID
+* Strategy type
+* Market
+* Account
+* Mode
+* Status
+
+---
+
+### Detail View
+
+#### Sections:
+
+1. **Identity (immutable)**
+2. **Config (editable)**
+3. **Runtime UI (widgets)**
+4. **Controls (mode switching)**
+
+---
+
+## 10. Execution Model
+
+### Tick Loop
+
+```python
+for instance in running_instances:
+    instance.on_tick(tick)
+```
+
+---
+
+### Rendering
+
+```python
+def get_ui(instance_id):
+    return instance.render()
+```
+
+---
+
+### Optimization
+
+* Rendering is **on-demand only**
+* No UI computation when not requested
+
+---
+
+## 11. Design Principles
+
+### 1. Separation of Concerns
+
+* Strategy logic vs system control
+* Config vs runtime state
+* UI input vs UI output
+
+---
+
+### 2. Declarative Control
+
+* No direct lifecycle manipulation
+* DB defines desired state
+
+---
+
+### 3. Minimal Control Surface
+
+* Single `mode` field
+* Avoid multiple flags
+
+---
+
+### 4. Capability-Based Execution
+
+* Strategies always run logic
+* Context restricts actions
+
+---
+
+### 5. Instance-Centric Design
+
+* Everything revolves around instances
+* Not strategies as singletons
+
+---
+
+## 12. Future Extensions
+
+* Backtesting (reuse instance config)
+* Simulation mode (observe + virtual trading)
+* Distributed execution
+* Strategy versioning
+* State persistence and replay
+* Advanced UI widgets (logs, trades, signals)
+
+---
+
+## 13. Summary
+
+The system consists of:
+
+* **Strategies**: reusable logic modules
+* **Instances**: configured, persistent deployments
+* **Engine**: reconciles desired vs actual state
+* **UI**: renders config and runtime data separately
+
+---
+
+### Core Insight
+
+```text
+Let strategies always think — but only sometimes act.
+```
+
+---

+ 46 - 0
killserver.sh

@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+PORT="${1:-8570}"
+
+echo "===================================================="
+echo "🧹 trader-mcp killserver.sh (port $PORT)"
+echo "===================================================="
+
+do_kill_pids() {
+  local pids="$1"
+  if [ -z "$pids" ]; then
+    return 0
+  fi
+  echo "Killing PIDs:" "$pids"
+  # shellcheck disable=SC2086
+  kill -9 $pids >/dev/null 2>&1 || true
+}
+
+# 1) lsof (precise)
+if command -v lsof >/dev/null 2>&1; then
+  PIDS=$(lsof -t -i tcp:"$PORT" 2>/dev/null || true)
+  if [ -n "$PIDS" ]; then
+    echo "Found listeners via lsof."
+    do_kill_pids "$PIDS"
+  else
+    echo "No listeners found via lsof."
+  fi
+else
+  echo "lsof not available."
+fi
+
+# 2) fuser (fallback)
+if command -v fuser >/dev/null 2>&1; then
+  echo "Checking via fuser..."
+  if fuser -n tcp "$PORT" >/dev/null 2>&1; then
+    echo "Listener(s) still present. Killing via fuser."
+    fuser -k -n tcp "$PORT" >/dev/null 2>&1 || true
+  else
+    echo "No listeners remaining via fuser."
+  fi
+else
+  echo "fuser not available."
+fi
+
+echo "Done."

+ 5 - 0
requirements.txt

@@ -0,0 +1,5 @@
+fastapi
+uvicorn[standard]
+fastmcp
+pytest
+httpx

+ 7 - 0
restart.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+PORT="${1:-8570}"
+
+./killserver.sh "$PORT"
+./run.sh "$PORT"

+ 15 - 0
run.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+PORT="${1:-8570}"
+
+killserver.sh "$PORT" >/dev/null 2>&1 || true
+
+export PYTHONPATH="${PYTHONPATH:-}:$(pwd)"
+
+# Start the server detached so `run.sh` can return immediately.
+mkdir -p ./logs
+uvicorn src.trader_mcp.server:app --host 0.0.0.0 --port "$PORT" > ./logs/server.log 2>&1 &
+PID=$!
+echo "$PID" > ./logs/server.pid
+echo "Trader MCP running on port $PORT (pid $PID)"

+ 1 - 0
src/trader_mcp/__init__.py

@@ -0,0 +1 @@
+# trader_mcp package

+ 29 - 0
src/trader_mcp/dashboard.py

@@ -0,0 +1,29 @@
+from fastapi import APIRouter
+from fastapi.responses import HTMLResponse
+
+router = APIRouter(prefix="/dashboard", tags=["dashboard"])
+
+
+@router.get("/", response_class=HTMLResponse)
+def dashboard_home():
+    return """<!doctype html>
+<html>
+  <head>
+    <meta charset=\"utf-8\" />
+    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
+    <title>Trader MCP Dashboard</title>
+    <style>
+      body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial; margin: 2rem; }
+      .card { max-width: 720px; padding: 1.25rem; border: 1px solid #e5e7eb; border-radius: 12px; }
+      code { background: #f3f4f6; padding: 0.15rem 0.35rem; border-radius: 8px; }
+      .muted { color: #6b7280; }
+    </style>
+  </head>
+  <body>
+    <div class=\"card\">
+      <h1>Trader MCP Dashboard</h1>
+      <p class=\"muted\">Scaffold page. Connect your trading widgets to MCP routes when you’re ready.</p>
+      <p>Current: <code>GET /dashboard/</code></p>
+    </div>
+  </body>
+</html>"""

+ 39 - 0
src/trader_mcp/server.py

@@ -0,0 +1,39 @@
+from fastapi import FastAPI
+
+from .dashboard import router as dashboard_router
+
+try:
+    from fastmcp import FastMCP
+except ImportError:  # pragma: no cover
+    FastMCP = None
+
+app = FastAPI(title="Trader MCP")
+app.include_router(dashboard_router)
+
+
+@app.get("/")
+def landing():
+    return {"name": "trader-mcp", "status": "ok"}
+
+
+@app.get("/health")
+def health():
+    return {"status": "ok"}
+
+
+# MCP (SSE)
+# FastMCP mounted at /mcp with SSE at /mcp/sse (when FastMCP is available)
+if FastMCP is not None:
+    mcp = FastMCP()
+
+    # Minimal public surface for now; expand once requirements are clear.
+    # Keep it read-oriented by default.
+
+    # FastMCP exposes an ASGI app via `http_app` (older/newer versions may differ).
+    mcp_asgi = getattr(mcp, "http_app", None) or getattr(mcp, "app", None) or getattr(mcp, "asgi_app", None)
+    if mcp_asgi is None:
+        raise AttributeError("FastMCP ASGI app attribute not found (expected http_app/app/asgi_app).")
+    app.mount("/mcp", mcp_asgi)
+
+    # SSE endpoint is expected at /mcp/sse by the FastMCP integration.
+

+ 16 - 0
tests.sh

@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Pytest-only smoke tests for trader-mcp.
+# Tests use FastAPI TestClient, so the server does not need to be running.
+
+# Ensure local package import works.
+export PYTHONPATH="${PYTHONPATH:-}:$(pwd)"
+
+if [ -f .venv/bin/activate ]; then
+  # shellcheck disable=SC1091
+  source .venv/bin/activate
+fi
+
+echo "Running pytest suite (trader-mcp)..."
+pytest -q

+ 1 - 0
tests/__init__.py

@@ -0,0 +1 @@
+# tests package

+ 32 - 0
tests/test_smoke.py

@@ -0,0 +1,32 @@
+import pytest
+
+from src.trader_mcp.server import app
+
+# Ensure src is a valid package import target in all runners.
+
+
+@pytest.fixture
+def client():
+    try:
+        from fastapi.testclient import TestClient
+    except Exception as e:  # pragma: no cover
+        pytest.skip(f"TestClient not available: {e}")
+    return TestClient(app)
+
+
+def test_root(client):
+    r = client.get("/")
+    assert r.status_code == 200
+    assert r.json().get("status") == "ok"
+
+
+def test_health(client):
+    r = client.get("/health")
+    assert r.status_code == 200
+    assert r.json().get("status") == "ok"
+
+
+def test_dashboard(client):
+    r = client.get("/dashboard/")
+    assert r.status_code == 200
+    assert "Trader MCP Dashboard" in r.text