Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import random | |
| import math | |
| import io | |
| from datetime import datetime, timedelta | |
| from flask import Flask, render_template, request, jsonify, send_file | |
| from werkzeug.utils import secure_filename | |
| from werkzeug.exceptions import RequestEntityTooLarge | |
| app = Flask(__name__) | |
| app.secret_key = os.urandom(24) | |
| # Robustness: Max content length for uploads | |
| app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 5MB limit | |
| ALLOWED_EXTENSIONS = {'json'} | |
| def allowed_file(filename): | |
| return '.' in filename and \ | |
| filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS | |
| class GridStrategy: | |
| def __init__(self, lower_price, upper_price, grid_num, investment, current_price): | |
| self.lower_price = float(lower_price) | |
| self.upper_price = float(upper_price) | |
| self.grid_num = int(grid_num) | |
| self.investment = float(investment) | |
| self.initial_price = float(current_price) | |
| # Calculate grid lines | |
| self.grids = [] | |
| step = (self.upper_price - self.lower_price) / self.grid_num | |
| for i in range(self.grid_num + 1): | |
| self.grids.append(self.lower_price + i * step) | |
| self.grid_step = step | |
| self.cash = self.investment | |
| self.holdings = 0.0 | |
| self.trades = [] | |
| self.total_arbitrage_profit = 0.0 | |
| # Initial Position Building | |
| # Ideally, if price is in the middle, we should hold some assets to sell as price goes up | |
| # Simple Logic: Buy assets worth 50% of investment if price is within range | |
| # Better Logic: Calculate required holdings based on how many grids are ABOVE current price (to sell) | |
| # Let's use a "Geometric" approach or simple "Equal Difference" approach | |
| # For this demo, we assume we start with 50/50 split if within range | |
| if self.lower_price < self.initial_price < self.upper_price: | |
| buy_amount = self.investment / 2 | |
| self.cash -= buy_amount | |
| self.holdings = buy_amount / self.initial_price | |
| self.trades.append({ | |
| 'type': 'INIT_BUY', | |
| 'price': self.initial_price, | |
| 'amount': self.holdings, | |
| 'time': 'Start' | |
| }) | |
| # Track last grid index crossed | |
| self.last_grid_index = self._get_grid_index(self.initial_price) | |
| def _get_grid_index(self, price): | |
| if price <= self.lower_price: | |
| return -1 | |
| if price >= self.upper_price: | |
| return self.grid_num + 1 | |
| return int((price - self.lower_price) / self.grid_step) | |
| def process_price(self, price, timestamp): | |
| current_grid_index = self._get_grid_index(price) | |
| # Price moved across grid lines | |
| if current_grid_index != self.last_grid_index: | |
| # Check direction and multiple grid crossings | |
| diff = current_grid_index - self.last_grid_index | |
| # Simple handling: Just process the boundary crossing closest to current price | |
| # In a real engine, we'd handle gaps. Here we assume granular data or just take the new state. | |
| # Logic: | |
| # If index increases (price goes up): We crossed a line upwards -> SELL | |
| # If index decreases (price goes down): We crossed a line downwards -> BUY | |
| # Amount per grid: Total Investment / Grid Num (Simplified) | |
| amount_per_grid_usdt = self.investment / self.grid_num | |
| action = None | |
| trade_price = price | |
| trade_amount = 0 | |
| if diff > 0: | |
| # Price Up -> Sell | |
| # Only sell if we have holdings and we are within valid grid range | |
| if self.holdings > 0 and 0 <= self.last_grid_index < self.grid_num: | |
| # Sell one grid's worth | |
| amount_to_sell = amount_per_grid_usdt / price | |
| if self.holdings >= amount_to_sell: | |
| self.holdings -= amount_to_sell | |
| self.cash += amount_to_sell * price | |
| trade_amount = amount_to_sell | |
| action = 'SELL' | |
| # Calculate profit per grid (approximate) | |
| # Profit = Buy Price (lower grid) vs Sell Price (this grid) | |
| # Roughly = grid_step * amount | |
| self.total_arbitrage_profit += self.grid_step * amount_to_sell | |
| elif diff < 0: | |
| # Price Down -> Buy | |
| # Only buy if we have cash and within range | |
| if self.cash > amount_per_grid_usdt and 0 <= current_grid_index < self.grid_num: | |
| amount_to_buy = amount_per_grid_usdt / price | |
| self.cash -= amount_to_buy * price | |
| self.holdings += amount_to_buy | |
| trade_amount = amount_to_buy | |
| action = 'BUY' | |
| if action: | |
| self.trades.append({ | |
| 'type': action, | |
| 'price': price, | |
| 'amount': trade_amount, | |
| 'time': timestamp, | |
| 'profit': self.total_arbitrage_profit | |
| }) | |
| self.last_grid_index = current_grid_index | |
| # Calculate current status | |
| total_assets_value = self.cash + (self.holdings * price) | |
| unrealized_pnl = total_assets_value - self.investment | |
| return { | |
| 'price': price, | |
| 'total_value': total_assets_value, | |
| 'cash': self.cash, | |
| 'holdings_value': self.holdings * price, | |
| 'arbitrage_profit': self.total_arbitrage_profit, | |
| 'unrealized_pnl': unrealized_pnl, | |
| 'timestamp': timestamp | |
| } | |
| def index(): | |
| return render_template('index.html') | |
| def generate_data(): | |
| try: | |
| data = request.json | |
| days = int(data.get('days', 30)) | |
| start_price = float(data.get('start_price', 1000)) | |
| volatility = float(data.get('volatility', 0.02)) # Daily volatility | |
| trend = float(data.get('trend', 0.000)) # Daily trend | |
| prices = [] | |
| current_price = start_price | |
| start_date = datetime.now() - timedelta(days=days) | |
| # Generate hourly data | |
| hours = days * 24 | |
| for i in range(hours): | |
| # Geometric Brownian Motion step | |
| change = random.normalvariate(trend/24, volatility/math.sqrt(24)) | |
| current_price = current_price * (1 + change) | |
| timestamp = (start_date + timedelta(hours=i)).strftime('%Y-%m-%d %H:%M') | |
| prices.append({'time': timestamp, 'price': round(current_price, 2)}) | |
| return jsonify({'prices': prices}) | |
| except Exception as e: | |
| app.logger.error(f"Generate data error: {str(e)}") | |
| return jsonify({'error': str(e)}), 500 | |
| def simulate(): | |
| try: | |
| data = request.json | |
| prices = data.get('prices', []) | |
| params = data.get('params', {}) | |
| if not prices or not params: | |
| return jsonify({'error': 'Missing data'}), 400 | |
| strategy = GridStrategy( | |
| lower_price=params.get('lower_price'), | |
| upper_price=params.get('upper_price'), | |
| grid_num=params.get('grid_num'), | |
| investment=params.get('investment'), | |
| current_price=prices[0]['price'] | |
| ) | |
| results = [] | |
| trades = [] | |
| for p in prices: | |
| step_result = strategy.process_price(p['price'], p['time']) | |
| results.append(step_result) | |
| return jsonify({ | |
| 'results': results, | |
| 'trades': strategy.trades, | |
| 'summary': { | |
| 'final_value': results[-1]['total_value'], | |
| 'total_profit': results[-1]['total_value'] - params.get('investment'), | |
| 'arbitrage_profit': strategy.total_arbitrage_profit, | |
| 'grid_yield': (strategy.total_arbitrage_profit / params.get('investment')) * 100 | |
| } | |
| }) | |
| except Exception as e: | |
| app.logger.error(f"Simulation error: {str(e)}") | |
| return jsonify({'error': str(e)}), 500 | |
| def upload_config(): | |
| # Robust file upload handling | |
| if 'file' not in request.files: | |
| return jsonify({'error': 'No file part'}), 400 | |
| file = request.files['file'] | |
| if file.filename == '': | |
| return jsonify({'error': 'No selected file'}), 400 | |
| if file and allowed_file(file.filename): | |
| try: | |
| # Read file content | |
| content = file.read() | |
| # Check for binary content (null bytes) | |
| if b'\0' in content: | |
| return jsonify({'error': 'Binary file detected'}), 400 | |
| # Parse JSON | |
| try: | |
| config = json.loads(content.decode('utf-8')) | |
| return jsonify({'message': 'Config uploaded successfully', 'config': config}) | |
| except UnicodeDecodeError: | |
| return jsonify({'error': 'File encoding not supported (must be UTF-8)'}), 400 | |
| except json.JSONDecodeError: | |
| return jsonify({'error': 'Invalid JSON format'}), 400 | |
| except Exception as e: | |
| return jsonify({'error': f"Upload failed: {str(e)}"}), 500 | |
| else: | |
| return jsonify({'error': 'Invalid file type (only .json allowed)'}), 400 | |
| def page_not_found(e): | |
| # Return a JSON response for API calls, or render a template | |
| if request.path.startswith('/api/'): | |
| return jsonify({'error': 'Not found'}), 404 | |
| return render_template('index.html'), 200 # Fallback to SPA entry or create a 404 page | |
| def internal_server_error(e): | |
| return jsonify({'error': 'Internal Server Error'}), 500 | |
| def handle_file_too_large(e): | |
| return jsonify({'error': 'File too large (max 5MB)'}), 413 | |
| def health(): | |
| return jsonify({"status": "healthy"}) | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=7860) | |