#!/usr/bin/env python3 """ ROB-BOT API Server Flask backend for Flutter trading app """ import os import sys import time import json import logging import sqlite3 from datetime import datetime, timedelta from functools import wraps from threading import Thread, Lock from flask import Flask, request, jsonify, g from flask_socketio import SocketIO, emit, disconnect from flask_cors import CORS from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity from cryptography.fernet import Fernet # Add src to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) from binance_client import BinanceClient from position_manager import PositionManager from setup_scanner import SetupScanner from sentiment import SentimentTracker from risk_manager import RiskManager # Configuration app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-change-in-production') app.config['JWT_SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY', 'jwt-dev-key-change') app.config['DATABASE'] = os.environ.get('DATABASE_PATH', 'data/rob_bot.db') # Extensions CORS(app, resources={r"/api/*": {"origins": "*"}}) jwt = JWTManager(app) socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading') # Logging logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[ logging.FileHandler('logs/api.log'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) # Global state class BotState: def __init__(self): self.bot_engaged = False self.binance_client = None self.position_manager = None self.setup_scanner = None self.sentiment_tracker = None self.risk_manager = None self.current_setups = [] self.price_cache = {} self._lock = Lock() self._init_components() def _init_components(self): """Initialize trading components""" try: api_key = os.environ.get('BINANCE_API_KEY') api_secret = os.environ.get('BINANCE_SECRET') if api_key and api_secret: self.binance_client = BinanceClient(api_key, api_secret) self.position_manager = PositionManager(self.binance_client) self.setup_scanner = SetupScanner(self.binance_client) self.sentiment_tracker = SentimentTracker() self.risk_manager = RiskManager() logger.info("Trading components initialized") else: logger.warning("Binance API keys not set - running in demo mode") except Exception as e: logger.error(f"Failed to initialize components: {e}") def toggle_bot(self, engaged: bool): """Start or stop the trading bot""" with self._lock: self.bot_engaged = engaged logger.info(f"Bot {'ENGAGED' if engaged else 'DISENGAGED'}") if engaged: self._start_trading_loop() def _start_trading_loop(self): """Start background trading thread""" def trading_loop(): while self.bot_engaged: try: # Scan for setups setups = self.setup_scanner.scan_all_pairs() self.current_setups = setups # Update positions if self.position_manager: self.position_manager.update_positions() # Update sentiment sentiment = self.sentiment_tracker.get_sentiment() # Broadcast updates via WebSocket socketio.emit('update', { 'setups': [s.to_dict() for s in setups], 'positions': self.position_manager.get_all_positions() if self.position_manager else [], 'sentiment': sentiment, 'timestamp': time.time() }) time.sleep(15) # Scan every 15 seconds except Exception as e: logger.error(f"Trading loop error: {e}") time.sleep(5) thread = Thread(target=trading_loop, daemon=True) thread.start() # Initialize bot state bot_state = BotState() # Database helpers def get_db(): """Get database connection""" if 'db' not in g: os.makedirs(os.path.dirname(app.config['DATABASE']), exist_ok=True) g.db = sqlite3.connect(app.config['DATABASE']) g.db.row_factory = sqlite3.Row return g.db @app.teardown_appcontext def close_db(exception): """Close database connection""" db = g.pop('db', None) if db is not None: db.close() def init_db(): """Initialize database tables""" with app.app_context(): db = get_db() with app.open_resource('database/schema.sql') as f: db.executescript(f.read().decode('utf8')) db.commit() logger.info("Database initialized") # Auth endpoints @app.route('/api/auth/register', methods=['POST']) def register(): """Register new user""" data = request.get_json() username = data.get('username') password = data.get('password') if not username or not password: return jsonify({'error': 'Username and password required'}), 400 db = get_db() # Check if user exists existing = db.execute('SELECT id FROM users WHERE username = ?', (username,)).fetchone() if existing: return jsonify({'error': 'Username already exists'}), 409 # Hash password (simplified - use bcrypt in production) import hashlib password_hash = hashlib.sha256(password.encode()).hexdigest() # Create user db.execute( 'INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)', (username, password_hash, datetime.now().isoformat()) ) db.commit() return jsonify({'message': 'User created successfully'}), 201 @app.route('/api/auth/login', methods=['POST']) def login(): """Login user""" data = request.get_json() username = data.get('username') password = data.get('password') if not username or not password: return jsonify({'error': 'Username and password required'}), 400 db = get_db() # Verify user import hashlib password_hash = hashlib.sha256(password.encode()).hexdigest() user = db.execute( 'SELECT id, username FROM users WHERE username = ? AND password_hash = ?', (username, password_hash) ).fetchone() if not user: return jsonify({'error': 'Invalid credentials'}), 401 # Create JWT token access_token = create_access_token(identity=user['id']) return jsonify({ 'access_token': access_token, 'username': user['username'] }) # Bot status endpoints @app.route('/api/bot/status', methods=['GET']) @jwt_required(optional=True) def get_bot_status(): """Get bot status""" return jsonify({ 'engaged': bot_state.bot_engaged, 'has_api_keys': bot_state.binance_client is not None, 'timestamp': time.time() }) @app.route('/api/bot/toggle', methods=['POST']) @jwt_required() def toggle_bot(): """Toggle bot on/off""" data = request.get_json() engaged = data.get('engaged', False) bot_state.toggle_bot(engaged) return jsonify({ 'engaged': bot_state.bot_engaged, 'message': f"Bot {'started' if engaged else 'stopped'}" }) # Positions endpoints @app.route('/api/positions', methods=['GET']) @jwt_required() def get_positions(): """Get all positions""" if not bot_state.position_manager: return jsonify({'error': 'Trading not configured'}), 503 positions = bot_state.position_manager.get_all_positions() return jsonify({'positions': positions}) @app.route('/api/positions/', methods=['GET']) @jwt_required() def get_position(position_id): """Get position details""" if not bot_state.position_manager: return jsonify({'error': 'Trading not configured'}), 503 position = bot_state.position_manager.get_position(position_id) if not position: return jsonify({'error': 'Position not found'}), 404 return jsonify({'position': position}) @app.route('/api/positions//close', methods=['POST']) @jwt_required() def close_position(position_id): """Close a position""" if not bot_state.position_manager: return jsonify({'error': 'Trading not configured'}), 503 result = bot_state.position_manager.close_position(position_id) return jsonify(result) # Setups endpoints @app.route('/api/setups', methods=['GET']) @jwt_required() def get_setups(): """Get current trading setups""" setups = [s.to_dict() for s in bot_state.current_setups] return jsonify({'setups': setups}) @app.route('/api/setups/scan', methods=['POST']) @jwt_required() def trigger_scan(): """Trigger manual scan""" if not bot_state.setup_scanner: return jsonify({'error': 'Scanner not configured'}), 503 setups = bot_state.setup_scanner.scan_all_pairs() bot_state.current_setups = setups return jsonify({ 'setups': [s.to_dict() for s in setups], 'count': len(setups) }) # Sentiment endpoints @app.route('/api/sentiment', methods=['GET']) @jwt_required(optional=True) def get_sentiment(): """Get market sentiment data""" if not bot_state.sentiment_tracker: return jsonify({'error': 'Sentiment tracker not configured'}), 503 sentiment = bot_state.sentiment_tracker.get_sentiment() return jsonify(sentiment) # Settings endpoints @app.route('/api/settings', methods=['GET']) @jwt_required() def get_settings(): """Get user settings""" user_id = get_jwt_identity() db = get_db() settings = db.execute( 'SELECT * FROM settings WHERE user_id = ?', (user_id,) ).fetchone() if not settings: # Return defaults return jsonify({ 'min_grade': 'B', 'risk_per_trade': 1.0, 'max_positions': 5, 'auto_trading': False }) return jsonify({ 'min_grade': settings['min_grade'], 'risk_per_trade': settings['risk_per_trade'], 'max_positions': settings['max_positions'], 'auto_trading': settings['auto_trading'] }) @app.route('/api/settings', methods=['POST']) @jwt_required() def update_settings(): """Update user settings""" user_id = get_jwt_identity() data = request.get_json() db = get_db() # Check if settings exist existing = db.execute( 'SELECT id FROM settings WHERE user_id = ?', (user_id,) ).fetchone() if existing: db.execute(''' UPDATE settings SET min_grade = ?, risk_per_trade = ?, max_positions = ?, auto_trading = ?, updated_at = ? WHERE user_id = ? ''', ( data.get('min_grade', 'B'), data.get('risk_per_trade', 1.0), data.get('max_positions', 5), data.get('auto_trading', False), datetime.now().isoformat(), user_id )) else: db.execute(''' INSERT INTO settings (user_id, min_grade, risk_per_trade, max_positions, auto_trading, updated_at) VALUES (?, ?, ?, ?, ?, ?) ''', ( user_id, data.get('min_grade', 'B'), data.get('risk_per_trade', 1.0), data.get('max_positions', 5), data.get('auto_trading', False), datetime.now().isoformat() )) db.commit() return jsonify({'message': 'Settings updated'}) # Health check @app.route('/api/health', methods=['GET']) def health_check(): """Health check endpoint""" return jsonify({ 'status': 'healthy', 'timestamp': time.time(), 'bot_engaged': bot_state.bot_engaged }) # WebSocket events @socketio.on('connect') def handle_connect(): """Handle client connection""" logger.info(f'Client connected: {request.sid}') emit('connected', {'message': 'Connected to ROB-BOT'}) @socketio.on('disconnect') def handle_disconnect(): """Handle client disconnection""" logger.info(f'Client disconnected: {request.sid}') @socketio.on('subscribe_prices') def handle_subscribe_prices(data): """Subscribe to price updates""" pairs = data.get('pairs', []) logger.info(f'Client subscribed to prices: {pairs}') emit('subscribed', {'pairs': pairs}) # Error handlers @app.errorhandler(404) def not_found(error): return jsonify({'error': 'Not found'}), 404 @app.errorhandler(500) def internal_error(error): logger.error(f'Server error: {error}') return jsonify({'error': 'Internal server error'}), 500 # Main entry point if __name__ == '__main__': # Initialize database init_db() # Create logs directory os.makedirs('logs', exist_ok=True) os.makedirs('data', exist_ok=True) # Start server logger.info("Starting ROB-BOT API server...") socketio.run(app, host='0.0.0.0', port=5000, debug=False)