Skip to content
Merged
63 changes: 51 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,30 +1,69 @@
# Binance API Configuration
# =============================================================================
# ELVIS Trading Bot — Environment Variables
# =============================================================================
# Copy this file to .env and fill in real values.
# NEVER commit .env to version control.
# =============================================================================

# --- Binance API ---
BINANCE_API_KEY=your_binance_api_key_here
BINANCE_API_SECRET=your_binance_api_secret_here
BINANCE_FUTURES_TESTNET_API_KEY=your_futures_testnet_api_key_here
BINANCE_FUTURES_TESTNET_API_SECRET=your_futures_testnet_api_secret_here

# --- HashiCorp Vault (ISSUE #10) ---
# REQUIRED: The bot will refuse to start if VAULT_TOKEN is not set.
VAULT_ADDR=http://127.0.0.1:8200
VAULT_TOKEN=your_vault_token_here

# --- PostgreSQL Database (ISSUE #11) ---
# REQUIRED: The bot will refuse to start if DB_PASSWORD is not set.
DB_HOST=localhost
DB_PORT=5432
DB_USER=elvis_user
DB_PASSWORD=your_secure_db_password_here
DB_NAME=elvis_trading

# --- Flask API Authentication (ISSUE #13) ---
# REQUIRED: All API requests must include header: X-API-Key: <value>
# The /health endpoint is exempt from authentication.
API_KEY=your_random_api_key_here

# Telegram Bot Configuration (optional)
# --- Telegram Bot (optional) ---
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
TELEGRAM_CHAT_ID=your_telegram_chat_id_here

# Redis Configuration (for caching)
# --- Redis (optional) ---
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASSWORD=

# Database Configuration (if using)
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=elvis_user
POSTGRES_PASSWORD=elvis_password
POSTGRES_DBNAME=elvis_trading

# Trading Configuration
# --- Trading Configuration ---
TRADING_MODE=paper # paper or live
MAX_POSITION_SIZE=0.1
MAX_DAILY_TRADES=5
RISK_PER_TRADE=0.02

# Monitoring
# --- Leverage Safety (ISSUE #14) ---
# Default is 3x. Set higher only with full understanding of liquidation risk.
DEFAULT_LEVERAGE=3
# Uncomment the line below ONLY if you explicitly need leverage > 10x.
# OVERRIDE_HIGH_LEVERAGE=true

# --- Binance Rate Limiting (ISSUE #12) ---
# Tune these only if you know what you're doing.
BINANCE_MAX_RETRIES=5
BINANCE_RETRY_MIN_WAIT=1
BINANCE_RETRY_MAX_WAIT=60
BINANCE_RATE_LIMIT_WARN_FRACTION=0.80
BINANCE_WEIGHT_LIMIT_PER_MIN=1200
BINANCE_ORDER_LIMIT_PER_SEC=10
BINANCE_RATE_LIMIT_PAUSE_SECONDS=5.0

# --- Kill-Switch / Redis Persistence (ISSUE #15) ---
# REDIS_HOST / REDIS_PORT already defined above; used for kill-switch persistence too.

# --- Monitoring ---
PROMETHEUS_PUSHGATEWAY_URL=http://localhost:9091
GRAFANA_API_KEY=your_grafana_api_key_here
84 changes: 78 additions & 6 deletions config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ def BINANCE_FUTURES_TESTNET_API_SECRET(self):
'TAKE_PROFIT_PCT': 0.02, # Added to fix the current error; adjust as needed
'LEVERAGE_MAX': 125, # Maximum leverage for futures
'LEVERAGE_MIN': 1, # Minimum leverage for futures
'DEFAULT_LEVERAGE': 100, # Default leverage for maximum trading power
# Issue #14: Default leverage reduced from 100x to 3x to prevent catastrophic
# losses on startup. Override via DEFAULT_LEVERAGE env var (integer).
'DEFAULT_LEVERAGE': int(os.getenv('DEFAULT_LEVERAGE', '3')),
'MAX_TRADES_PER_DAY': 10, # Added to fix MAX_TRADES_PER_DAY error
'DAILY_PROFIT_TARGET_USD': 100, # Added to fix DAILY_PROFIT_TARGET_USD error
'DAILY_LOSS_LIMIT_USD': 100, # Added to fix DAILY_LOSS_LIMIT_USD error
Expand All @@ -59,14 +61,23 @@ def BINANCE_FUTURES_TESTNET_API_SECRET(self):
'LOG_TO_FILE': True,
}

# ISSUE #11 FIX: Removed hardcoded Postgres password 'elvis_password'.
# Database credentials must never be hardcoded in source code.
# Set DB_PASSWORD (and optionally DB_HOST, DB_PORT, DB_USER, DB_NAME) as environment variables.
POSTGRES_CONFIG = {
'HOST': 'localhost',
'PORT': 5432,
'USER': 'elvis_user',
'PASSWORD': 'elvis_password',
'DBNAME': 'elvis_trading'
'HOST': os.getenv('DB_HOST', 'localhost'),
'PORT': int(os.getenv('DB_PORT', '5432')),
'USER': os.getenv('DB_USER', 'elvis_user'),
'PASSWORD': os.getenv('DB_PASSWORD'), # Required — no default; must be set explicitly
'DBNAME': os.getenv('DB_NAME', 'elvis_trading'),
}

if not POSTGRES_CONFIG['PASSWORD']:
raise EnvironmentError(
"DB_PASSWORD environment variable is not set. "
"Export it before starting: export DB_PASSWORD=<your-db-password>"
)




Expand All @@ -92,3 +103,64 @@ def BINANCE_FUTURES_TESTNET_API_SECRET(self):
'MAX_CONCURRENT_PAIRS': 3, # Maximum pairs to trade simultaneously
}



# ---------------------------------------------------------------------------
# Issue #14: Leverage safety validation
# Call validate_leverage_config() before starting the trading engine.
# ---------------------------------------------------------------------------
import logging as _logging

_leverage_logger = _logging.getLogger(__name__)

def validate_leverage_config(leverage: int = None) -> int:
"""
Validate the configured leverage and refuse to start if it is dangerously
high without an explicit operator override.

Rules:
- leverage > 10x requires OVERRIDE_HIGH_LEVERAGE=true in the environment.
- leverage > 5x emits a WARNING log on every startup.
- leverage <= 0 raises ValueError immediately.

Args:
leverage: The leverage value to validate. Defaults to
TRADING_CONFIG['DEFAULT_LEVERAGE'].

Returns:
The validated leverage value (int).

Raises:
ValueError: If leverage <= 0 or an invalid value is supplied.
EnvironmentError: If leverage > 10x and OVERRIDE_HIGH_LEVERAGE != 'true'.
"""
if leverage is None:
leverage = TRADING_CONFIG['DEFAULT_LEVERAGE']

leverage = int(leverage)

if leverage <= 0:
raise ValueError(f"Leverage must be a positive integer, got {leverage}.")

if leverage > 5:
_leverage_logger.warning(
"⚠️ Leverage is set to %dx which is above the recommended maximum of 5x. "
"High leverage significantly increases liquidation risk.",
leverage,
)

if leverage > 10:
override = os.getenv("OVERRIDE_HIGH_LEVERAGE", "false").strip().lower()
if override != "true":
raise EnvironmentError(
f"Leverage {leverage}x exceeds the 10x safety limit. "
"Set OVERRIDE_HIGH_LEVERAGE=true in your environment to acknowledge "
"the risk and allow startup."
)
_leverage_logger.warning(
"🚨 OVERRIDE_HIGH_LEVERAGE=true detected — starting with %dx leverage. "
"Ensure you understand the liquidation risks.",
leverage,
)

return leverage
8 changes: 7 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
if not os.getenv('VAULT_ADDR'):
os.environ['VAULT_ADDR'] = 'http://127.0.0.1:8200'
if not os.getenv('VAULT_TOKEN'):
os.environ['VAULT_TOKEN'] = 'trading-bot-token'
# ISSUE #10 FIX: Removed hardcoded Vault token 'trading-bot-token'.
# Hardcoded secrets in source code expose credentials to anyone with repo access.
# VAULT_TOKEN must be set as an environment variable before starting the bot.
raise EnvironmentError(
"VAULT_TOKEN environment variable is not set. "
"Export it before starting: export VAULT_TOKEN=<your-vault-token>"
)

from core.bootstrap import bootstrap_application
from core.di import container
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Retry / back-off (Issue #12 — Binance rate limiting)
tenacity

# Core dependencies
numpy
pandas
Expand Down
52 changes: 43 additions & 9 deletions trading/execution/binance_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,28 @@
from trading.fees.binance_fee_calculator import BinanceFeeCalculator
from datetime import datetime

# Issue #12: Import rate-limit utilities (retry decorator + header checker).
from utils.binance_rate_limiter import binance_retry, check_rate_limit_headers

class BinanceExecutor(BaseExecutor):
def __init__(self, logger: logging.Logger = None, api_key: str = None, api_secret: str = None, is_testnet: bool = False, use_futures: bool = False, default_leverage: int = 100, **kwargs):
# Issue #14: Default leverage reduced from 100x to 3x. Callers may pass an
# explicit value, which is validated by validate_leverage_config() below.
def __init__(self, logger: logging.Logger = None, api_key: str = None, api_secret: str = None, is_testnet: bool = False, use_futures: bool = False, default_leverage: int = None, **kwargs):
super().__init__(logger, **kwargs)
self.client = None
self.api_key = api_key
self.api_secret = api_secret
self.is_testnet = is_testnet
self.use_futures = use_futures
self.default_leverage = default_leverage
self.fee_calculator = BinanceFeeCalculator(logger)
self.db_available = False

# Issue #14: Validate leverage before storing it. Imports here to
# avoid circular-import issues at module load time.
from config.config import validate_leverage_config, TRADING_CONFIG
resolved_leverage = default_leverage if default_leverage is not None else TRADING_CONFIG['DEFAULT_LEVERAGE']
self.default_leverage = validate_leverage_config(resolved_leverage)

if is_testnet:
self._init_paper_trading_db()

Expand Down Expand Up @@ -70,19 +81,30 @@ def initialize(self) -> bool:
self.logger.error(f"Failed to initialize BinanceExecutor: {e}")
return False # Failed due to other error

# Issue #12: Wrap each live Binance API call with @binance_retry so that
# transient failures (network blips, 429 rate-limit responses) are retried
# with exponential back-off instead of failing immediately.

def get_balance(self) -> Dict[str, float]:
if self.client is None or (self.is_testnet and not self.use_futures):
return self._calculate_paper_balance()
try:
if FUTURES_AVAILABLE and isinstance(self.client, UMFutures):
account = self.client.balance()
@binance_retry
def _fetch():
account = self.client.balance()
account_info = self.client.account()
return account, account_info
account, account_info = _fetch()
balances = {item['asset']: float(item['balance']) for item in account if float(item['balance']) > 0}
account_info = self.client.account()
wallet_balance = float(account_info['totalWalletBalance'])
self.logger.info(f"Futures account - Wallet Balance: ${wallet_balance:.2f}")
return {'USDT': wallet_balance, **balances}
else:
account = self.client.get_account()
@binance_retry
def _fetch():
return self.client.get_account()
account = _fetch()
return {item['asset']: float(item['free']) for item in account['balances']}
except (ClientError if FUTURES_AVAILABLE else BinanceAPIException) as e:
self.logger.error(f"Error getting balance: {e}")
Expand All @@ -92,7 +114,10 @@ def get_position(self, symbol: str) -> Dict[str, Any]:
if self.client is None or not self.use_futures:
return {}
try:
positions = self.client.get_position_risk(symbol=symbol)
@binance_retry
def _fetch():
return self.client.get_position_risk(symbol=symbol)
positions = _fetch()
return positions[0] if positions else {}
except (ClientError if FUTURES_AVAILABLE else BinanceAPIException) as e:
self.logger.error(f"Error getting position for {symbol}: {e}")
Expand All @@ -103,9 +128,15 @@ def get_current_price(self, symbol: str) -> float:
return self._get_mock_price(symbol)
try:
if self.use_futures:
return float(self.client.ticker_price(symbol=symbol)['price'])
@binance_retry
def _fetch():
return self.client.ticker_price(symbol=symbol)
return float(_fetch()['price'])
else:
return float(self.client.get_symbol_ticker(symbol=symbol)['price'])
@binance_retry
def _fetch():
return self.client.get_symbol_ticker(symbol=symbol)
return float(_fetch()['price'])
except (ClientError if FUTURES_AVAILABLE else BinanceAPIException) as e:
self.logger.error(f"Error getting current price for {symbol}: {e}")
return 0.0
Expand All @@ -115,7 +146,10 @@ def set_leverage(self, symbol: str, leverage: int) -> None:
self.logger.info(f"Paper trading: Leverage set to {leverage}x for {symbol}")
return
try:
self.client.change_leverage(symbol=symbol, leverage=leverage)
@binance_retry
def _set():
return self.client.change_leverage(symbol=symbol, leverage=leverage)
_set()
self.logger.info(f"Leverage for {symbol} set to {leverage}x.")
except BinanceAPIException as e:
self.logger.error(f"Error setting leverage for {symbol}: {e}")
Expand Down
Loading
Loading