Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions MAINNET_VPS_RUNBOOK_ES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Darwin v4 — Guía clara para correr 24/7 en VPS (Binance Mainnet)

## 0) Requisitos mínimos de VPS

Recomendado para correr Darwin 24/7 de forma estable:

- CPU: 2 vCPU (mínimo), 4 vCPU recomendado.
- RAM: 4 GB mínimo, 8 GB recomendado.
- Disco: 30 GB SSD mínimo.
- SO: Ubuntu 22.04 LTS o 24.04 LTS.
- Red: latencia estable y salida HTTPS sin bloqueos a APIs Binance.
- Hora del sistema: NTP activo (muy importante para evitar errores de timestamp).
- Seguridad:
- firewall activo (UFW)
- acceso SSH por llave
- fail2ban recomendado

## 1) Antes de poner API keys (checklist obligatorio)

- [ ] Ejecutar en **testnet** mínimo 48h sin errores críticos.
- [ ] Confirmar que `mode=live` solo se activará cuando todo esté estable.
- [ ] Crear API key de Binance Futures con:
- **Enable Futures**
- **NO** habilitar retiro (withdrawals).
- Restringir por IP al VPS.
- [ ] Definir límites de riesgo realistas en `config.yaml`:
- `max_total_exposure_pct`
- `halted_drawdown_pct`
- `max_consecutive_losses`

## 2) Preparar VPS

```bash
sudo apt update && sudo apt install -y python3.12 python3.12-venv git
mkdir -p /opt/darwin && cd /opt/darwin
git clone <tu-repo> darwin-v4
cd darwin-v4
python3.12 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
```

## 3) Variables de entorno seguras

Crear `/opt/darwin/darwin-v4/.env`:

```bash
DARWIN_MODE=live
BINANCE_API_KEY=TU_API_KEY
BINANCE_API_SECRET=TU_API_SECRET
LOG_LEVEL=INFO
```

> Nunca subas `.env` al repo.

## 4) Config mínima recomendada para Binance mainnet

Archivo `config.yaml` (ejemplo base):

```yaml
mode: live
capital:
starting_capital: 100.0
risk:
max_total_exposure_pct: 40.0
defensive_drawdown_pct: 6.0
critical_drawdown_pct: 10.0
halted_drawdown_pct: 14.0
max_consecutive_losses: 4
infra:
tick_interval: 30
log_level: INFO
exchanges:
- exchange_id: binance
enabled: true
testnet: false
leverage: 10
symbols: ["BTCUSDT", "ETHUSDT"]
```

## 5) Preflight antes de dejarlo solo

```bash
cd /opt/darwin/darwin-v4
source .venv/bin/activate
pytest -q test_binance_adapter_safety.py test_router_set_leverage.py test_main_runtime_compat.py
python -m compileall -q darwin_agent
python -m darwin_agent.main --config config.yaml --mode live --tick 30
```

Observa 15-30 minutos logs antes de dejarlo 24/7.

## 6) Ejecutar 24/7 con systemd

Crear `/etc/systemd/system/darwin.service`:

```ini
[Unit]
Description=Darwin Agent v4
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/darwin/darwin-v4
EnvironmentFile=/opt/darwin/darwin-v4/.env
ExecStart=/opt/darwin/darwin-v4/.venv/bin/python -m darwin_agent.main --config /opt/darwin/darwin-v4/config.yaml
Restart=always
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target
```

Activar:

```bash
sudo systemctl daemon-reload
sudo systemctl enable darwin
sudo systemctl start darwin
sudo systemctl status darwin --no-pager
```

Logs en vivo:

```bash
journalctl -u darwin -f
```

## 7) Operación diaria (runbook corto)

- Revisar cada mañana:
- PnL
- drawdown
- número de errores de exchange en logs
- Si hay errores repetidos (`timestamp`, `recvWindow`, `insufficient margin`):
1. parar servicio
2. revisar reloj/NTP, margen y límites
3. reiniciar con tamaño de riesgo menor

## 8) Kill switch manual

Paro inmediato:

```bash
sudo systemctl stop darwin
```

Si quieres hard stop por riesgo, baja temporalmente:
- `max_total_exposure_pct` a 0-5
- o cambiar `mode: test` mientras investigas.

## 9) Actualizar sin downtime largo

```bash
cd /opt/darwin/darwin-v4
sudo systemctl stop darwin
git pull
source .venv/bin/activate
pip install -r requirements.txt
pytest -q test_binance_adapter_safety.py test_router_set_leverage.py test_main_runtime_compat.py
sudo systemctl start darwin
```

---

Si sigues esta guía, el siguiente paso de meter API keys queda controlado y con salvaguardas operativas.
70 changes: 61 additions & 9 deletions darwin_agent/exchanges/binance.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import hmac
import logging
import time
from datetime import datetime, timezone
from typing import Dict, List
from datetime import datetime, timezone, timedelta
from typing import Dict, List, Set
from urllib.parse import urlencode

from darwin_agent.interfaces.enums import (
Expand All @@ -31,6 +31,11 @@
}


def _normalize_symbol(symbol: str) -> str:
# Accept formats like BTC/USDT, btcusdt, BTC-USDT
return symbol.replace("/", "").replace("-", "").replace("_", "").upper()


class BinanceAdapter:
"""
Production Binance USDⓈ-M Futures adapter.
Expand All @@ -40,7 +45,7 @@ class BinanceAdapter:
- Hedge mode awareness
"""

__slots__ = ("_api_key", "_api_secret", "_base_url", "_session", "_testnet")
__slots__ = ("_api_key", "_api_secret", "_base_url", "_session", "_testnet", "_symbols_cache", "_symbols_cache_at")

def __init__(self, api_key: str, api_secret: str, testnet: bool = False) -> None:
self._api_key = api_key
Expand All @@ -51,6 +56,8 @@ def __init__(self, api_key: str, api_secret: str, testnet: bool = False) -> None
else "https://fapi.binance.com"
)
self._session = None
self._symbols_cache: Set[str] = set()
self._symbols_cache_at: datetime | None = None

@property
def exchange_id(self) -> ExchangeID:
Expand Down Expand Up @@ -102,11 +109,45 @@ async def _signed(self, method: str, path: str, params: Dict = None) -> Dict:
else:
raise

async def get_exchange_symbols(self, force_refresh: bool = False) -> Set[str]:
"""Fetch Binance futures tradable symbols and cache briefly."""
now = datetime.now(timezone.utc)
cache_ttl = timedelta(minutes=5)
if (
not force_refresh
and self._symbols_cache
and self._symbols_cache_at is not None
and (now - self._symbols_cache_at) <= cache_ttl
):
return set(self._symbols_cache)

data = await self._public("/fapi/v1/exchangeInfo")
symbols: Set[str] = set()
for item in data.get("symbols", []):
if item.get("status") != "TRADING":
continue
name = item.get("symbol")
if name:
symbols.add(_normalize_symbol(name))
self._symbols_cache = symbols
self._symbols_cache_at = now
return set(symbols)

async def validate_symbols(self, symbols: List[str]) -> List[str]:
"""Return list of symbols not present/tradable on Binance futures."""
available = await self.get_exchange_symbols()
missing = []
for raw in symbols:
sym = _normalize_symbol(raw)
if sym not in available:
missing.append(raw)
return missing

# ── IExchangeAdapter ─────────────────────────────────────

async def get_candles(self, symbol: str, timeframe: TimeFrame, limit: int = 100) -> List[Candle]:
data = await self._public("/fapi/v1/klines", {
"symbol": symbol,
"symbol": _normalize_symbol(symbol),
"interval": _TF_MAP.get(timeframe, "15m"),
"limit": limit,
})
Expand All @@ -121,6 +162,7 @@ async def get_candles(self, symbol: str, timeframe: TimeFrame, limit: int = 100)
]

async def get_ticker(self, symbol: str) -> Ticker:
symbol = _normalize_symbol(symbol)
data = await self._public("/fapi/v1/ticker/bookTicker", {"symbol": symbol})
price_data = await self._public("/fapi/v1/ticker/price", {"symbol": symbol})
return Ticker(
Expand Down Expand Up @@ -154,14 +196,16 @@ async def get_positions(self) -> List[Position]:

async def place_order(self, request: OrderRequest) -> OrderResult:
params = {
"symbol": request.symbol,
"symbol": _normalize_symbol(request.symbol),
"side": "BUY" if request.side == OrderSide.BUY else "SELL",
"type": "MARKET" if request.order_type == OrderType.MARKET else "LIMIT",
"quantity": str(request.quantity),
}
if request.price and request.order_type == OrderType.LIMIT:
params["price"] = str(request.price)
params["timeInForce"] = "GTC"
if request.reduce_only:
params["reduceOnly"] = "true"
data = await self._signed("POST", "/fapi/v1/order", params)
if "orderId" not in data:
return OrderResult(
Expand All @@ -177,20 +221,26 @@ async def place_order(self, request: OrderRequest) -> OrderResult:
)

async def close_position(self, symbol: str, side: OrderSide) -> OrderResult:
symbol = _normalize_symbol(symbol)
positions = await self.get_positions()
pos = next((p for p in positions if p.symbol == symbol), None)
pos = next((p for p in positions if p.symbol == symbol and p.side == side), None)
if not pos:
return OrderResult(
success=False, error="no_position",
exchange_id=ExchangeID.BINANCE,
)
close_side = "SELL" if side == OrderSide.BUY else "BUY"
params = {
"symbol": symbol, "side": close_side,
"symbol": _normalize_symbol(symbol), "side": close_side,
"type": "MARKET", "quantity": str(pos.size),
"reduceOnly": "true",
}
data = await self._signed("POST", "/fapi/v1/order", params)
if "orderId" not in data:
return OrderResult(
success=False, error=data.get("msg", "unknown"),
exchange_id=ExchangeID.BINANCE,
)
return OrderResult(
order_id=str(data.get("orderId", "")), symbol=symbol,
side=OrderSide(close_side),
Expand All @@ -200,9 +250,11 @@ async def close_position(self, symbol: str, side: OrderSide) -> OrderResult:

async def set_leverage(self, symbol: str, leverage: int) -> bool:
try:
await self._signed("POST", "/fapi/v1/leverage", {
"symbol": symbol, "leverage": str(leverage),
data = await self._signed("POST", "/fapi/v1/leverage", {
"symbol": _normalize_symbol(symbol), "leverage": str(leverage),
})
if isinstance(data, dict) and data.get("code", 0) < 0:
return False
return True
except Exception:
return False
Expand Down
29 changes: 27 additions & 2 deletions darwin_agent/exchanges/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,11 @@ async def set_leverage(self, symbol: str, leverage: int) -> bool:
if adapter is None:
return False
try:
await self._timed(
result = await self._timed(
adapter.set_leverage(symbol, leverage),
f"set_leverage({symbol}@{target.value})",
)
return True
return bool(result)
except Exception as exc:
logger.warning("set_leverage failed on %s: %s", target.value, exc)
return False
Expand All @@ -228,6 +228,31 @@ async def get_balance(self) -> float:
logger.error("get_balance failed on ALL exchanges — returning 0.0")
return total


async def close(self) -> None:
"""Close all adapters that expose an async close() method."""
for eid, adapter in self._adapters.items():
close = getattr(adapter, "close", None)
if close is None:
continue
try:
await self._timed(close(), f"close({eid.value})")
except Exception as exc:
logger.warning("close failed on %s: %s", eid.value, exc)

async def connect_all(self) -> None:
"""Backward-compatible startup hook."""
await self.refresh_statuses()

async def get_all_statuses(self) -> Dict[ExchangeID, ExchangeStatus]:
"""Backward-compatible status API expected by main entrypoint."""
await self.refresh_statuses()
return self.get_exchange_statuses()

async def disconnect_all(self) -> None:
"""Backward-compatible shutdown hook."""
await self.close()

# ── IExchangeRouter specific ─────────────────────────────

def get_exchange_statuses(self) -> Dict[ExchangeID, ExchangeStatus]:
Expand Down
Loading