├── tmp └── README.md ├── routes ├── voice │ ├── utils │ │ ├── __init__.py │ │ └── helpers.py │ ├── services │ │ └── __init__.py │ └── __init__.py ├── scalper │ ├── __init__.py │ ├── routes.py │ └── services │ │ └── scalper_service.py ├── orders │ ├── __init__.py │ ├── utils │ │ ├── formatters.py │ │ └── helpers.py │ ├── constants.py │ ├── routes.py │ ├── services │ │ ├── market_feed.py │ │ └── broker_service.py │ └── validators │ │ ├── order_validator.py │ │ └── exchange_rules.py ├── dashboard │ ├── __init__.py │ ├── settings_service.py │ ├── utils.py │ ├── routes.py │ └── market_data_service.py ├── home.py ├── __init__.py ├── logs.py └── funds.py ├── static └── js │ ├── modules │ ├── templates │ │ ├── emptyWatchlist.html │ │ ├── marketDepth.html │ │ └── symbolListItem.html │ ├── watchlistManager.js │ ├── orderEntry │ │ ├── utils │ │ │ ├── formatters.js │ │ │ └── validators.js │ │ ├── services │ │ │ ├── orderApi.js │ │ │ ├── orderState.js │ │ │ └── priceService.js │ │ └── components │ │ │ ├── OrderForm.js │ │ │ ├── MarketDepth.js │ │ │ ├── QuantityInput.js │ │ │ └── PriceInput.js │ ├── watchlistCore.js │ └── marketDataDecoder.js │ └── theme.js ├── prestart.sh ├── production_files ├── prestart.sh ├── openterminal.service └── openterminal_nginx ├── templates ├── index.html ├── components │ ├── watchlist │ │ ├── _list.html │ │ ├── _market_depth.html │ │ ├── _item.html │ │ └── _manage_modal.html │ └── orders │ │ ├── _market_depth.html │ │ └── _order_form.html ├── register.html ├── login.html ├── logs.html ├── tradebook.html ├── positions.html ├── orderbook.html ├── about.html └── funds.html ├── scheduler.py ├── triggerdb.py ├── .gitignore ├── requirements.txt ├── config.py ├── app.py ├── README.md ├── master_contract.py ├── models.py ├── extensions.py └── Proposed Architecture.md /tmp/README.md: -------------------------------------------------------------------------------- 1 | DUMMY File -------------------------------------------------------------------------------- /routes/voice/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Initialize utils package 2 | -------------------------------------------------------------------------------- /routes/voice/services/__init__.py: -------------------------------------------------------------------------------- 1 | # Initialize services package 2 | -------------------------------------------------------------------------------- /routes/scalper/__init__.py: -------------------------------------------------------------------------------- 1 | from .routes import scalper_bp 2 | 3 | __all__ = ['scalper_bp'] 4 | -------------------------------------------------------------------------------- /routes/orders/__init__.py: -------------------------------------------------------------------------------- 1 | # routes/orders/__init__.py 2 | from flask import Blueprint 3 | 4 | orders_bp = Blueprint('orders', __name__) 5 | 6 | from . import routes -------------------------------------------------------------------------------- /routes/dashboard/__init__.py: -------------------------------------------------------------------------------- 1 | from .routes import dashboard_bp 2 | 3 | # This allows other parts of the app to continue importing dashboard_bp as before 4 | __all__ = ['dashboard_bp'] -------------------------------------------------------------------------------- /static/js/modules/templates/emptyWatchlist.html: -------------------------------------------------------------------------------- 1 |
2 |

No watchlists created yet

3 |

Click the "Add" button to create your first watchlist

4 |
5 | -------------------------------------------------------------------------------- /static/js/modules/watchlistManager.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/watchlistManager.js 2 | 3 | const WatchlistManager = { 4 | init() { 5 | WatchlistCore.init(); 6 | } 7 | }; 8 | 9 | // Export for use in other modules 10 | window.WatchlistManager = WatchlistManager; 11 | -------------------------------------------------------------------------------- /routes/home.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | 3 | home_bp = Blueprint('home', __name__) 4 | 5 | @home_bp.route('/') 6 | def home(): 7 | return render_template('index.html') 8 | 9 | @home_bp.route('/about') 10 | def about(): 11 | return render_template('about.html') 12 | -------------------------------------------------------------------------------- /prestart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create and set permissions for the Gunicorn log directory 4 | mkdir -p /var/log/gunicorn 5 | chown -R www-data:www-data /var/log/gunicorn 6 | chmod 755 /var/log/gunicorn 7 | 8 | # Set ownership and permissions for the application directory 9 | chown -R www-data:www-data /var/python/openterminal 10 | chmod -R u+rwX,g+rX,o+rX /var/python/openterminal 11 | 12 | -------------------------------------------------------------------------------- /production_files/prestart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create and set permissions for the Gunicorn log directory 4 | mkdir -p /var/log/gunicorn 5 | chown -R www-data:www-data /var/log/gunicorn 6 | chmod 755 /var/log/gunicorn 7 | 8 | # Set ownership and permissions for the application directory 9 | chown -R www-data:www-data /var/python/openterminal 10 | chmod -R u+rwX,g+rX,o+rX /var/python/openterminal 11 | 12 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 |
4 |
5 |

Welcome to Open Terminal

6 |

Empowering Traders with Precision and Speed

7 | Learn More 8 |
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /routes/voice/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | voice_bp = Blueprint('voice', __name__) 4 | 5 | # Import routes after blueprint creation to avoid circular imports 6 | from .routes import voice_trading, voice_settings, transcribe 7 | 8 | # Register routes using decorators 9 | voice_bp.route('/')(voice_trading) 10 | voice_bp.route('/settings', methods=['GET', 'POST'])(voice_settings) 11 | voice_bp.route('/transcribe', methods=['POST'])(transcribe) 12 | -------------------------------------------------------------------------------- /routes/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth import auth_bp 2 | from .home import home_bp 3 | from .funds import funds_bp 4 | from .books import books_bp 5 | from .dashboard import dashboard_bp 6 | from .orders import orders_bp 7 | from .voice import voice_bp 8 | from .scalper import scalper_bp 9 | 10 | __all__ = [ 11 | 'auth_bp', 12 | 'home_bp', 13 | 'funds_bp', 14 | 'books_bp', 15 | 'dashboard_bp', 16 | 'orders_bp', 17 | 'voice_bp', 18 | 'scalper_bp' 19 | ] 20 | -------------------------------------------------------------------------------- /templates/components/watchlist/_list.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {% if watchlist.items_list and watchlist.items_list|length > 0 %} 4 | {% for item in watchlist.items_list %} 5 | {% include 'watchlist/_item.html' %} 6 | {% endfor %} 7 | {% else %} 8 |
9 | No items in this watchlist 10 |
11 | {% endif %} 12 |
-------------------------------------------------------------------------------- /scheduler.py: -------------------------------------------------------------------------------- 1 | from app import create_app, schedule_task 2 | 3 | # Create the Flask application instance 4 | app = create_app() 5 | 6 | # Start the scheduler with the application context 7 | schedule_task(app) 8 | 9 | # Keep the script running 10 | try: 11 | # If you need to perform any periodic tasks within the app context, you can do so here 12 | app.logger.info("Scheduler started and running.") 13 | while True: 14 | pass # Keep the script alive 15 | except KeyboardInterrupt: 16 | pass 17 | -------------------------------------------------------------------------------- /production_files/openterminal.service: -------------------------------------------------------------------------------- 1 | # /etc/systemd/system/openterminal.service 2 | [Unit] 3 | Description=Gunicorn instance to serve OpenTerminal Flask app 4 | After=network.target 5 | 6 | [Service] 7 | User=www-data 8 | Group=www-data 9 | WorkingDirectory=/var/python/openterminal 10 | Environment="PATH=/var/python/venv/bin" 11 | 12 | ExecStart=/var/python/venv/bin/python -m gunicorn --workers 4 --bind unix:/var/python/openterminal/openterminal.sock "app:create_app()" 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /triggerdb.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from extensions import db 3 | from master_contract import download_and_store_json 4 | from config import Config 5 | 6 | # Create a Flask application with configuration 7 | def create_app(): 8 | app = Flask(__name__) 9 | app.config.from_object(Config) 10 | 11 | # Initialize extensions 12 | db.init_app(app) 13 | 14 | return app 15 | 16 | if __name__ == '__main__': 17 | # Set up the app context 18 | app = create_app() 19 | 20 | # Trigger the master contract download manually 21 | with app.app_context(): 22 | download_and_store_json(app) 23 | 24 | print("Master Contract download manually triggered.") 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Python 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | __pycache__/ 7 | *.env 8 | *.venv 9 | *.vscode 10 | 11 | # Flask files 12 | instance/ 13 | *.sqlite3 14 | *.db 15 | *.sqlite 16 | 17 | # Environment variables 18 | .env 19 | 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ 22 | *.py[cod] 23 | *$py.class 24 | 25 | # Unit test / coverage reports 26 | htmlcov/ 27 | .tox/ 28 | .coverage 29 | .cache 30 | nosetests.xml 31 | coverage.xml 32 | *.cover 33 | 34 | # Distribution / packaging 35 | .Python 36 | build/ 37 | develop-eggs/ 38 | dist/ 39 | downloads/ 40 | eggs/ 41 | .eggs/ 42 | lib/ 43 | lib64/ 44 | parts/ 45 | sdist/ 46 | var/ 47 | wheels/ 48 | *.egg-info/ 49 | .installed.cfg 50 | *.egg 51 | 52 | # Virtual environments 53 | venv/ 54 | ENV/ 55 | env/ 56 | env.bak/ 57 | venv.bak/ 58 | 59 | # IDE and Editor files 60 | .idea/ 61 | .vscode/ 62 | -------------------------------------------------------------------------------- /routes/orders/utils/formatters.py: -------------------------------------------------------------------------------- 1 | # routes/orders/utils/formatters.py 2 | from typing import Dict, Any 3 | from datetime import datetime 4 | 5 | def format_order_response(response: Dict) -> Dict: 6 | """Format broker response for client""" 7 | try: 8 | return { 9 | 'status': response.get('status'), 10 | 'order_id': response.get('data', {}).get('orderid'), 11 | 'message': response.get('message'), 12 | 'timestamp': datetime.now().isoformat() 13 | } 14 | except Exception as e: 15 | return { 16 | 'status': 'error', 17 | 'message': str(e), 18 | 'timestamp': datetime.now().isoformat() 19 | } 20 | 21 | def format_price(price: Any) -> str: 22 | """Format price for display""" 23 | try: 24 | return f"{float(price):.2f}" 25 | except (ValueError, TypeError): 26 | return "0.00" 27 | 28 | def format_quantity(quantity: Any) -> str: 29 | """Format quantity for display""" 30 | try: 31 | return str(int(quantity)) 32 | except (ValueError, TypeError): 33 | return "0" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.6.2.post1 3 | APScheduler==3.10.4 4 | asgiref==3.8.1 5 | blinker==1.8.2 6 | cachelib==0.13.0 7 | certifi==2024.8.30 8 | charset-normalizer==3.4.0 9 | click==8.1.7 10 | colorama==0.4.6 11 | distro==1.9.0 12 | dnspython==2.7.0 13 | eventlet==0.37.0 14 | Flask==3.0.3 15 | Flask-Cors==5.0.0 16 | Flask-Login==0.6.3 17 | Flask-Session==0.8.0 18 | Flask-SQLAlchemy==3.1.1 19 | flask-talisman==1.1.0 20 | Flask-WTF==1.2.2 21 | gevent==24.10.3 22 | greenlet==3.1.1 23 | groq==0.12.0 24 | gunicorn==23.0.0 25 | h11==0.14.0 26 | httpcore==1.0.7 27 | httpx==0.27.2 28 | idna==3.10 29 | itsdangerous==2.2.0 30 | Jinja2==3.1.4 31 | MarkupSafe==2.1.5 32 | msgspec==0.18.6 33 | packaging==24.2 34 | pydantic==2.10.1 35 | pydantic_core==2.27.1 36 | pytz==2024.2 37 | ratelimit==2.2.1 38 | redis==5.1.1 39 | requests==2.32.3 40 | setuptools==75.3.0 41 | six==1.16.0 42 | sniffio==1.3.1 43 | SQLAlchemy==2.0.35 44 | typing_extensions==4.12.2 45 | tzdata==2024.2 46 | tzlocal==5.2 47 | urllib3==2.2.3 48 | Werkzeug==3.0.4 49 | word2number==1.1 50 | WTForms==3.2.1 51 | zope.event==5.0 52 | zope.interface==7.1.1 53 | -------------------------------------------------------------------------------- /routes/orders/constants.py: -------------------------------------------------------------------------------- 1 | # routes/orders/constants.py 2 | 3 | # Order Types 4 | REGULAR = "NORMAL" 5 | STOPLOSS = "STOPLOSS" 6 | COVER = "COVER" 7 | AMO = "AMO" 8 | 9 | # Order Varieties 10 | INTRADAY = "INTRADAY" 11 | DELIVERY = "DELIVERY" 12 | CARRYFORWARD = "CARRYFORWARD" 13 | 14 | # Price Types 15 | MARKET = "MARKET" 16 | LIMIT = "LIMIT" 17 | SL_MARKET = "STOPLOSS_MARKET" 18 | SL_LIMIT = "STOPLOSS_LIMIT" 19 | 20 | # Exchange Segments 21 | EQUITY_SEGMENTS = ["NSE", "BSE"] 22 | DERIVATIVE_SEGMENTS = ["NFO", "BFO", "MCX", "CDS"] 23 | 24 | # Order Duration 25 | DAY = "DAY" 26 | IOC = "IOC" 27 | 28 | # Order Side 29 | BUY = "BUY" 30 | SELL = "SELL" 31 | 32 | # API Endpoints 33 | PLACE_ORDER_ENDPOINT = "/rest/secure/angelbroking/order/v1/placeOrder" 34 | MODIFY_ORDER_ENDPOINT = "/rest/secure/angelbroking/order/v1/modifyOrder" 35 | CANCEL_ORDER_ENDPOINT = "/rest/secure/angelbroking/order/v1/cancelOrder" 36 | 37 | # Error Codes 38 | ERROR_INVALID_QUANTITY = "E001" 39 | ERROR_INVALID_PRICE = "E002" 40 | ERROR_INVALID_TRIGGER = "E003" 41 | ERROR_SESSION_EXPIRED = "E004" 42 | ERROR_MARKET_CLOSED = "E005" 43 | ERROR_INVALID_SYMBOL = "E006" -------------------------------------------------------------------------------- /static/js/modules/templates/marketDepth.html: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | import redis # Import redis module 4 | 5 | class Config: 6 | # Broker API configurations 7 | BROKER_API_URL = os.environ.get('BROKER_API_URL', 'https://apiconnect.angelone.in') 8 | BROKER_API_VERSION = 'v1' 9 | WEBSOCKET_URL = os.environ.get('WEBSOCKET_URL', 'wss://smartapisocket.angelone.in/smart-stream') 10 | 11 | # Order related configurations 12 | MAX_ORDER_VALUE = 10000000 # 1 Crore 13 | MIN_ORDER_VALUE = 0 14 | MAX_QUANTITY = 500000 15 | DEFAULT_TICK_SIZE = 0.05 16 | 17 | # Database configuration 18 | SQLALCHEMY_DATABASE_URI = 'sqlite:///open_terminal.db' 19 | SQLALCHEMY_TRACK_MODIFICATIONS = False 20 | 21 | # Secret key for CSRF protection and session signing 22 | SECRET_KEY = '45ggd3rf3dhgr456gtygx45dftrg' # Ensure this is a securely generated key 23 | 24 | # Session and cookie settings 25 | SESSION_COOKIE_SECURE = True 26 | SESSION_COOKIE_HTTPONLY = True 27 | SESSION_COOKIE_SAMESITE = 'Lax' 28 | 29 | # Server-side session configuration using Redis 30 | SESSION_TYPE = 'redis' 31 | SESSION_REDIS = redis.StrictRedis( 32 | host='localhost', 33 | port=6379, 34 | db=0 35 | # No password parameter since you don't have a Redis password 36 | ) 37 | SESSION_PERMANENT = False 38 | PERMANENT_SESSION_LIFETIME = timedelta(hours=1) 39 | 40 | # (Optional) CSRF protection settings 41 | WTF_CSRF_TIME_LIMIT = None # Disable CSRF token expiration if needed 42 | WTF_CSRF_ENABLED = True 43 | 44 | -------------------------------------------------------------------------------- /templates/components/watchlist/_market_depth.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /routes/logs.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, jsonify, session, redirect, url_for, flash 2 | from models import User, OrderLog 3 | from extensions import db 4 | 5 | logs_bp = Blueprint('logs', __name__) 6 | 7 | @logs_bp.route('/') 8 | def order_logs(): 9 | """Display order logs""" 10 | if 'client_id' not in session: 11 | flash('Please login to access logs.', 'warning') 12 | return redirect(url_for('auth.login')) 13 | 14 | user = User.query.filter_by(client_id=session['client_id']).first() 15 | if not user: 16 | flash('User not found.', 'error') 17 | return redirect(url_for('auth.login')) 18 | 19 | # Get all orders for the user 20 | orders = OrderLog.query.filter_by( 21 | user_id=user.id 22 | ).order_by(OrderLog.timestamp.desc()).all() 23 | 24 | # Group orders by source 25 | grouped_orders = {} 26 | for order in orders: 27 | source = order.order_source 28 | if source not in grouped_orders: 29 | grouped_orders[source] = [] 30 | grouped_orders[source].append(order) 31 | 32 | # Calculate statistics 33 | stats = { 34 | 'total': len(orders), 35 | 'by_source': {}, 36 | 'by_type': { 37 | 'BUY': len([o for o in orders if o.transaction_type == 'BUY']), 38 | 'SELL': len([o for o in orders if o.transaction_type == 'SELL']) 39 | } 40 | } 41 | 42 | # Calculate stats by source 43 | for source in grouped_orders: 44 | source_orders = grouped_orders[source] 45 | stats['by_source'][source] = { 46 | 'total': len(source_orders), 47 | 'BUY': len([o for o in source_orders if o.transaction_type == 'BUY']), 48 | 'SELL': len([o for o in source_orders if o.transaction_type == 'SELL']) 49 | } 50 | 51 | return render_template('logs.html', 52 | orders=grouped_orders, 53 | stats=stats, 54 | title='Order Logs') 55 | -------------------------------------------------------------------------------- /static/js/modules/orderEntry/utils/formatters.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/orderEntry/utils/formatters.js 2 | var OrderFormatters = (function() { 3 | function formatPrice(price) { 4 | if (!price || isNaN(price)) return '0.00'; 5 | return parseFloat(price).toFixed(2); 6 | } 7 | 8 | function formatQuantity(quantity) { 9 | if (!quantity || isNaN(quantity)) return '0'; 10 | return parseInt(quantity).toString(); 11 | } 12 | 13 | function formatPriceChange(change, changePercent) { 14 | if (!change || isNaN(change)) return ''; 15 | 16 | var formattedChange = parseFloat(change).toFixed(2); 17 | var formattedPercent = parseFloat(changePercent).toFixed(2); 18 | var sign = change >= 0 ? '+' : ''; 19 | 20 | return sign + formattedChange + ' (' + sign + formattedPercent + '%)'; 21 | } 22 | 23 | function formatOrderParams(orderData) { 24 | return { 25 | variety: orderData.variety || 'NORMAL', 26 | tradingsymbol: orderData.tradingSymbol, 27 | symboltoken: orderData.symbolToken, 28 | transactiontype: orderData.side, 29 | exchange: orderData.exchange, 30 | ordertype: orderData.orderType, 31 | producttype: orderData.productType, 32 | duration: 'DAY', 33 | price: formatPrice(orderData.price), 34 | triggerprice: orderData.triggerPrice ? formatPrice(orderData.triggerPrice) : '0', 35 | quantity: formatQuantity(orderData.quantity) 36 | }; 37 | } 38 | 39 | function formatDate(date) { 40 | if (!date) return ''; 41 | var d = new Date(date); 42 | return d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); 43 | } 44 | 45 | return { 46 | formatPrice: formatPrice, 47 | formatQuantity: formatQuantity, 48 | formatPriceChange: formatPriceChange, 49 | formatOrderParams: formatOrderParams, 50 | formatDate: formatDate 51 | }; 52 | })(); -------------------------------------------------------------------------------- /static/js/modules/orderEntry/services/orderApi.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/orderEntry/services/orderApi.js 2 | var OrderAPI = (function() { 3 | var baseUrl = '/api/orders'; 4 | var csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); 5 | 6 | function placeOrder(orderData) { 7 | return fetch(baseUrl + '/place', { 8 | method: 'POST', 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | 'X-CSRFToken': csrfToken 12 | }, 13 | body: JSON.stringify(orderData) 14 | }) 15 | .then(handleResponse) 16 | .catch(handleError); 17 | } 18 | 19 | function modifyOrder(orderId, modifications) { 20 | return fetch(baseUrl + '/modify/' + orderId, { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | 'X-CSRFToken': csrfToken 25 | }, 26 | body: JSON.stringify(modifications) 27 | }) 28 | .then(handleResponse) 29 | .catch(handleError); 30 | } 31 | 32 | function cancelOrder(orderId) { 33 | return fetch(baseUrl + '/cancel/' + orderId, { 34 | method: 'POST', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | 'X-CSRFToken': csrfToken 38 | } 39 | }) 40 | .then(handleResponse) 41 | .catch(handleError); 42 | } 43 | 44 | function handleResponse(response) { 45 | if (!response.ok) { 46 | return response.json().then(function(data) { 47 | throw new Error(data.message || 'Server error'); 48 | }); 49 | } 50 | return response.json(); 51 | } 52 | 53 | function handleError(error) { 54 | console.error('API Error:', error); 55 | throw error; 56 | } 57 | 58 | return { 59 | placeOrder: placeOrder, 60 | modifyOrder: modifyOrder, 61 | cancelOrder: cancelOrder 62 | }; 63 | })(); -------------------------------------------------------------------------------- /routes/orders/utils/helpers.py: -------------------------------------------------------------------------------- 1 | # routes/orders/utils/helpers.py 2 | from typing import Dict, Optional 3 | import json 4 | from datetime import datetime 5 | from extensions import redis_client 6 | 7 | def calculate_order_value(quantity: int, price: float) -> float: 8 | """Calculate total order value""" 9 | return quantity * price 10 | 11 | def is_market_open() -> bool: 12 | """Check if market is currently open""" 13 | # Implement market timing logic 14 | now = datetime.now() 15 | # Example: Market hours 9:15 AM to 3:30 PM 16 | market_start = now.replace(hour=0, minute=00, second=0) 17 | market_end = now.replace(hour=23, minute=59, second=0) 18 | return market_start <= now <= market_end 19 | 20 | def get_cached_token(user_id: str) -> Optional[Dict]: 21 | """Get cached auth token from Redis""" 22 | try: 23 | token_data = redis_client.get(f"auth_token:{user_id}") 24 | return json.loads(token_data) if token_data else None 25 | except Exception: 26 | return None 27 | 28 | def format_log_message( 29 | action: str, 30 | user_id: str, 31 | order_data: Dict, 32 | status: str, 33 | error: Optional[str] = None 34 | ) -> Dict: 35 | """Format log message for order actions""" 36 | return { 37 | 'timestamp': datetime.now().isoformat(), 38 | 'action': action, 39 | 'user_id': user_id, 40 | 'order_data': order_data, 41 | 'status': status, 42 | 'error': error 43 | } 44 | 45 | 46 | def handle_broker_error(error_response): 47 | """Handle broker API error responses""" 48 | error_codes = { 49 | 'E001': 'Invalid quantity', 50 | 'E002': 'Invalid price', 51 | 'E003': 'Invalid trigger price', 52 | 'E004': 'Session expired', 53 | 'E005': 'Market closed', 54 | # Add more error codes as needed 55 | } 56 | 57 | error_code = error_response.get('errorcode', 'UNKNOWN') 58 | return { 59 | 'status': 'error', 60 | 'code': error_code, 61 | 'message': error_codes.get(error_code, 'Unknown error occurred') 62 | } -------------------------------------------------------------------------------- /templates/components/orders/_market_depth.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |
O: --
6 |
H: --
7 |
L: --
8 |
C: --
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for i in range(5) %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% endfor %} 34 | 35 |
QtyOrdersBidAskOrdersQty
------------
36 | 37 | 38 |
39 |
Vol: --
40 |
41 | Buy: -- | 42 | Sell: -- 43 |
44 |
45 |
-------------------------------------------------------------------------------- /routes/dashboard/settings_service.py: -------------------------------------------------------------------------------- 1 | from extensions import redis_client 2 | from .utils import cache_key, get_cached_data, set_cached_data 3 | 4 | class SettingsService: 5 | def get_user_settings(self, user_id): 6 | """Get user's watchlist display settings""" 7 | settings_key = cache_key('user', f'{user_id}:watchlist_settings') 8 | settings = redis_client.hgetall(settings_key) 9 | 10 | if not settings: 11 | settings = self._set_default_settings(user_id) 12 | 13 | return settings 14 | 15 | def update_settings(self, user_id, settings_data): 16 | """Update user's watchlist settings""" 17 | try: 18 | settings_key = cache_key('user', f'{user_id}:watchlist_settings') 19 | settings = { 20 | 'show_ltp_change': str(settings_data.get('show_ltp_change', True)).lower(), 21 | 'show_ltp_change_percent': str(settings_data.get('show_ltp_change_percent', True)).lower(), 22 | 'show_holdings': str(settings_data.get('show_holdings', True)).lower() 23 | } 24 | 25 | redis_client.hmset(settings_key, settings) 26 | return {'status': 'success'} 27 | 28 | except Exception as e: 29 | return {'error': str(e), 'code': 500} 30 | 31 | def _set_default_settings(self, user_id): 32 | """Set and return default settings""" 33 | default_settings = { 34 | 'show_ltp_change': 'true', 35 | 'show_ltp_change_percent': 'true', 36 | 'show_holdings': 'true' 37 | } 38 | 39 | settings_key = cache_key('user', f'{user_id}:watchlist_settings') 40 | redis_client.hmset(settings_key, default_settings) 41 | return default_settings 42 | 43 | def cleanup_settings(self, user_id): 44 | """Remove user settings from cache""" 45 | try: 46 | settings_key = cache_key('user', f'{user_id}:watchlist_settings') 47 | redis_client.delete(settings_key) 48 | return True 49 | except Exception: 50 | return False -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from extensions import db 3 | from routes import ( 4 | auth_bp, home_bp, funds_bp, books_bp, 5 | dashboard_bp, orders_bp, voice_bp, scalper_bp 6 | ) 7 | from routes.logs import logs_bp # Add logs blueprint 8 | from apscheduler.schedulers.background import BackgroundScheduler 9 | from master_contract import download_and_store_json 10 | import pytz 11 | from flask_wtf.csrf import CSRFProtect 12 | from flask_session import Session 13 | from werkzeug.middleware.proxy_fix import ProxyFix 14 | import logging 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | def create_app(): 19 | app = Flask(__name__, static_folder='static') 20 | app.config.from_object('config.Config') 21 | 22 | # Apply ProxyFix middleware 23 | app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1) 24 | 25 | # Initialize extensions 26 | Session(app) 27 | db.init_app(app) 28 | csrf = CSRFProtect(app) 29 | 30 | # Register blueprints 31 | blueprints = [ 32 | (auth_bp, None), 33 | (dashboard_bp, None), 34 | (home_bp, None), 35 | (funds_bp, None), 36 | (books_bp, None), 37 | (orders_bp, '/api'), 38 | (voice_bp, '/voice'), 39 | (scalper_bp, '/scalper'), 40 | (logs_bp, '/logs'), # Add logs route 41 | ] 42 | 43 | for blueprint, url_prefix in blueprints: 44 | app.register_blueprint(blueprint, url_prefix=url_prefix) 45 | 46 | # Initialize the database 47 | with app.app_context(): 48 | db.create_all() 49 | 50 | return app 51 | 52 | def schedule_task(app): 53 | with app.app_context(): 54 | scheduler = BackgroundScheduler() 55 | ist = pytz.timezone('Asia/Kolkata') 56 | scheduler.add_job( 57 | func=lambda: download_and_store_json(app), 58 | trigger='cron', 59 | hour=12, 60 | minute=28, 61 | timezone=ist 62 | ) 63 | scheduler.start() 64 | app.logger.info("Scheduler started.") 65 | 66 | if __name__ == "__main__": 67 | app = create_app() 68 | # Do not start the scheduler here in production deployment 69 | schedule_task(app) 70 | app.run(debug=True) 71 | -------------------------------------------------------------------------------- /production_files/openterminal_nginx: -------------------------------------------------------------------------------- 1 | # HTTP server configuration to redirect all HTTP traffic to HTTPS 2 | server { 3 | listen 80; 4 | listen [::]:80; 5 | server_name terminal.marketcalls.in; 6 | 7 | # Redirect all HTTP requests to HTTPS 8 | return 301 https://$host$request_uri; 9 | } 10 | 11 | # HTTPS server configuration for secure connections 12 | server { 13 | listen 443 ssl; 14 | listen [::]:443 ssl; 15 | server_name terminal.marketcalls.in; 16 | 17 | # SSL certificates 18 | ssl_certificate /etc/letsencrypt/live/terminal.marketcalls.in/fullchain.pem; 19 | ssl_certificate_key /etc/letsencrypt/live/terminal.marketcalls.in/privkey.pem; 20 | include /etc/letsencrypt/options-ssl-nginx.conf; 21 | 22 | # Security headers 23 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; 24 | add_header X-Content-Type-Options nosniff; 25 | add_header X-Frame-Options DENY; 26 | add_header X-XSS-Protection "1; mode=block"; 27 | 28 | # Serve static files directly with caching 29 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { 30 | root /var/python/openterminal; # Adjust this path if needed 31 | expires 30d; 32 | add_header Cache-Control "public, no-transform"; 33 | } 34 | 35 | # Alias for /static/ route in Flask 36 | location /static/ { 37 | alias /var/python/openterminal/static/; # Adjust this path if needed 38 | expires 30d; 39 | add_header Cache-Control "public, no-transform"; 40 | } 41 | 42 | # Main Flask app route, proxied to Gunicorn through the .sock file 43 | location / { 44 | proxy_pass http://unix:/var/python/openterminal/openterminal.sock; 45 | proxy_set_header Host $host; 46 | proxy_set_header X-Real-IP $remote_addr; 47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 48 | proxy_set_header X-Forwarded-Proto $scheme; 49 | } 50 | 51 | # WebSocket handling (if applicable in your app) 52 | location /socket.io { 53 | proxy_http_version 1.1; 54 | proxy_buffering off; 55 | proxy_set_header Upgrade $http_upgrade; 56 | proxy_set_header Connection "upgrade"; 57 | proxy_pass http://unix:/var/python/openterminal/openterminal.sock; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /routes/orders/routes.py: -------------------------------------------------------------------------------- 1 | # routes/orders/routes.py 2 | from flask import request, jsonify, session 3 | from . import orders_bp # Import the blueprint from __init__.py 4 | from .services.order_service import OrderService 5 | from .utils.helpers import is_market_open, format_log_message 6 | import logging 7 | 8 | order_service = OrderService() 9 | logger = logging.getLogger('orders') 10 | 11 | @orders_bp.route('/orders/place', methods=['POST']) 12 | async def place_order(): 13 | """Place a new order""" 14 | try: 15 | # Check if market is open 16 | if not is_market_open(): 17 | return jsonify({ 18 | 'status': 'error', 19 | 'message': 'Market is closed' 20 | }), 400 21 | 22 | # Get user ID from session 23 | user_id = session['client_id'] # Implement your auth logic 24 | 25 | # Get order data 26 | order_data = request.get_json() 27 | 28 | # Log order request 29 | logger.info(format_log_message( 30 | 'PLACE_ORDER_REQUEST', 31 | user_id, 32 | order_data, 33 | 'PENDING' 34 | )) 35 | 36 | # Place order 37 | response = await order_service.place_order(user_id, order_data) 38 | 39 | # Log success 40 | logger.info(format_log_message( 41 | 'PLACE_ORDER_SUCCESS', 42 | user_id, 43 | order_data, 44 | 'SUCCESS' 45 | )) 46 | 47 | return jsonify(response) 48 | 49 | except ValueError as e: 50 | # Log validation error 51 | logger.warning(format_log_message( 52 | 'PLACE_ORDER_VALIDATION_ERROR', 53 | user_id, 54 | order_data, 55 | 'ERROR', 56 | str(e) 57 | )) 58 | return jsonify({ 59 | 'status': 'error', 60 | 'message': str(e) 61 | }), 400 62 | 63 | except Exception as e: 64 | # Log unexpected error 65 | logger.error(format_log_message( 66 | 'PLACE_ORDER_ERROR', 67 | user_id, 68 | order_data, 69 | 'ERROR', 70 | str(e) 71 | )) 72 | return jsonify({ 73 | 'status': 'error', 74 | 'message': 'Internal server error' 75 | }), 500 76 | 77 | # Add more routes as needed (modify, cancel, etc.) -------------------------------------------------------------------------------- /routes/orders/services/market_feed.py: -------------------------------------------------------------------------------- 1 | # routes/orders/services/market_feed.py 2 | from typing import Dict, Optional 3 | import http.client 4 | import json 5 | from extensions import redis_client 6 | 7 | class MarketFeedService: 8 | def __init__(self): 9 | self.base_url = "apiconnect.angelone.in" 10 | 11 | async def get_ltp(self, token: str, exchange: str) -> Optional[float]: 12 | """Get Last Traded Price for a symbol""" 13 | try: 14 | # Try getting from Redis first 15 | redis_key = f"ltp:{exchange}:{token}" 16 | ltp = redis_client.get(redis_key) 17 | if ltp: 18 | return float(ltp) 19 | 20 | # If not in Redis, fetch from API 21 | data = await self._fetch_market_data(token, exchange) 22 | if data and 'ltp' in data: 23 | # Cache in Redis 24 | redis_client.setex(redis_key, 1, str(data['ltp'])) 25 | return float(data['ltp']) 26 | 27 | return None 28 | 29 | except Exception as e: 30 | print(f"Error fetching LTP: {str(e)}") 31 | return None 32 | 33 | async def _fetch_market_data(self, token: str, exchange: str) -> Dict: 34 | """Fetch market data from Angel One API""" 35 | try: 36 | conn = http.client.HTTPSConnection(self.base_url) 37 | 38 | payload = json.dumps({ 39 | "mode": "OHLC", 40 | "exchangeTokens": { 41 | exchange: [token] 42 | } 43 | }) 44 | 45 | # Get auth token from Redis/DB 46 | # This is simplified - you'll need proper token management 47 | headers = self._get_headers() 48 | 49 | conn.request("POST", "/rest/secure/angelbroking/market/v1/quote/", 50 | payload, headers) 51 | 52 | response = conn.getresponse() 53 | return json.loads(response.read().decode("utf-8")) 54 | 55 | except Exception as e: 56 | print(f"Error fetching market data: {str(e)}") 57 | return {} 58 | 59 | def _get_headers(self) -> Dict: 60 | """Get headers for market data API""" 61 | # Implement proper token management 62 | return { 63 | 'Content-Type': 'application/json', 64 | 'Accept': 'application/json', 65 | 'X-UserType': 'USER', 66 | 'X-SourceID': 'WEB' 67 | } -------------------------------------------------------------------------------- /templates/components/watchlist/_item.html: -------------------------------------------------------------------------------- 1 | 2 |
  • 11 | 12 | 13 |
    15 |
    16 | {{ item.symbol }} 17 | {{ item.exch_seg }} 18 |
    19 |
    20 | 21 |
    22 |
    --
    23 |
    24 | 0.00 25 | (0.00%) 26 |
    27 |
    28 | 29 | 33 | 40 |
    41 |
    42 | 43 | 44 | {% include 'watchlist/_market_depth.html' %} 45 |
  • -------------------------------------------------------------------------------- /routes/funds.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, session, flash, redirect, url_for 2 | from extensions import db, redis_client 3 | from models import User 4 | import http.client 5 | import json 6 | 7 | # Define the blueprint for funds-related routes 8 | funds_bp = Blueprint('funds', __name__) 9 | 10 | # Helper function to retrieve the auth token 11 | def get_auth_token(user): 12 | auth_token = redis_client.hget(f"user:{user.client_id}", "access_token") 13 | print("Getting Access Token from Redis") 14 | if not auth_token: 15 | # Fetch from the database if not in Redis 16 | auth_token = user.access_token 17 | print("Getting Access Token from Sqlite DB") 18 | return auth_token 19 | 20 | # Route to display fund details 21 | @funds_bp.route('/funds') 22 | def funds(): 23 | client_id = session.get('client_id') 24 | if not client_id: 25 | flash('Please login to access fund details.', 'danger') 26 | return redirect(url_for('auth.login')) 27 | 28 | # Find the user in the database 29 | user = User.query.filter_by(client_id=client_id).first() 30 | if not user: 31 | flash('User not found.', 'danger') 32 | return redirect(url_for('auth.login')) 33 | 34 | auth_token = get_auth_token(user) 35 | if not auth_token: 36 | flash('Authentication token not found.', 'danger') 37 | return redirect(url_for('auth.login')) 38 | 39 | try: 40 | # Make the API request to fetch fund details 41 | conn = http.client.HTTPSConnection("apiconnect.angelone.in") 42 | headers = { 43 | 'Authorization': f'Bearer {auth_token}', 44 | 'Content-Type': 'application/json', 45 | 'Accept': 'application/json', 46 | 'X-UserType': 'USER', 47 | 'X-SourceID': 'WEB', 48 | 'X-ClientLocalIP': 'CLIENT_LOCAL_IP', 49 | 'X-ClientPublicIP': 'CLIENT_PUBLIC_IP', 50 | 'X-MACAddress': 'MAC_ADDRESS', 51 | 'X-PrivateKey': user.api_key 52 | } 53 | conn.request("GET", "/rest/secure/angelbroking/user/v1/getRMS", '', headers) 54 | res = conn.getresponse() 55 | data = res.read() 56 | response_json = json.loads(data.decode("utf-8")) 57 | 58 | if response_json.get('status') and response_json['status'] == True: 59 | fund_data = response_json['data'] 60 | return render_template('funds.html', fund_data=fund_data) 61 | else: 62 | flash('Failed to retrieve fund details.', 'danger') 63 | 64 | except Exception as e: 65 | flash(f'An error occurred: {str(e)}', 'danger') 66 | 67 | return redirect(url_for('dashboard.dashboard')) 68 | -------------------------------------------------------------------------------- /static/js/modules/orderEntry/components/OrderForm.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/orderEntry/components/OrderForm.js 2 | var OrderForm = (function() { 3 | var formElement = null; 4 | var callbacks = {}; 5 | 6 | function init(element, options) { 7 | formElement = element; 8 | callbacks = options.callbacks || {}; 9 | setupEventListeners(); 10 | } 11 | 12 | function setupEventListeners() { 13 | if (!formElement) return; 14 | 15 | formElement.addEventListener('submit', handleSubmit); 16 | 17 | // Product type change 18 | var productTypes = formElement.querySelectorAll('[name="producttype"]'); 19 | productTypes.forEach(function(elem) { 20 | elem.addEventListener('change', handleProductTypeChange); 21 | }); 22 | 23 | // Order type change 24 | var orderTypes = formElement.querySelectorAll('[name="ordertype"]'); 25 | orderTypes.forEach(function(elem) { 26 | elem.addEventListener('change', handleOrderTypeChange); 27 | }); 28 | } 29 | 30 | function handleSubmit(event) { 31 | event.preventDefault(); 32 | 33 | var formData = { 34 | symbol: formElement.querySelector('[name="symbol"]').value, 35 | exchange: formElement.querySelector('[name="exchange"]').value, 36 | quantity: formElement.querySelector('[name="quantity"]').value, 37 | price: formElement.querySelector('[name="price"]').value, 38 | ordertype: formElement.querySelector('[name="ordertype"]:checked').value, 39 | producttype: formElement.querySelector('[name="producttype"]:checked').value, 40 | tradingsymbol: formElement.querySelector('[name="tradingsymbol"]').value, 41 | symboltoken: formElement.querySelector('[name="symboltoken"]').value 42 | }; 43 | 44 | if (callbacks.onSubmit) { 45 | callbacks.onSubmit(formData); 46 | } 47 | } 48 | 49 | function handleProductTypeChange(event) { 50 | if (callbacks.onProductTypeChange) { 51 | callbacks.onProductTypeChange(event.target.value); 52 | } 53 | } 54 | 55 | function handleOrderTypeChange(event) { 56 | var isMarket = event.target.value === 'MARKET'; 57 | var priceInput = formElement.querySelector('[name="price"]'); 58 | if (priceInput) { 59 | priceInput.disabled = isMarket; 60 | } 61 | 62 | if (callbacks.onOrderTypeChange) { 63 | callbacks.onOrderTypeChange(event.target.value); 64 | } 65 | } 66 | 67 | function reset() { 68 | if (formElement) { 69 | formElement.reset(); 70 | } 71 | } 72 | 73 | return { 74 | init: init, 75 | reset: reset 76 | }; 77 | })(); -------------------------------------------------------------------------------- /static/js/modules/orderEntry/utils/validators.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/orderEntry/utils/validators.js 2 | var OrderValidators = (function() { 3 | function validateQuantity(quantity, lotSize) { 4 | quantity = parseInt(quantity, 10); 5 | lotSize = parseInt(lotSize, 10) || 1; 6 | 7 | if (isNaN(quantity) || quantity <= 0) { 8 | return { 9 | isValid: false, 10 | message: 'Quantity must be greater than 0' 11 | }; 12 | } 13 | 14 | if (quantity % lotSize !== 0) { 15 | return { 16 | isValid: false, 17 | message: 'Quantity must be multiple of lot size: ' + lotSize 18 | }; 19 | } 20 | 21 | return { 22 | isValid: true, 23 | message: '' 24 | }; 25 | } 26 | 27 | function validatePrice(price, ltp, tickSize) { 28 | price = parseFloat(price); 29 | ltp = parseFloat(ltp); 30 | tickSize = parseFloat(tickSize) || 0.05; 31 | 32 | if (isNaN(price) || price <= 0) { 33 | return { 34 | isValid: false, 35 | message: 'Invalid price' 36 | }; 37 | } 38 | 39 | if (price % tickSize !== 0) { 40 | return { 41 | isValid: false, 42 | message: 'Price must be multiple of tick size: ' + tickSize 43 | }; 44 | } 45 | 46 | // Optional: Add price band validation 47 | // var upperLimit = ltp * 1.20; // 20% above LTP 48 | // var lowerLimit = ltp * 0.80; // 20% below LTP 49 | 50 | return { 51 | isValid: true, 52 | message: '' 53 | }; 54 | } 55 | 56 | function validateTriggerPrice(triggerPrice, price, orderType) { 57 | triggerPrice = parseFloat(triggerPrice); 58 | price = parseFloat(price); 59 | 60 | if (orderType !== 'STOPLOSS') { 61 | return { 62 | isValid: true, 63 | message: '' 64 | }; 65 | } 66 | 67 | if (isNaN(triggerPrice) || triggerPrice <= 0) { 68 | return { 69 | isValid: false, 70 | message: 'Invalid trigger price' 71 | }; 72 | } 73 | 74 | if (triggerPrice >= price) { 75 | return { 76 | isValid: false, 77 | message: 'Trigger price must be less than order price' 78 | }; 79 | } 80 | 81 | return { 82 | isValid: true, 83 | message: '' 84 | }; 85 | } 86 | 87 | function validateProductType(productType, exchange) { 88 | var validTypes = ['INTRADAY', 'DELIVERY']; 89 | if (['NFO', 'MCX', 'CDS'].includes(exchange)) { 90 | validTypes = ['INTRADAY', 'CARRYFORWARD']; 91 | } 92 | 93 | return { 94 | isValid: validTypes.includes(productType), 95 | message: validTypes.includes(productType) ? '' : 'Invalid product type' 96 | }; 97 | } 98 | 99 | return { 100 | validateQuantity: validateQuantity, 101 | validatePrice: validatePrice, 102 | validateTriggerPrice: validateTriggerPrice, 103 | validateProductType: validateProductType 104 | }; 105 | })(); -------------------------------------------------------------------------------- /static/js/modules/orderEntry/components/MarketDepth.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/orderEntry/components/MarketDepth.js 2 | var MarketDepth = (function() { 3 | var container = null; 4 | var currentSymbol = null; 5 | var depthData = null; 6 | 7 | function init(element) { 8 | container = element; 9 | setupEventListeners(); 10 | } 11 | 12 | function setupEventListeners() { 13 | if (!container) return; 14 | 15 | container.addEventListener('click', function(event) { 16 | var priceElement = event.target.closest('.depth-price'); 17 | if (priceElement) { 18 | handlePriceClick(priceElement.dataset.price); 19 | } 20 | }); 21 | } 22 | 23 | function update(symbol, data) { 24 | currentSymbol = symbol; 25 | depthData = data; 26 | render(); 27 | } 28 | 29 | function render() { 30 | if (!container || !depthData) return; 31 | 32 | var html = '
    ' + 33 | '
    ' + 34 | '
    Qty
    ' + 35 | '
    Orders
    ' + 36 | '
    Bid
    ' + 37 | '
    Ask
    ' + 38 | '
    Orders
    ' + 39 | '
    Qty
    ' + 40 | '
    '; 41 | 42 | html += '
    '; 43 | 44 | // Assuming depthData has bids and asks arrays 45 | for (var i = 0; i < Math.max(depthData.bids.length, depthData.asks.length); i++) { 46 | var bid = depthData.bids[i] || { quantity: 0, price: 0, orders: 0 }; 47 | var ask = depthData.asks[i] || { quantity: 0, price: 0, orders: 0 }; 48 | 49 | html += '
    ' + 50 | '
    ' + formatters.formatQuantity(bid.quantity) + '
    ' + 51 | '
    ' + bid.orders + '
    ' + 52 | '
    ' + 53 | formatters.formatPrice(bid.price) + '
    ' + 54 | '
    ' + 55 | formatters.formatPrice(ask.price) + '
    ' + 56 | '
    ' + ask.orders + '
    ' + 57 | '
    ' + formatters.formatQuantity(ask.quantity) + '
    ' + 58 | '
    '; 59 | } 60 | 61 | html += '
    '; 62 | container.innerHTML = html; 63 | } 64 | 65 | function handlePriceClick(price) { 66 | // Dispatch event for price selection 67 | var event = new CustomEvent('depth-price-selected', { 68 | detail: { price: price } 69 | }); 70 | container.dispatchEvent(event); 71 | } 72 | 73 | function clear() { 74 | if (container) { 75 | container.innerHTML = ''; 76 | } 77 | currentSymbol = null; 78 | depthData = null; 79 | } 80 | 81 | return { 82 | init: init, 83 | update: update, 84 | clear: clear 85 | }; 86 | })(); -------------------------------------------------------------------------------- /routes/orders/services/broker_service.py: -------------------------------------------------------------------------------- 1 | # routes/orders/services/broker_service.py 2 | 3 | import http.client 4 | import json 5 | from typing import Dict 6 | from flask import request 7 | from ..constants import ( 8 | PLACE_ORDER_ENDPOINT, 9 | MODIFY_ORDER_ENDPOINT, 10 | CANCEL_ORDER_ENDPOINT 11 | ) 12 | 13 | class BrokerService: 14 | def __init__(self): 15 | self.base_url = "apiconnect.angelone.in" 16 | 17 | async def place_order(self, order_data: Dict, access_token: str, api_key: str) -> Dict: 18 | """Place order with Angel One broker""" 19 | try: 20 | conn = http.client.HTTPSConnection(self.base_url) 21 | 22 | # Prepare payload 23 | payload = self._prepare_order_payload(order_data) 24 | #print('Prepared payload:', payload) 25 | # Prepare headers with provided tokens 26 | headers = self._prepare_headers(access_token, api_key) 27 | #print('Prepared headers:', headers) 28 | # Send request 29 | conn.request("POST", PLACE_ORDER_ENDPOINT, payload, headers) 30 | 31 | # Get response 32 | response = conn.getresponse() 33 | data = response.read() 34 | return json.loads(data.decode("utf-8")) 35 | 36 | except Exception as e: 37 | print(f"Error in broker service: {str(e)}") 38 | raise 39 | 40 | def _prepare_order_payload(self, order_data: Dict) -> str: 41 | """Prepare order payload for Angel One API""" 42 | payload = { 43 | "variety": order_data.get("variety", "NORMAL"), 44 | "tradingsymbol": order_data["symbol"], 45 | "symboltoken": order_data["token"], 46 | "transactiontype": order_data["side"], 47 | "exchange": order_data["exchange"], 48 | "ordertype": order_data["ordertype"], 49 | "producttype": order_data["producttype"], 50 | "duration": order_data.get("duration", "DAY"), 51 | "price": str(order_data.get("price", "0")), 52 | "squareoff": "0", 53 | "stoploss": "0", 54 | "quantity": str(order_data["quantity"]), 55 | "disclosedquantity": str(order_data.get("disclosedquantity", "0")) 56 | } 57 | 58 | # Add stop loss specific fields 59 | if order_data.get("variety") == "STOPLOSS": 60 | payload["triggerprice"] = str(order_data.get("triggerprice", "0")) 61 | 62 | return json.dumps(payload) 63 | 64 | def _prepare_headers(self, access_token: str, api_key: str) -> Dict: 65 | """Prepare request headers with authentication""" 66 | 67 | return { 68 | 'Authorization': f'Bearer {access_token}', 69 | 'Content-Type': 'application/json', 70 | 'Accept': 'application/json', 71 | 'X-UserType': 'USER', 72 | 'X-SourceID': 'WEB', 73 | 'X-ClientLocalIP': 'CLIENT_LOCAL_IP', 74 | 'X-ClientPublicIP': 'CLIENT_PUBLIC_IP', 75 | 'X-MACAddress': 'MAC_ADDRESS', 76 | 'X-PrivateKey': api_key 77 | } 78 | 79 | async def modify_order(self, order_data: Dict, access_token: str, api_key: str) -> Dict: 80 | """Modify an existing order""" 81 | pass 82 | 83 | async def cancel_order(self, order_id: str, access_token: str, api_key: str) -> Dict: 84 | """Cancel an existing order""" 85 | pass -------------------------------------------------------------------------------- /static/js/modules/templates/symbolListItem.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 | ${symbol} 6 | ${exchSeg} 7 |
    8 |
    9 | -- 10 | 0.00 11 | (0.00%) 12 |
    13 |
    14 |
    15 | 16 | 24 | 25 | 32 |
    33 |
    34 | 35 | 36 | 71 |
    -------------------------------------------------------------------------------- /templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 |
    4 |
    5 |
    6 |

    Create Account

    7 | 8 | {% with messages = get_flashed_messages() %} 9 | {% if messages %} 10 | {% for message in messages %} 11 |
    12 | 13 | 14 | 15 | {{ message }} 16 |
    17 | {% endfor %} 18 | {% endif %} 19 | {% endwith %} 20 | 21 |
    22 | 23 | 24 |
    25 | 28 | 34 |
    35 | 36 |
    37 | 40 | 45 |
    46 | 47 |
    48 | 52 | 57 |
    58 | 59 |
    60 | 66 |
    67 | 68 |
    OR
    69 | 70 | 75 |
    76 |
    77 |
    78 |
    79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /static/js/theme.js: -------------------------------------------------------------------------------- 1 | // Theme Manager 2 | const ThemeManager = { 3 | init() { 4 | this.themeController = document.querySelector('.theme-controller'); 5 | this.themeIcons = { 6 | dark: document.querySelector('.theme-icon-dark'), 7 | light: document.querySelector('.theme-icon-light') 8 | }; 9 | this.transitionElement = document.querySelector('.theme-transition'); 10 | 11 | // Load theme in this order: localStorage > system preference > default (dark) 12 | this.loadTheme(); 13 | 14 | // Bind events 15 | this.bindEvents(); 16 | 17 | // Setup system theme listener 18 | this.setupSystemThemeListener(); 19 | }, 20 | 21 | loadTheme() { 22 | try { 23 | // First check localStorage 24 | const storedTheme = localStorage.getItem('theme'); 25 | 26 | if (storedTheme) { 27 | // Use stored theme if available 28 | this.setTheme(storedTheme); 29 | } else { 30 | // Check system preference 31 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 32 | const systemTheme = prefersDark ? 'dark' : 'light'; 33 | this.setTheme(systemTheme); 34 | } 35 | } catch (error) { 36 | console.error('Error loading theme:', error); 37 | // Fallback to dark theme 38 | this.setTheme('dark'); 39 | } 40 | }, 41 | 42 | setTheme(theme) { 43 | // Update HTML attribute 44 | document.documentElement.setAttribute('data-theme', theme); 45 | 46 | // Update toggle state 47 | if (this.themeController) { 48 | this.themeController.checked = theme === 'light'; 49 | } 50 | 51 | // Store the preference 52 | localStorage.setItem('theme', theme); 53 | }, 54 | 55 | bindEvents() { 56 | if (!this.themeController) return; 57 | 58 | this.themeController.addEventListener('change', (event) => { 59 | const theme = event.target.checked ? 'light' : 'dark'; 60 | const rect = event.target.getBoundingClientRect(); 61 | this.toggleTheme(theme, rect); 62 | }); 63 | }, 64 | 65 | setupSystemThemeListener() { 66 | // Listen for system theme changes 67 | window.matchMedia('(prefers-color-scheme: dark)') 68 | .addEventListener('change', (e) => { 69 | // Only update if no manual preference is stored 70 | if (!localStorage.getItem('theme')) { 71 | const theme = e.matches ? 'dark' : 'light'; 72 | this.toggleTheme(theme); 73 | } 74 | }); 75 | }, 76 | 77 | async toggleTheme(theme, rect = null) { 78 | try { 79 | // Animate transition if rect is provided 80 | if (rect && this.transitionElement) { 81 | this.transitionElement.style.setProperty('--x', `${rect.left + rect.width / 2}px`); 82 | this.transitionElement.style.setProperty('--y', `${rect.top + rect.height / 2}px`); 83 | this.transitionElement.classList.add('active'); 84 | } 85 | 86 | // Update theme 87 | this.setTheme(theme); 88 | 89 | // End transition animation 90 | if (this.transitionElement) { 91 | await new Promise(resolve => setTimeout(resolve, 500)); 92 | this.transitionElement.classList.remove('active'); 93 | } 94 | } catch (error) { 95 | console.error('Error toggling theme:', error); 96 | } 97 | } 98 | }; 99 | 100 | // Initialize on DOMContentLoaded 101 | document.addEventListener('DOMContentLoaded', () => { 102 | ThemeManager.init(); 103 | }); -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 |
    4 |
    5 |
    6 |

    Welcome Back

    7 | 8 | {% with messages = get_flashed_messages() %} 9 | {% if messages %} 10 | {% for message in messages %} 11 |
    12 | 13 | 14 | 15 | {{ message }} 16 |
    17 | {% endfor %} 18 | {% endif %} 19 | {% endwith %} 20 | 21 |
    22 | 23 | 24 |
    25 | 28 | 34 |
    35 | 36 |
    37 | 40 | 46 |
    47 | 48 |
    49 | 53 | 59 |
    60 | 61 |
    62 | 68 |
    69 | 70 |
    OR
    71 | 72 | 77 |
    78 |
    79 |
    80 |
    81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Terminal - Trading Dashboard 2 | 3 | Open Terminal is a Flask-based trading dashboard that provides a secure, feature-rich platform for traders using AngelOne API. It offers real-time market data, watchlist management, and comprehensive trading capabilities. 4 | 5 | ## Key Features 6 | 7 | ### Trading Features 8 | - **Real-time Market Data**: Live streaming of market prices and depth 9 | - **Watchlist Management**: Create and manage up to 5 watchlists 10 | - **Order Management**: Place, modify, and track orders 11 | - **Portfolio Overview**: View holdings, positions, and P&L 12 | - **Market Depth**: Level 2 order book display 13 | - **Symbol Search**: Extensive search functionality for adding instruments 14 | 15 | ### Technical Features 16 | - **Secure Authentication**: AngelOne API integration for secure login 17 | - **WebSocket Integration**: Real-time data updates 18 | - **Redis Caching**: Optimized performance with Redis 19 | - **Modular Architecture**: Well-organized, maintainable codebase 20 | - **Responsive Design**: Modern UI using Tailwind CSS and DaisyUI 21 | 22 | ## Technology Stack 23 | 24 | ### Backend 25 | - Flask & Flask extensions (SQLAlchemy, Login, WTF) 26 | - SQLite Database 27 | - Redis for caching 28 | - WebSocket for real-time data 29 | - APScheduler for task scheduling 30 | 31 | ### Frontend 32 | - Tailwind CSS with DaisyUI 33 | - Modern JavaScript (ES6+) 34 | - Modular component architecture 35 | - WebSocket client integration 36 | 37 | ## Quick Start 38 | 39 | ### Prerequisites 40 | - Python 3.9+ 41 | - Redis Server 42 | - Git 43 | 44 | ### Installation 45 | 46 | 1. **Clone the repository:** 47 | ```bash 48 | git clone https://github.com/marketcalls/OpenTerminal.git 49 | cd OpenTerminal 50 | ``` 51 | 52 | 2. **Set up Python environment:** 53 | ```bash 54 | python -m venv venv 55 | source venv/bin/activate # On Windows: venv\Scripts\activate 56 | pip install -r requirements.txt 57 | ``` 58 | 59 | 3. **Configure environment:** 60 | Create `.env` file with: 61 | ```env 62 | SECRET_KEY=your-secret-key 63 | SQLALCHEMY_DATABASE_URI=sqlite:///open_terminal.db 64 | REDIS_URL=redis://localhost:6379/0 65 | ``` 66 | 67 | 4. **Start Redis server** 68 | 69 | 5. **Run the application:** 70 | ```bash 71 | python app.py 72 | ``` 73 | 74 | Access at `http://127.0.0.1:5000` 75 | 76 | ## Project Structure 77 | 78 | ``` 79 | openterminal/ 80 | ├── app.py # Application entry point 81 | ├── config.py # Configuration settings 82 | ├── extensions.py # Flask extensions 83 | ├── models.py # Database models 84 | ├── routes/ # Route modules 85 | │ ├── auth.py # Authentication 86 | │ ├── dashboard/ # Dashboard features 87 | │ └── orders/ # Order management 88 | ├── static/ # Static assets 89 | │ ├── css/ # Stylesheets 90 | │ └── js/ # JavaScript modules 91 | └── templates/ # HTML templates 92 | ``` 93 | 94 | ## Development 95 | 96 | ### Code Organization 97 | - **Routes**: Organized by feature in separate modules 98 | - **Services**: Business logic separated from routes 99 | - **Models**: SQLAlchemy models for data structure 100 | - **Static**: Modular JavaScript and CSS assets 101 | - **Templates**: Jinja2 templates with component structure 102 | 103 | ### Best Practices 104 | - Follows Flask application factory pattern 105 | - Implements proper error handling 106 | - Uses type hints and docstrings 107 | - Maintains consistent code style 108 | - Includes comprehensive logging 109 | 110 | ## Production Deployment 111 | 112 | See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed production deployment instructions. 113 | 114 | ## License 115 | 116 | GNU Affero General Public License v3.0 (AGPL-3.0) 117 | 118 | See [LICENSE](LICENSE) file for complete terms. 119 | 120 | ## Contributing 121 | 122 | 1. Fork the repository 123 | 2. Create a feature branch 124 | 3. Submit a pull request 125 | 4. Open an issue for discussion 126 | 127 | --- 128 | Built with ❤️ for the trading community 129 | -------------------------------------------------------------------------------- /routes/dashboard/utils.py: -------------------------------------------------------------------------------- 1 | from extensions import redis_client 2 | import json 3 | from typing import Optional, Any, Dict, Union 4 | 5 | def cache_key(prefix: str, identifier: str) -> str: 6 | """Generate a consistent cache key format 7 | 8 | Args: 9 | prefix: The prefix for the key (e.g., 'user', 'market') 10 | identifier: The unique identifier (e.g., '123:watchlists') 11 | 12 | Returns: 13 | str: Formatted cache key 14 | """ 15 | return f"{prefix}:{identifier}" 16 | 17 | def get_cached_data(key: str, default: Any = None) -> Any: 18 | """Safely retrieve and parse cached JSON data 19 | 20 | Args: 21 | key: Redis key to retrieve 22 | default: Default value if key doesn't exist 23 | 24 | Returns: 25 | Parsed JSON data or default value 26 | """ 27 | try: 28 | data = redis_client.get(key) 29 | return json.loads(data) if data else default 30 | except (json.JSONDecodeError, TypeError): 31 | return default 32 | except Exception as e: 33 | print(f"Error retrieving cached data: {str(e)}") 34 | return default 35 | 36 | def set_cached_data(key: str, data: Any, expire: Optional[int] = None) -> bool: 37 | """Safely cache JSON serializable data 38 | 39 | Args: 40 | key: Redis key to set 41 | data: Data to cache (must be JSON serializable) 42 | expire: Optional expiration time in seconds 43 | 44 | Returns: 45 | bool: True if successful, False otherwise 46 | """ 47 | try: 48 | json_data = json.dumps(data) 49 | if expire: 50 | redis_client.setex(key, expire, json_data) 51 | else: 52 | redis_client.set(key, json_data) 53 | return True 54 | except Exception as e: 55 | print(f"Error setting cached data: {str(e)}") 56 | return False 57 | 58 | def format_number(value: Union[int, float], decimal_places: int = 2) -> str: 59 | """Format numbers consistently 60 | 61 | Args: 62 | value: Number to format 63 | decimal_places: Number of decimal places to show 64 | 65 | Returns: 66 | str: Formatted number string 67 | """ 68 | try: 69 | return f"{float(value):,.{decimal_places}f}" 70 | except (ValueError, TypeError): 71 | return str(value) 72 | 73 | def validate_user_watchlist(watchlist_id: int, user_id: int) -> bool: 74 | """Validate that a watchlist belongs to a user 75 | 76 | Args: 77 | watchlist_id: ID of the watchlist 78 | user_id: ID of the user 79 | 80 | Returns: 81 | bool: True if watchlist belongs to user, False otherwise 82 | """ 83 | key = cache_key('user', f'{user_id}:watchlists') 84 | watchlists = get_cached_data(key, []) 85 | 86 | return any(w.get('id') == watchlist_id for w in watchlists) 87 | 88 | def cleanup_user_cache(user_id: int) -> bool: 89 | """Clean up all cached data for a user 90 | 91 | Args: 92 | user_id: ID of the user 93 | 94 | Returns: 95 | bool: True if cleanup successful, False otherwise 96 | """ 97 | try: 98 | pattern = f"user:{user_id}:*" 99 | keys = redis_client.keys(pattern) 100 | if keys: 101 | redis_client.delete(*keys) 102 | return True 103 | except Exception as e: 104 | print(f"Error cleaning up user cache: {str(e)}") 105 | return False 106 | 107 | def parse_exchange_code(exchange: str) -> Optional[int]: 108 | """Parse exchange string to numeric code 109 | 110 | Args: 111 | exchange: Exchange string (e.g., 'NSE', 'BSE') 112 | 113 | Returns: 114 | Optional[int]: Exchange code or None if invalid 115 | """ 116 | exchange_map = { 117 | 'NSE': 1, 118 | 'NFO': 2, 119 | 'BSE': 3, 120 | 'BFO': 4, 121 | 'MCX': 5, 122 | 'NCX': 7, 123 | 'CDS': 13 124 | } 125 | return exchange_map.get(exchange.upper()) -------------------------------------------------------------------------------- /master_contract.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import json 4 | from models import Instrument 5 | from extensions import db 6 | from datetime import datetime 7 | from sqlalchemy import Index, inspect 8 | 9 | def index_exists(engine, index_name): 10 | """Check if an index exists in the database.""" 11 | inspector = inspect(engine) 12 | for idx in inspector.get_indexes(Instrument.__tablename__): 13 | if idx['name'] == index_name: 14 | return True 15 | return False 16 | 17 | def download_and_store_json(app): 18 | tmp_folder = "/tmp" 19 | json_file_path = os.path.join(tmp_folder, "OpenAPIScripMaster.json") 20 | 21 | with app.app_context(): 22 | try: 23 | print("Master Contract Download Started") 24 | # 1. Download JSON to the local tmp folder 25 | url = "https://margincalculator.angelbroking.com/OpenAPI_File/files/OpenAPIScripMaster.json" 26 | response = requests.get(url) 27 | 28 | # Save the JSON locally 29 | with open(json_file_path, 'w') as json_file: 30 | json.dump(response.json(), json_file) 31 | 32 | # Load data from the JSON file 33 | with open(json_file_path, 'r') as json_file: 34 | instruments_data = json.load(json_file) 35 | 36 | # 3. Clear existing data (drop and recreate the Instrument table only) 37 | Instrument.__table__.drop(db.engine) 38 | Instrument.__table__.create(db.engine) 39 | 40 | # 2. Prepare bulk insert with `strike` and `tick_size` divided by 100 41 | filtered_instruments_data = [ 42 | instrument for instrument in instruments_data 43 | if (instrument['exch_seg'] != 'NSE' or # Keep non-NSE instruments as they are 44 | (('-EQ' in instrument['symbol'] or '-BE' in instrument['symbol']) or 45 | instrument['instrumenttype'] == 'AMXIDX')) 46 | ] 47 | 48 | instrument_objects = [ 49 | Instrument( 50 | token=instrument['token'], 51 | symbol=instrument['symbol'], 52 | name=instrument['name'], 53 | expiry=instrument['expiry'], 54 | strike=float(instrument['strike']) / 100, 55 | lotsize=int(instrument['lotsize']), 56 | instrumenttype=instrument['instrumenttype'], 57 | exch_seg=instrument['exch_seg'], 58 | tick_size=float(instrument['tick_size']) / 10000000 if instrument['exch_seg'] == 'CDS' else float(instrument['tick_size']) / 100 59 | ) for instrument in filtered_instruments_data 60 | ] 61 | 62 | # Bulk insert all filtered instruments at once 63 | db.session.bulk_save_objects(instrument_objects) 64 | db.session.commit() 65 | 66 | # 4. Create indexes on `symbol`, `token`, `expiry`, and `exch_seg` if they don't exist 67 | index_mappings = [ 68 | ('idx_symbol', Instrument.symbol), 69 | ('idx_token', Instrument.token), 70 | ('idx_expiry', Instrument.expiry), 71 | ('idx_exch_seg', Instrument.exch_seg) 72 | ] 73 | 74 | for index_name, column in index_mappings: 75 | if not index_exists(db.engine, index_name): 76 | index = Index(index_name, column) 77 | index.create(db.engine) 78 | 79 | print(f"Filtered bulk data successfully downloaded and stored with indexes at {datetime.now()}") 80 | 81 | # 5. Delete the JSON file after successful bulk upload 82 | if os.path.exists(json_file_path): 83 | os.remove(json_file_path) 84 | print(f"Temporary JSON file deleted: {json_file_path}") 85 | 86 | except Exception as e: 87 | print(f"Error occurred: {e}") 88 | # Clean up in case of error 89 | if os.path.exists(json_file_path): 90 | os.remove(json_file_path) 91 | -------------------------------------------------------------------------------- /static/js/modules/orderEntry/services/orderState.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/orderEntry/services/orderState.js 2 | var OrderState = (function() { 3 | var currentState = { 4 | symbol: null, 5 | order: { 6 | variety: 'NORMAL', 7 | side: 'BUY', 8 | productType: 'INTRADAY', 9 | orderType: 'LIMIT', 10 | quantity: 0, 11 | price: 0, 12 | triggerPrice: 0, 13 | exchange: '', 14 | tradingSymbol: '', 15 | symbolToken: '' 16 | }, 17 | validation: { 18 | isValid: false, 19 | errors: {} 20 | }, 21 | market: { 22 | ltp: 0, 23 | change: 0, 24 | changePercent: 0, 25 | high: 0, 26 | low: 0 27 | } 28 | }; 29 | 30 | var listeners = []; 31 | 32 | function updateState(updates, source) { 33 | var oldState = JSON.parse(JSON.stringify(currentState)); 34 | var hasChanged = false; 35 | 36 | // Deep merge updates into current state 37 | Object.keys(updates).forEach(function(key) { 38 | if (typeof updates[key] === 'object' && updates[key] !== null) { 39 | currentState[key] = Object.assign({}, currentState[key], updates[key]); 40 | hasChanged = true; 41 | } else if (currentState[key] !== updates[key]) { 42 | currentState[key] = updates[key]; 43 | hasChanged = true; 44 | } 45 | }); 46 | 47 | if (hasChanged) { 48 | validate(); 49 | notifyListeners(oldState, source); 50 | } 51 | } 52 | 53 | function validate() { 54 | var errors = {}; 55 | var order = currentState.order; 56 | 57 | // Quantity validation 58 | if (!order.quantity || order.quantity <= 0) { 59 | errors.quantity = 'Invalid quantity'; 60 | } 61 | 62 | // Price validation for limit orders 63 | if (order.orderType === 'LIMIT' && (!order.price || order.price <= 0)) { 64 | errors.price = 'Invalid price'; 65 | } 66 | 67 | // Trigger price validation for stop loss orders 68 | if (order.variety === 'STOPLOSS' && (!order.triggerPrice || order.triggerPrice <= 0)) { 69 | errors.triggerPrice = 'Invalid trigger price'; 70 | } 71 | 72 | currentState.validation = { 73 | isValid: Object.keys(errors).length === 0, 74 | errors: errors 75 | }; 76 | } 77 | 78 | function subscribe(callback) { 79 | listeners.push(callback); 80 | // Immediately notify with current state 81 | callback(currentState, null, 'SUBSCRIPTION'); 82 | return function() { 83 | unsubscribe(callback); 84 | }; 85 | } 86 | 87 | function unsubscribe(callback) { 88 | var index = listeners.indexOf(callback); 89 | if (index > -1) { 90 | listeners.splice(index, 1); 91 | } 92 | } 93 | 94 | function notifyListeners(oldState, source) { 95 | listeners.forEach(function(listener) { 96 | listener(currentState, oldState, source); 97 | }); 98 | } 99 | 100 | function getState() { 101 | return JSON.parse(JSON.stringify(currentState)); 102 | } 103 | 104 | function reset() { 105 | updateState({ 106 | order: { 107 | variety: 'NORMAL', 108 | side: 'BUY', 109 | productType: 'INTRADAY', 110 | orderType: 'LIMIT', 111 | quantity: 0, 112 | price: 0, 113 | triggerPrice: 0, 114 | exchange: '', 115 | tradingSymbol: '', 116 | symbolToken: '' 117 | } 118 | }, 'RESET'); 119 | } 120 | 121 | return { 122 | updateState: updateState, 123 | subscribe: subscribe, 124 | unsubscribe: unsubscribe, 125 | getState: getState, 126 | reset: reset 127 | }; 128 | })(); -------------------------------------------------------------------------------- /templates/logs.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
    5 | 6 |
    7 |
    8 |
    Total Orders
    9 |
    {{ stats.total }}
    10 |
    All trading activity
    11 |
    12 |
    13 |
    Buy Orders
    14 |
    {{ stats.by_type.BUY }}
    15 |
    Total buy orders
    16 |
    17 |
    18 |
    Sell Orders
    19 |
    {{ stats.by_type.SELL }}
    20 |
    Total sell orders
    21 |
    22 |
    23 | 24 | 25 | {% for source, source_orders in orders.items() %} 26 |
    27 |
    28 |
    29 |

    {{ source }} Orders

    30 |
    31 |
    32 |
    Total
    33 |
    {{ stats.by_source[source].total }}
    34 |
    35 |
    36 |
    Buy
    37 |
    {{ stats.by_source[source].BUY }}
    38 |
    39 |
    40 |
    Sell
    41 |
    {{ stats.by_source[source].SELL }}
    42 |
    43 |
    44 |
    45 |
    46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {% for order in source_orders %} 61 | 62 | 63 | 64 | 69 | 72 | 73 | 76 | 77 | 78 | 79 | {% endfor %} 80 | 81 |
    TimeSymbolTypeProductQtyStatusOrder IDMessage
    {{ order.timestamp.strftime('%d/%m/%Y %I:%M:%S %p') }}{{ order.symbol }} 65 |
    66 | {{ order.transaction_type }} 67 |
    68 |
    70 |
    {{ order.product_type }}
    71 |
    {{ order.quantity }} 74 |
    {{ order.status }}
    75 |
    {{ order.order_id }}{{ order.message or '' }}
    82 |
    83 |
    84 |
    85 | {% endfor %} 86 |
    87 | {% endblock %} 88 | -------------------------------------------------------------------------------- /static/js/modules/orderEntry/services/priceService.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/orderEntry/services/priceService.js 2 | var PriceService = (function() { 3 | var subscribers = new Map(); 4 | var websocket = null; 5 | var reconnectAttempts = 0; 6 | var maxReconnectAttempts = 5; 7 | var reconnectDelay = 1000; 8 | 9 | function init(wsUrl) { 10 | connectWebSocket(wsUrl); 11 | } 12 | 13 | function connectWebSocket(wsUrl) { 14 | if (websocket && websocket.readyState === WebSocket.OPEN) { 15 | return; 16 | } 17 | 18 | websocket = new WebSocket(wsUrl); 19 | websocket.binaryType = 'arraybuffer'; 20 | 21 | websocket.onopen = function() { 22 | console.log('WebSocket connected'); 23 | reconnectAttempts = 0; 24 | subscribeAll(); 25 | }; 26 | 27 | websocket.onmessage = function(event) { 28 | handleMessage(event.data); 29 | }; 30 | 31 | websocket.onerror = function(error) { 32 | console.error('WebSocket error:', error); 33 | }; 34 | 35 | websocket.onclose = function() { 36 | handleDisconnect(); 37 | }; 38 | } 39 | 40 | function handleDisconnect() { 41 | if (reconnectAttempts < maxReconnectAttempts) { 42 | reconnectAttempts++; 43 | setTimeout(function() { 44 | connectWebSocket(); 45 | }, reconnectDelay * reconnectAttempts); 46 | } 47 | } 48 | 49 | function handleMessage(data) { 50 | try { 51 | // Assuming you have a market data decoder 52 | var decodedData = MarketDataDecoder.decode(data); 53 | notifySubscribers(decodedData); 54 | } catch (error) { 55 | console.error('Error processing market data:', error); 56 | } 57 | } 58 | 59 | function subscribeSymbol(token, exchange, callback) { 60 | var key = getSubscriptionKey(token, exchange); 61 | subscribers.set(key, callback); 62 | 63 | if (websocket && websocket.readyState === WebSocket.OPEN) { 64 | sendSubscription(token, exchange); 65 | } 66 | } 67 | 68 | function unsubscribeSymbol(token, exchange) { 69 | var key = getSubscriptionKey(token, exchange); 70 | subscribers.delete(key); 71 | 72 | if (websocket && websocket.readyState === WebSocket.OPEN) { 73 | sendUnsubscription(token, exchange); 74 | } 75 | } 76 | 77 | function sendSubscription(token, exchange) { 78 | var message = { 79 | action: 1, // Subscribe 80 | params: { 81 | mode: 3, 82 | tokenList: [{ 83 | exchangeType: getExchangeType(exchange), 84 | tokens: [token] 85 | }] 86 | } 87 | }; 88 | websocket.send(JSON.stringify(message)); 89 | } 90 | 91 | function sendUnsubscription(token, exchange) { 92 | var message = { 93 | action: 2, // Unsubscribe 94 | params: { 95 | mode: 3, 96 | tokenList: [{ 97 | exchangeType: getExchangeType(exchange), 98 | tokens: [token] 99 | }] 100 | } 101 | }; 102 | websocket.send(JSON.stringify(message)); 103 | } 104 | 105 | function notifySubscribers(data) { 106 | var key = getSubscriptionKey(data.token, data.exchange); 107 | var callback = subscribers.get(key); 108 | if (callback) { 109 | callback(data); 110 | } 111 | } 112 | 113 | function getSubscriptionKey(token, exchange) { 114 | return exchange + ':' + token; 115 | } 116 | 117 | function getExchangeType(exchange) { 118 | var exchangeTypes = { 119 | 'NSE': 1, 120 | 'BSE': 3, 121 | 'NFO': 2, 122 | 'MCX': 5, 123 | 'CDS': 7 124 | }; 125 | return exchangeTypes[exchange] || 1; 126 | } 127 | 128 | return { 129 | init: init, 130 | subscribeSymbol: subscribeSymbol, 131 | unsubscribeSymbol: unsubscribeSymbol 132 | }; 133 | })(); -------------------------------------------------------------------------------- /static/js/modules/orderEntry/components/QuantityInput.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/orderEntry/components/QuantityInput.js 2 | var QuantityInput = (function() { 3 | var input = null; 4 | var options = { 5 | lotSize: 1, 6 | maxQuantity: 999999, 7 | minQuantity: 1 8 | }; 9 | 10 | function init(inputElement, customOptions) { 11 | input = inputElement; 12 | options = Object.assign({}, options, customOptions); 13 | setupEventListeners(); 14 | } 15 | 16 | function setupEventListeners() { 17 | if (!input) return; 18 | 19 | input.addEventListener('input', handleQuantityInput); 20 | input.addEventListener('blur', handleQuantityBlur); 21 | input.addEventListener('keydown', handleKeyDown); 22 | 23 | // Add increment/decrement buttons if they exist 24 | var incrementBtn = input.parentElement.querySelector('.qty-increment'); 25 | var decrementBtn = input.parentElement.querySelector('.qty-decrement'); 26 | 27 | if (incrementBtn) { 28 | incrementBtn.addEventListener('click', function() { 29 | adjustQuantity(1); 30 | }); 31 | } 32 | 33 | if (decrementBtn) { 34 | decrementBtn.addEventListener('click', function() { 35 | adjustQuantity(-1); 36 | }); 37 | } 38 | } 39 | 40 | function handleQuantityInput(event) { 41 | // Allow only numbers 42 | var value = event.target.value.replace(/\D/g, ''); 43 | event.target.value = value; 44 | } 45 | 46 | function handleQuantityBlur(event) { 47 | var quantity = parseInt(event.target.value, 10); 48 | if (isNaN(quantity)) { 49 | event.target.value = options.lotSize; 50 | return; 51 | } 52 | 53 | // Round to lot size 54 | quantity = roundToLotSize(quantity); 55 | 56 | // Apply min/max constraints 57 | quantity = Math.min(Math.max(quantity, options.minQuantity), options.maxQuantity); 58 | 59 | event.target.value = quantity; 60 | 61 | // Dispatch change event 62 | var changeEvent = new CustomEvent('quantity-changed', { 63 | detail: { 64 | quantity: quantity, 65 | lots: quantity / options.lotSize 66 | } 67 | }); 68 | input.dispatchEvent(changeEvent); 69 | } 70 | 71 | function handleKeyDown(event) { 72 | if (event.key === 'ArrowUp') { 73 | event.preventDefault(); 74 | adjustQuantity(1); 75 | } else if (event.key === 'ArrowDown') { 76 | event.preventDefault(); 77 | adjustQuantity(-1); 78 | } 79 | } 80 | 81 | function adjustQuantity(direction) { 82 | if (!input) return; 83 | 84 | var currentQty = parseInt(input.value, 10) || 0; 85 | var newQty = roundToLotSize(currentQty + (direction * options.lotSize)); 86 | 87 | // Apply min/max constraints 88 | newQty = Math.min(Math.max(newQty, options.minQuantity), options.maxQuantity); 89 | 90 | input.value = newQty; 91 | 92 | // Dispatch change event 93 | var changeEvent = new CustomEvent('quantity-changed', { 94 | detail: { 95 | quantity: newQty, 96 | lots: newQty / options.lotSize 97 | } 98 | }); 99 | input.dispatchEvent(changeEvent); 100 | } 101 | 102 | function roundToLotSize(quantity) { 103 | return Math.round(quantity / options.lotSize) * options.lotSize; 104 | } 105 | 106 | function setValue(quantity) { 107 | if (!input) return; 108 | 109 | var validQty = roundToLotSize(parseInt(quantity, 10) || options.lotSize); 110 | validQty = Math.min(Math.max(validQty, options.minQuantity), options.maxQuantity); 111 | input.value = validQty; 112 | } 113 | 114 | function setLotSize(newLotSize) { 115 | options.lotSize = parseInt(newLotSize, 10) || 1; 116 | // Update current value to match new lot size 117 | if (input) { 118 | var currentQty = parseInt(input.value, 10) || options.lotSize; 119 | setValue(currentQty); 120 | } 121 | } 122 | 123 | return { 124 | init: init, 125 | setValue: setValue, 126 | setLotSize: setLotSize 127 | }; 128 | })(); -------------------------------------------------------------------------------- /templates/components/watchlist/_manage_modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 73 | -------------------------------------------------------------------------------- /static/js/modules/orderEntry/components/PriceInput.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/orderEntry/components/PriceInput.js 2 | var PriceInput = (function() { 3 | var input = null; 4 | var options = { 5 | tickSize: 0.05, 6 | maxPrice: 999999.99, 7 | minPrice: 0.01 8 | }; 9 | 10 | function init(inputElement, customOptions) { 11 | input = inputElement; 12 | options = Object.assign({}, options, customOptions); 13 | setupEventListeners(); 14 | } 15 | 16 | function setupEventListeners() { 17 | if (!input) return; 18 | 19 | input.addEventListener('input', handlePriceInput); 20 | input.addEventListener('blur', handlePriceBlur); 21 | input.addEventListener('keydown', handleKeyDown); 22 | 23 | // Add increment/decrement buttons if they exist 24 | var incrementBtn = input.parentElement.querySelector('.price-increment'); 25 | var decrementBtn = input.parentElement.querySelector('.price-decrement'); 26 | 27 | if (incrementBtn) { 28 | incrementBtn.addEventListener('click', function() { 29 | adjustPrice(1); 30 | }); 31 | } 32 | 33 | if (decrementBtn) { 34 | decrementBtn.addEventListener('click', function() { 35 | adjustPrice(-1); 36 | }); 37 | } 38 | } 39 | 40 | function handlePriceInput(event) { 41 | var value = event.target.value; 42 | 43 | // Remove non-numeric characters except decimal point 44 | value = value.replace(/[^\d.]/g, ''); 45 | 46 | // Ensure only one decimal point 47 | var parts = value.split('.'); 48 | if (parts.length > 2) { 49 | value = parts[0] + '.' + parts.slice(1).join(''); 50 | } 51 | 52 | // Limit decimal places 53 | if (parts.length > 1) { 54 | value = parts[0] + '.' + parts[1].slice(0, 2); 55 | } 56 | 57 | event.target.value = value; 58 | } 59 | 60 | function handlePriceBlur(event) { 61 | var price = parseFloat(event.target.value); 62 | if (isNaN(price)) { 63 | event.target.value = ''; 64 | return; 65 | } 66 | 67 | // Round to tick size 68 | price = roundToTickSize(price); 69 | 70 | // Apply min/max constraints 71 | price = Math.min(Math.max(price, options.minPrice), options.maxPrice); 72 | 73 | event.target.value = price.toFixed(2); 74 | 75 | // Dispatch change event 76 | var changeEvent = new CustomEvent('price-changed', { 77 | detail: { price: price } 78 | }); 79 | input.dispatchEvent(changeEvent); 80 | } 81 | 82 | function handleKeyDown(event) { 83 | // Allow up/down arrow keys to increment/decrement 84 | if (event.key === 'ArrowUp') { 85 | event.preventDefault(); 86 | adjustPrice(1); 87 | } else if (event.key === 'ArrowDown') { 88 | event.preventDefault(); 89 | adjustPrice(-1); 90 | } 91 | } 92 | 93 | function adjustPrice(direction) { 94 | if (!input || input.disabled) return; 95 | 96 | var currentPrice = parseFloat(input.value) || 0; 97 | var newPrice = roundToTickSize(currentPrice + (direction * options.tickSize)); 98 | 99 | // Apply min/max constraints 100 | newPrice = Math.min(Math.max(newPrice, options.minPrice), options.maxPrice); 101 | 102 | input.value = newPrice.toFixed(2); 103 | 104 | // Dispatch change event 105 | var changeEvent = new CustomEvent('price-changed', { 106 | detail: { price: newPrice } 107 | }); 108 | input.dispatchEvent(changeEvent); 109 | } 110 | 111 | function roundToTickSize(price) { 112 | return Math.round(price / options.tickSize) * options.tickSize; 113 | } 114 | 115 | function setValue(price) { 116 | if (!input) return; 117 | 118 | var validPrice = roundToTickSize(parseFloat(price) || 0); 119 | validPrice = Math.min(Math.max(validPrice, options.minPrice), options.maxPrice); 120 | input.value = validPrice.toFixed(2); 121 | } 122 | 123 | function setTickSize(newTickSize) { 124 | options.tickSize = parseFloat(newTickSize) || 0.05; 125 | } 126 | 127 | return { 128 | init: init, 129 | setValue: setValue, 130 | setTickSize: setTickSize 131 | }; 132 | })(); -------------------------------------------------------------------------------- /static/js/modules/watchlistCore.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/watchlistCore.js 2 | 3 | const WatchlistCore = { 4 | /** 5 | * Core state and template cache 6 | */ 7 | state: { 8 | csrfToken: null, 9 | searchTimeout: null, 10 | activeSubscriptions: new Map(), 11 | templates: new Map() 12 | }, 13 | 14 | /** 15 | * Initialize the watchlist manager 16 | */ 17 | init() { 18 | this.state.csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); 19 | this.loadTemplates(); 20 | WatchlistEvents.bindEventListeners(); 21 | this.initializeSearch(); 22 | this.initializeTabManagement(); 23 | }, 24 | 25 | /** 26 | * Template Management 27 | */ 28 | async loadTemplates() { 29 | const templateFiles = [ 30 | 'symbolListItem', 31 | 'marketDepth', 32 | 'emptyWatchlist' 33 | ]; 34 | 35 | for (const name of templateFiles) { 36 | try { 37 | const response = await fetch(`/static/js/modules/templates/${name}.html`); 38 | const template = await response.text(); 39 | this.state.templates.set(name, template); 40 | } catch (error) { 41 | console.error(`Error loading template ${name}:`, error); 42 | } 43 | } 44 | }, 45 | 46 | getTemplate(name, data = {}) { 47 | let template = this.state.templates.get(name) || ''; 48 | return template.replace(/\${(\w+)}/g, (_, key) => data[key] || ''); 49 | }, 50 | 51 | initializeTabManagement() { 52 | const activeTab = document.querySelector('.tab-active'); 53 | if (activeTab) { 54 | WatchlistEvents.handleTabActivation(activeTab.dataset.watchlistId); 55 | } 56 | }, 57 | 58 | /** 59 | * Symbol Search Management 60 | */ 61 | initializeSearch() { 62 | const searchInput = document.getElementById('search-symbol-input'); 63 | const searchResults = document.getElementById('search-results'); 64 | 65 | searchInput?.addEventListener('input', (e) => { 66 | clearTimeout(this.state.searchTimeout); 67 | const query = e.target.value.trim(); 68 | 69 | if (query.length >= 2) { 70 | this.state.searchTimeout = setTimeout(() => this.performSearch(query), 300); 71 | } else { 72 | searchResults.innerHTML = ''; 73 | } 74 | }); 75 | 76 | document.addEventListener('click', (e) => { 77 | if (!e.target.closest('#search-symbol-input') && !e.target.closest('#search-results')) { 78 | searchResults.innerHTML = ''; 79 | searchInput.value = ''; 80 | } 81 | }); 82 | }, 83 | 84 | async performSearch(query) { 85 | try { 86 | const response = await fetch(`/search_symbols?q=${encodeURIComponent(query)}`); 87 | const data = await response.json(); 88 | const filteredResults = this.filterSearchResults(data.results, query); 89 | this.displaySearchResults(filteredResults); 90 | } catch (error) { 91 | console.error('Error searching symbols:', error); 92 | } 93 | }, 94 | 95 | filterSearchResults(results, query) { 96 | const terms = query.toUpperCase().split(/\s+/); 97 | return results.filter(result => { 98 | const symbolInfo = `${result.symbol} ${result.exch_seg}`.toUpperCase(); 99 | return terms.every(term => symbolInfo.includes(term)); 100 | }); 101 | }, 102 | 103 | displaySearchResults(results) { 104 | const searchResults = document.getElementById('search-results'); 105 | searchResults.innerHTML = results.map(result => ` 106 |
  • 108 |
    109 |
    ${result.symbol}
    110 |
    ${result.exch_seg}
    111 |
    112 |
  • 113 | `).join(''); 114 | }, 115 | 116 | /** 117 | * Utility Functions 118 | */ 119 | async makeRequest(url, options = {}) { 120 | const defaultOptions = { 121 | headers: { 122 | 'Content-Type': 'application/json', 123 | 'X-CSRFToken': this.state.csrfToken 124 | } 125 | }; 126 | const response = await fetch(url, { ...defaultOptions, ...options }); 127 | return response.json(); 128 | }, 129 | 130 | getExchTypeCode(exchSeg) { 131 | const exchMap = { 132 | 'NSE': 1, 'NFO': 2, 'BSE': 3, 'BFO': 4, 133 | 'MCX': 5, 'NCX': 7, 'CDS': 13 134 | }; 135 | return exchMap[exchSeg] || 1; 136 | } 137 | }; 138 | 139 | // Export for use in other modules 140 | window.WatchlistCore = WatchlistCore; 141 | -------------------------------------------------------------------------------- /routes/voice/utils/helpers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, time 2 | import pytz 3 | import json 4 | import logging 5 | 6 | logger = logging.getLogger('voice') 7 | 8 | # Mapping between user-facing product types and Angel API product types 9 | PRODUCT_TYPE_MAPPING = { 10 | "CNC": "DELIVERY", # Cash & Carry maps to DELIVERY 11 | "NRML": "CARRYFORWARD", # Normal maps to CARRYFORWARD 12 | "MIS": "INTRADAY" # Margin Intraday Squareoff maps to INTRADAY 13 | } 14 | 15 | def map_product_type_to_api(product_type: str) -> str: 16 | """Map user-facing product type to Angel API product type""" 17 | return PRODUCT_TYPE_MAPPING.get(product_type, product_type) 18 | 19 | def is_market_open() -> bool: 20 | """Check if market is currently open""" 21 | return True 22 | # ist = pytz.timezone('Asia/Kolkata') 23 | # current_time = datetime.now(ist).time() 24 | # current_day = datetime.now(ist).weekday() 25 | 26 | # # Check if it's a weekday (0-4 represents Monday to Friday) 27 | # if current_day >= 5: # Saturday or Sunday 28 | # return False 29 | 30 | # # Market hours: 9:15 AM to 3:30 PM IST 31 | # market_start = time(9, 15) 32 | # market_end = time(15, 30) 33 | 34 | # # Check if current time is within market hours 35 | # return market_start <= current_time <= market_end 36 | 37 | def format_log_message(action: str, user_id: str, data: dict, status: str, error_msg: str = None) -> str: 38 | """Format log message for voice orders""" 39 | log_data = { 40 | 'timestamp': datetime.utcnow().isoformat(), 41 | 'action': action, 42 | 'user_id': user_id, 43 | 'data': data, 44 | 'status': status 45 | } 46 | 47 | if error_msg: 48 | log_data['error'] = error_msg 49 | 50 | return json.dumps(log_data) 51 | 52 | def validate_audio_format(mimetype: str) -> bool: 53 | """Validate if audio format is supported""" 54 | supported_formats = [ 55 | 'audio/webm', 56 | 'audio/wav', 57 | 'audio/mp3', 58 | 'audio/mpeg', 59 | 'audio/ogg', 60 | 'audio/flac' 61 | ] 62 | return mimetype in supported_formats 63 | 64 | def validate_exchange(exchange: str) -> bool: 65 | """Validate if exchange is supported""" 66 | valid_exchanges = [ 67 | "NSE", "NFO", "CDS", 68 | "BSE", "BFO", "BCD", 69 | "MCX", "NCDEX" 70 | ] 71 | return exchange in valid_exchanges 72 | 73 | def validate_product_type(product_type: str) -> bool: 74 | """Validate if product type is supported""" 75 | valid_product_types = [ 76 | "CNC", # Cash & Carry for equity 77 | "NRML", # Normal for futures and options 78 | "MIS" # Margin Intraday Squareoff 79 | ] 80 | return product_type in valid_product_types 81 | 82 | def validate_model(model: str) -> bool: 83 | """Validate if model is supported""" 84 | valid_models = [ 85 | "whisper-large-v3", 86 | "whisper-large-v3-turbo", 87 | "distil-whisper-large-v3-en" 88 | ] 89 | return model in valid_models 90 | 91 | def get_valid_exchanges() -> list: 92 | """Get list of valid exchanges""" 93 | return [ 94 | {"code": "NSE", "name": "NSE: NSE Equity"}, 95 | {"code": "BSE", "name": "BSE: BSE Equity"}, 96 | {"code": "NFO", "name": "NFO: NSE F&O"}, 97 | {"code": "CDS", "name": "CDS: NSE Currency"}, 98 | {"code": "BFO", "name": "BFO: BSE F&O"}, 99 | {"code": "BCD", "name": "BCD: BSE Currency"}, 100 | {"code": "MCX", "name": "MCX: Multi Commodity Exchange"}, 101 | {"code": "NCDEX", "name": "NCDEX: National Commodity Exchange"} 102 | ] 103 | 104 | def get_valid_product_types() -> list: 105 | """Get list of valid product types""" 106 | return [ 107 | {"code": "CNC", "name": "Cash & Carry for equity (DELIVERY)"}, 108 | {"code": "NRML", "name": "Normal for futures and options (CARRYFORWARD)"}, 109 | {"code": "MIS", "name": "Margin Intraday Squareoff (INTRADAY)"} 110 | ] 111 | 112 | def get_valid_models() -> list: 113 | """Get list of valid models""" 114 | return [ 115 | {"code": "whisper-large-v3", "name": "Whisper Large V3"}, 116 | {"code": "whisper-large-v3-turbo", "name": "Whisper Large V3 Turbo"}, 117 | {"code": "distil-whisper-large-v3-en", "name": "Distil Whisper Large V3 (English)"} 118 | ] 119 | 120 | def get_order_variety() -> list: 121 | """Get list of valid order varieties""" 122 | return [ 123 | {"code": "NORMAL", "name": "Regular Order"}, 124 | {"code": "STOPLOSS", "name": "Stop Loss Order"}, 125 | {"code": "AMO", "name": "After Market Order"} 126 | ] 127 | 128 | def get_order_types() -> list: 129 | """Get list of valid order types""" 130 | return [ 131 | {"code": "MARKET", "name": "Market Order (MKT)"}, 132 | {"code": "LIMIT", "name": "Limit Order (L)"}, 133 | {"code": "STOPLOSS_LIMIT", "name": "Stop Loss Limit Order (SL)"}, 134 | {"code": "STOPLOSS_MARKET", "name": "Stop Loss Market Order (SL-M)"} 135 | ] 136 | 137 | def get_order_durations() -> list: 138 | """Get list of valid order durations""" 139 | return [ 140 | {"code": "DAY", "name": "Regular Order"}, 141 | {"code": "IOC", "name": "Immediate or Cancel"} 142 | ] 143 | -------------------------------------------------------------------------------- /templates/tradebook.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block content %} 4 |
    5 |

    Trade Book

    6 | 7 |
    8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for trade in trade_data %} 26 | 27 | 28 | 29 | 30 | 40 | 41 | 42 | 43 | 44 | 45 | {% else %} 46 | 47 | 48 | 49 | {% endfor %} 50 | 51 |
    Order IDSymbolProductTransactionTrade ValueFill PriceSizeTime
    {{ trade.orderid }}{{ trade.tradingsymbol }}{{ trade.producttype }} 31 | 37 | {{ trade.transactiontype }} 38 | 39 | ₹{{ trade.tradevalue | float | round(2) }}{{ trade.fillprice | float | round(2) }}{{ trade.fillsize }}{{ trade.filltime }}
    No trades found.
    52 |
    53 |
    54 | 55 | 147 | {% endblock %} -------------------------------------------------------------------------------- /routes/orders/validators/order_validator.py: -------------------------------------------------------------------------------- 1 | # routes/orders/validators/order_validator.py 2 | from typing import Dict, Any, Optional 3 | from ..constants import ( 4 | EQUITY_SEGMENTS, DERIVATIVE_SEGMENTS, 5 | MARKET, LIMIT, SL_MARKET, SL_LIMIT, 6 | REGULAR, STOPLOSS 7 | ) 8 | 9 | class OrderValidator: 10 | def validate(self, order_data: Dict) -> Dict: 11 | """Validate complete order data""" 12 | try: 13 | self._validate_required_fields(order_data) 14 | self._validate_order_type(order_data) 15 | self._validate_exchange(order_data) 16 | self._validate_quantity(order_data) 17 | self._validate_price_values(order_data) 18 | 19 | return order_data 20 | except ValueError as e: 21 | raise ValueError(f"Order validation failed: {str(e)}") 22 | 23 | def _validate_required_fields(self, data: Dict) -> None: 24 | """Validate presence of required fields""" 25 | required_fields = { 26 | 'symbol', 'token', 'exchange', 'side', 27 | 'ordertype', 'producttype', 'quantity' 28 | } 29 | 30 | missing_fields = required_fields - set(data.keys()) 31 | if missing_fields: 32 | raise ValueError(f"Missing required fields: {missing_fields}") 33 | 34 | def _validate_order_type(self, data: Dict) -> None: 35 | """Validate order type and related fields""" 36 | order_type = data.get('ordertype') 37 | variety = data.get('variety', REGULAR) 38 | 39 | # Validate order type based on variety 40 | if variety == STOPLOSS: 41 | if order_type not in [SL_MARKET, SL_LIMIT]: 42 | raise ValueError(f"Invalid order type {order_type} for STOPLOSS orders") 43 | 44 | if 'triggerprice' not in data: 45 | raise ValueError("Trigger price required for stop loss orders") 46 | try: 47 | trigger_price = float(data['triggerprice']) 48 | if trigger_price <= 0: 49 | raise ValueError("Trigger price must be greater than 0") 50 | 51 | if order_type == SL_LIMIT: 52 | price = float(data.get('price', 0)) 53 | if price <= 0: 54 | raise ValueError("Price is required for STOPLOSS_LIMIT orders") 55 | 56 | # For BUY STOPLOSS, trigger price should be lower than price 57 | if data['side'] == 'BUY': 58 | if price <= trigger_price: 59 | raise ValueError("For Buy STOPLOSS, trigger price must be less than price") 60 | # For SELL STOPLOSS, trigger price should be higher than price 61 | else: 62 | if price >= trigger_price: 63 | raise ValueError("For Sell STOPLOSS, trigger price must be greater than price") 64 | 65 | except (ValueError, TypeError): 66 | raise ValueError("Invalid trigger price") 67 | 68 | elif variety == REGULAR: 69 | if order_type not in [MARKET, LIMIT]: 70 | raise ValueError(f"Invalid order type {order_type} for REGULAR orders") 71 | 72 | if order_type == LIMIT and not self._is_valid_price(data.get('price')): 73 | raise ValueError("Invalid price for limit order") 74 | 75 | def _validate_exchange(self, data: Dict) -> None: 76 | """Validate exchange and segment specific rules""" 77 | exchange = data.get('exchange') 78 | if not exchange: 79 | raise ValueError("Exchange is required") 80 | 81 | if exchange not in (EQUITY_SEGMENTS + DERIVATIVE_SEGMENTS): 82 | raise ValueError(f"Invalid exchange: {exchange}") 83 | 84 | def _validate_quantity(self, data: Dict) -> None: 85 | """Validate quantity based on exchange rules""" 86 | quantity = data.get('quantity') 87 | if not quantity or not isinstance(quantity, (int, str)): 88 | raise ValueError("Invalid quantity") 89 | 90 | try: 91 | qty = int(quantity) 92 | if qty <= 0: 93 | raise ValueError("Quantity must be positive") 94 | except ValueError: 95 | raise ValueError("Quantity must be a valid number") 96 | 97 | def _validate_price_values(self, data: Dict) -> None: 98 | """Validate price values""" 99 | order_type = data.get('ordertype') 100 | if order_type in [LIMIT, SL_LIMIT]: 101 | if not self._is_valid_price(data.get('price')): 102 | raise ValueError("Invalid price value") 103 | 104 | def _is_valid_price(self, price: Any) -> bool: 105 | """Check if price is valid""" 106 | if price is None: 107 | return False 108 | try: 109 | price_float = float(price) 110 | return price_float > 0 111 | except (ValueError, TypeError): 112 | return False 113 | 114 | def validate_price(self, order_price: float, market_price: float, 115 | order_type: str) -> bool: 116 | """Validate price against market price""" 117 | if order_type == MARKET: 118 | return True 119 | 120 | try: 121 | order_price = float(order_price) 122 | market_price = float(market_price) 123 | 124 | # Add your price validation logic here 125 | # For example, checking circuit limits, price bands, etc. 126 | return True 127 | 128 | except (ValueError, TypeError): 129 | return False -------------------------------------------------------------------------------- /routes/scalper/routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, jsonify, session, redirect, url_for, flash, request, current_app 2 | from models import User, OrderLog 3 | from ..dashboard.watchlist_service import WatchlistService 4 | from .services.scalper_service import ScalperService 5 | from ..voice.utils.helpers import validate_product_type, is_market_open 6 | from extensions import db 7 | import logging 8 | import json 9 | 10 | scalper_bp = Blueprint('scalper', __name__) 11 | watchlist_service = WatchlistService() 12 | scalper_service = ScalperService() 13 | logger = logging.getLogger('scalper') 14 | 15 | @scalper_bp.route('/') 16 | def scalper(): 17 | """Render scalping interface""" 18 | if 'client_id' not in session: 19 | flash('Please login to access scalping.', 'warning') 20 | return redirect(url_for('auth.login')) 21 | 22 | user = User.query.filter_by(client_id=session['client_id']).first() 23 | if not user: 24 | flash('User not found.', 'error') 25 | return redirect(url_for('auth.login')) 26 | 27 | return render_template('scalper.html', title='Scalping Terminal') 28 | 29 | @scalper_bp.route('/search', methods=['GET']) 30 | def search_symbols(): 31 | """Search for trading symbols""" 32 | if 'client_id' not in session: 33 | return jsonify({'error': 'Not authenticated'}), 401 34 | 35 | query = request.args.get('q', '') 36 | results = watchlist_service.search_symbols(query) 37 | return jsonify(results) 38 | 39 | @scalper_bp.route('/place_order', methods=['POST']) 40 | def place_order(): 41 | """Place a scalping order""" 42 | try: 43 | # Authentication check 44 | if 'client_id' not in session: 45 | return jsonify({'error': 'Not authenticated'}), 401 46 | 47 | user = User.query.filter_by(client_id=session['client_id']).first() 48 | if not user: 49 | return jsonify({'error': 'User not found'}), 404 50 | 51 | # Check if market is open 52 | if not is_market_open(): 53 | return jsonify({"error": "Market is closed"}), 400 54 | 55 | # Get and validate request data 56 | try: 57 | if not request.is_json: 58 | logger.error("Request is not JSON") 59 | return jsonify({'error': 'Request must be JSON'}), 400 60 | 61 | data = request.get_json() 62 | if not data: 63 | logger.error("No JSON data in request") 64 | return jsonify({'error': 'No data provided'}), 400 65 | 66 | # Log request details 67 | logger.info(f"Received order request: {json.dumps(data)}") 68 | 69 | except json.JSONDecodeError as e: 70 | logger.error(f"JSON decode error: {str(e)}") 71 | return jsonify({'error': 'Invalid JSON data'}), 400 72 | 73 | # Extract order details 74 | action = data.get('action') 75 | quantity = data.get('quantity') 76 | symbol = data.get('symbol') 77 | exchange = data.get('exchange') 78 | product_type = data.get('product_type', 'MIS') # Default to MIS if not provided 79 | 80 | # Validate inputs 81 | if not all([action, quantity, symbol, exchange]): 82 | missing = [k for k in ['action', 'quantity', 'symbol', 'exchange'] 83 | if not data.get(k)] 84 | return jsonify({'error': f'Missing required fields: {", ".join(missing)}'}), 400 85 | 86 | if action not in ['BUY', 'SELL']: 87 | return jsonify({'error': 'Invalid action'}), 400 88 | 89 | try: 90 | quantity = int(quantity) 91 | if quantity <= 0: 92 | return jsonify({'error': 'Invalid quantity'}), 400 93 | except ValueError: 94 | return jsonify({'error': 'Invalid quantity'}), 400 95 | 96 | # Validate product type 97 | if not validate_product_type(product_type): 98 | return jsonify({'error': f'Invalid product type: {product_type}'}), 400 99 | 100 | # Place the order using scalper service 101 | logger.info(f"Placing order: {action} {quantity} {symbol} {product_type}") 102 | result = scalper_service.place_order( 103 | action=action, 104 | quantity=quantity, 105 | tradingsymbol=symbol, 106 | exchange=exchange, 107 | product_type=product_type, 108 | client_id=session['client_id'] 109 | ) 110 | 111 | if 'error' in result: 112 | logger.error(f"Order placement failed: {result['error']}") 113 | return jsonify({'error': result['error']}), 400 114 | 115 | logger.info(f"Order placed successfully: {result}") 116 | return jsonify(result) 117 | 118 | except Exception as e: 119 | logger.error(f"Unexpected error in place_order: {str(e)}") 120 | return jsonify({'error': str(e)}), 500 121 | 122 | @scalper_bp.route('/orders', methods=['GET']) 123 | def get_orders(): 124 | """Get recent orders for the user""" 125 | if 'client_id' not in session: 126 | return jsonify({'error': 'Not authenticated'}), 401 127 | 128 | user = User.query.filter_by(client_id=session['client_id']).first() 129 | if not user: 130 | return jsonify({'error': 'User not found'}), 404 131 | 132 | try: 133 | orders = OrderLog.query.filter_by( 134 | user_id=user.id, 135 | order_source='SCALPER' 136 | ).order_by(OrderLog.timestamp.desc()).limit(50).all() 137 | 138 | return jsonify([order.to_dict() for order in orders]) 139 | except Exception as e: 140 | logger.error(f"Error fetching orders: {str(e)}") 141 | return jsonify({'error': 'Failed to fetch orders'}), 500 142 | -------------------------------------------------------------------------------- /static/js/modules/marketDataDecoder.js: -------------------------------------------------------------------------------- 1 | // static/js/modules/marketDataDecoder.js 2 | 3 | const MarketDataDecoder = { 4 | getPriceDivisor(exchangeType) { 5 | // For CDS (Currency Derivatives), use 10000000.0 6 | return exchangeType === 13 ? 10000000.0 : 100; 7 | }, 8 | 9 | decode(binaryData) { 10 | const dataView = new DataView(binaryData); 11 | let offset = 0; 12 | 13 | // Header information 14 | const subscriptionMode = dataView.getInt8(offset); 15 | offset += 1; 16 | 17 | const exchangeType = dataView.getInt8(offset); 18 | offset += 1; 19 | 20 | // Get the appropriate price divisor based on exchange type 21 | const priceDivisor = this.getPriceDivisor(exchangeType); 22 | 23 | // Read token (25 bytes) 24 | const tokenString = this.decodeToken(dataView, offset); 25 | offset += 25; 26 | 27 | // Read sequence number (8 bytes) 28 | const sequenceNumber = dataView.getBigInt64(offset, true); 29 | offset += 8; 30 | 31 | // Read exchange timestamp (8 bytes) 32 | const exchangeTimestamp = dataView.getBigInt64(offset, true); 33 | offset += 8; 34 | 35 | // Read last traded price (8 bytes) 36 | const lastTradedPrice = Number(dataView.getBigInt64(offset, true)) / priceDivisor; 37 | offset += 8; 38 | 39 | // Read last traded quantity (8 bytes) 40 | const lastTradedQuantity = Number(dataView.getBigInt64(offset, true)); 41 | offset += 8; 42 | 43 | // Read average traded price (8 bytes) 44 | const averageTradedPrice = Number(dataView.getBigInt64(offset, true)) / priceDivisor; 45 | offset += 8; 46 | 47 | // Read volume traded (8 bytes) 48 | const volTraded = Number(dataView.getBigInt64(offset, true)); 49 | offset += 8; 50 | 51 | // Read total buy quantity (8 bytes) 52 | const totalBuyQty = dataView.getFloat64(offset, true); 53 | offset += 8; 54 | 55 | // Read total sell quantity (8 bytes) 56 | const totalSellQty = dataView.getFloat64(offset, true); 57 | offset += 8; 58 | 59 | // Read open price (8 bytes) 60 | const openPrice = Number(dataView.getBigInt64(offset, true)) / priceDivisor; 61 | offset += 8; 62 | 63 | // Read high price (8 bytes) 64 | const highPrice = Number(dataView.getBigInt64(offset, true)) / priceDivisor; 65 | offset += 8; 66 | 67 | // Read low price (8 bytes) 68 | const lowPrice = Number(dataView.getBigInt64(offset, true)) / priceDivisor; 69 | offset += 8; 70 | 71 | // Read close price (8 bytes) 72 | const closePrice = Number(dataView.getBigInt64(offset, true)) / priceDivisor; 73 | offset += 8; 74 | 75 | if (subscriptionMode === 3) { 76 | // Skip last traded timestamp, OI, and OI change (24 bytes) 77 | offset += 24; 78 | 79 | // Read Best Five Data (10 packets) 80 | const bestFiveData = []; 81 | for (let i = 0; i < 10; i++) { 82 | const buySellFlag = dataView.getInt16(offset, true); 83 | offset += 2; 84 | 85 | const quantity = Number(dataView.getBigInt64(offset, true)); 86 | offset += 8; 87 | 88 | const price = Number(dataView.getBigInt64(offset, true)) / priceDivisor; 89 | offset += 8; 90 | 91 | const orders = dataView.getInt16(offset, true); 92 | offset += 2; 93 | 94 | bestFiveData.push({ buySellFlag, quantity, price, orders }); 95 | } 96 | 97 | // Filter best five data into buy and sell orders 98 | const bestBids = bestFiveData.filter(order => order.buySellFlag === 1) 99 | .slice(0, 5) 100 | .map(order => ({ 101 | qty: order.quantity, 102 | price: order.price, 103 | numOrders: order.orders 104 | })); 105 | 106 | const bestAsks = bestFiveData.filter(order => order.buySellFlag === 0) 107 | .slice(0, 5) 108 | .map(order => ({ 109 | qty: order.quantity, 110 | price: order.price, 111 | numOrders: order.orders 112 | })); 113 | 114 | return { 115 | subscriptionMode, 116 | exchangeType, 117 | tokenString, 118 | sequenceNumber, 119 | exchangeTimestamp, 120 | lastTradedPrice, 121 | lastTradedQuantity, 122 | averageTradedPrice, 123 | volTraded, 124 | totalBuyQty, 125 | totalSellQty, 126 | openPrice, 127 | highPrice, 128 | lowPrice, 129 | closePrice, 130 | bestBids, 131 | bestAsks 132 | }; 133 | } 134 | 135 | return { 136 | subscriptionMode, 137 | exchangeType, 138 | tokenString, 139 | sequenceNumber, 140 | exchangeTimestamp, 141 | lastTradedPrice, 142 | lastTradedQuantity, 143 | averageTradedPrice, 144 | volTraded, 145 | totalBuyQty, 146 | totalSellQty, 147 | openPrice, 148 | highPrice, 149 | lowPrice, 150 | closePrice 151 | }; 152 | }, 153 | 154 | decodeToken(dataView, offset) { 155 | const token = []; 156 | for (let i = 0; i < 25; i++) { 157 | const charCode = dataView.getInt8(offset + i); 158 | if (charCode !== 0) { 159 | token.push(String.fromCharCode(charCode)); 160 | } 161 | } 162 | return token.join(''); 163 | } 164 | }; 165 | 166 | // Export for use in other modules 167 | window.MarketDataDecoder = MarketDataDecoder; 168 | -------------------------------------------------------------------------------- /templates/positions.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block content %} 4 |
    5 |

    Positions

    6 | 7 |
    8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for position in positions_data %} 24 | {% set net_qty = position.netqty | int %} 25 | 26 | 29 | 39 | 42 | 45 | 53 | 56 | 57 | {% else %} 58 | 59 | 60 | 61 | {% endfor %} 62 | 63 |
    SymbolProductBuy AmountSell AmountNet QtyNet Price
    27 |
    {{ position.tradingsymbol }}
    28 |
    30 | 36 | {{ position.producttype }} 37 | 38 | 40 | ₹{{ position.buyamount | float | round(2) }} 41 | 43 | ₹{{ position.sellamount | float | round(2) }} 44 | 46 | 50 | {{ net_qty }} 51 | 52 | 54 | ₹{{ position.netprice | float | round(2) }} 55 |
    No positions found.
    64 |
    65 |
    66 | 67 | 160 | {% endblock %} -------------------------------------------------------------------------------- /templates/orderbook.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block content %} 4 |
    5 |

    Order Book

    6 | 7 |
    8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for order in order_data %} 28 | 29 | 30 | 31 | 32 | 33 | 43 | 44 | 45 | 46 | 60 | 61 | 62 | {% else %} 63 | 64 | 65 | 66 | {% endfor %} 67 | 68 |
    Order IDSymbolOrder TypeProduct TypeTransactionPriceTriggerQtyStatusTime
    {{ order.orderid }}{{ order.tradingsymbol }}{{ order.ordertype }}{{ order.producttype }} 34 | 40 | {{ order.transactiontype }} 41 | 42 | {{ order.price | float | round(2) }}{{ order.triggerprice | float | round(2) }}{{ order.quantity }} 47 | 57 | {{ order.orderstatus }} 58 | 59 | {{ order.updatetime }}
    No orders found.
    69 |
    70 |
    71 | 72 | 144 | {% endblock %} -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | # models.py 2 | 3 | from extensions import db 4 | from datetime import datetime 5 | 6 | class User(db.Model): 7 | id = db.Column(db.Integer, primary_key=True) 8 | username = db.Column(db.String(150), nullable=False) 9 | client_id = db.Column(db.String(50), nullable=False, unique=True) 10 | api_key = db.Column(db.String(100), nullable=False) 11 | 12 | # Token fields 13 | access_token = db.Column(db.String(1000), nullable=True) 14 | feed_token = db.Column(db.String(1000), nullable=True) 15 | refresh_token = db.Column(db.String(1000), nullable=True) 16 | 17 | # Session tracking 18 | token_expiry = db.Column(db.DateTime, nullable=True) 19 | last_login = db.Column(db.DateTime, nullable=True, default=datetime.utcnow) 20 | last_activity = db.Column(db.DateTime, nullable=True) 21 | 22 | # Relationships 23 | watchlists = db.relationship('Watchlist', backref='user', lazy=True) 24 | settings = db.relationship('UserSettings', backref='user', uselist=False, lazy=True) 25 | 26 | def __repr__(self): 27 | return f'' 28 | 29 | def to_dict(self): 30 | """Convert user object to dictionary""" 31 | return { 32 | 'id': self.id, 33 | 'username': self.username, 34 | 'client_id': self.client_id, 35 | 'last_login': self.last_login.isoformat() if self.last_login else None, 36 | 'has_active_session': bool(self.access_token and self.feed_token) 37 | } 38 | 39 | class UserSettings(db.Model): 40 | id = db.Column(db.Integer, primary_key=True) 41 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, unique=True) 42 | 43 | # Voice Trading Settings 44 | voice_activate_commands = db.Column(db.String(500), nullable=False, default='["MILO"]') 45 | groq_api_key = db.Column(db.String(100), nullable=True) 46 | preferred_exchange = db.Column(db.String(10), nullable=False, default='NSE') 47 | preferred_product_type = db.Column(db.String(10), nullable=False, default='MIS') 48 | preferred_model = db.Column(db.String(50), nullable=False, default='whisper-large-v3') 49 | 50 | # Trading Symbol Mappings stored as JSON string 51 | trading_symbols_mapping = db.Column(db.Text, nullable=False, default='{}') 52 | 53 | # Watchlist Display Settings 54 | show_ltp_change = db.Column(db.Boolean, default=True) 55 | show_ltp_change_percent = db.Column(db.Boolean, default=True) 56 | show_holdings = db.Column(db.Boolean, default=True) 57 | 58 | def __repr__(self): 59 | return f'' 60 | 61 | class Watchlist(db.Model): 62 | id = db.Column(db.Integer, primary_key=True) 63 | name = db.Column(db.String(100), nullable=False) 64 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) 65 | items = db.relationship('WatchlistItem', backref='watchlist', lazy=True) 66 | 67 | def __repr__(self): 68 | return f'' 69 | 70 | class WatchlistItem(db.Model): 71 | id = db.Column(db.Integer, primary_key=True) 72 | watchlist_id = db.Column(db.Integer, db.ForeignKey('watchlist.id'), nullable=False) 73 | symbol = db.Column(db.String(150), nullable=False) 74 | name = db.Column(db.String(150), nullable=True) 75 | token = db.Column(db.String(50), nullable=True) 76 | expiry = db.Column(db.String(50), nullable=True) 77 | strike = db.Column(db.Float, nullable=True) 78 | lotsize = db.Column(db.Integer, nullable=True) 79 | instrumenttype = db.Column(db.String(50), nullable=True) 80 | exch_seg = db.Column(db.String(10), nullable=False) 81 | tick_size = db.Column(db.Float, nullable=True) 82 | 83 | def __repr__(self): 84 | return f'' 85 | 86 | class Instrument(db.Model): 87 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 88 | token = db.Column(db.String(50), nullable=False) 89 | symbol = db.Column(db.String(150), nullable=False) 90 | name = db.Column(db.String(150), nullable=True) 91 | expiry = db.Column(db.String(50), nullable=True) 92 | strike = db.Column(db.Float, nullable=True) 93 | lotsize = db.Column(db.Integer, nullable=True) 94 | instrumenttype = db.Column(db.String(50), nullable=True) 95 | exch_seg = db.Column(db.String(10), nullable=False) 96 | tick_size = db.Column(db.Float, nullable=True) 97 | 98 | def __repr__(self): 99 | return f'' 100 | 101 | class OrderLog(db.Model): 102 | id = db.Column(db.Integer, primary_key=True) 103 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) 104 | order_id = db.Column(db.String(100), nullable=False) 105 | timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 106 | symbol = db.Column(db.String(50), nullable=False) 107 | exchange = db.Column(db.String(10), nullable=False) 108 | order_type = db.Column(db.String(20), nullable=False) 109 | transaction_type = db.Column(db.String(10), nullable=False) 110 | product_type = db.Column(db.String(20), nullable=False) 111 | quantity = db.Column(db.Integer, nullable=False) 112 | price = db.Column(db.Float, nullable=True) 113 | trigger_price = db.Column(db.Float, nullable=True) 114 | status = db.Column(db.String(20), nullable=False) 115 | message = db.Column(db.String(255), nullable=True) 116 | order_source = db.Column(db.String(20), nullable=False, default='REGULAR') # Added field for order source 117 | 118 | def to_dict(self): 119 | return { 120 | 'id': self.id, 121 | 'order_id': self.order_id, 122 | 'timestamp': self.timestamp.isoformat(), 123 | 'symbol': self.symbol, 124 | 'exchange': self.exchange, 125 | 'order_type': self.order_type, 126 | 'transaction_type': self.transaction_type, 127 | 'product_type': self.product_type, 128 | 'quantity': self.quantity, 129 | 'price': self.price, 130 | 'trigger_price': self.trigger_price, 131 | 'status': self.status, 132 | 'message': self.message, 133 | 'order_source': self.order_source 134 | } 135 | -------------------------------------------------------------------------------- /extensions.py: -------------------------------------------------------------------------------- 1 | # extensions.py 2 | 3 | from flask_sqlalchemy import SQLAlchemy 4 | from redis import Redis 5 | from datetime import datetime, timedelta 6 | import json 7 | 8 | # Initialize SQLAlchemy 9 | db = SQLAlchemy() 10 | 11 | # Initialize Redis with connection pool 12 | redis_client = Redis( 13 | host='localhost', # or your Redis host 14 | port=6379, # default Redis port 15 | db=0, # database number 16 | decode_responses=True, # automatically decode responses to Python strings 17 | socket_timeout=5 # timeout in seconds 18 | ) 19 | 20 | class RedisTokenManager: 21 | """Helper class for managing tokens in Redis""" 22 | 23 | @staticmethod 24 | def store_tokens(client_id, access_token, feed_token, expiry=86400): 25 | """Store user tokens in Redis with expiration""" 26 | try: 27 | user_key = f"user:{client_id}" 28 | token_data = { 29 | "access_token": access_token, 30 | "feed_token": feed_token, 31 | "created_at": datetime.now().timestamp() 32 | } 33 | 34 | # Store tokens 35 | redis_client.hmset(user_key, token_data) 36 | redis_client.expire(user_key, expiry) 37 | 38 | return True 39 | except Exception as e: 40 | print(f"Error storing tokens in Redis: {str(e)}") 41 | return False 42 | 43 | @staticmethod 44 | def get_tokens(client_id): 45 | """Get user tokens from Redis""" 46 | try: 47 | user_key = f"user:{client_id}" 48 | token_data = redis_client.hgetall(user_key) 49 | 50 | if not token_data: 51 | return None 52 | 53 | return { 54 | "access_token": token_data.get("access_token"), 55 | "feed_token": token_data.get("feed_token") 56 | } 57 | except Exception as e: 58 | print(f"Error getting tokens from Redis: {str(e)}") 59 | return None 60 | 61 | @staticmethod 62 | def clear_tokens(client_id): 63 | """Clear user tokens from Redis""" 64 | try: 65 | user_key = f"user:{client_id}" 66 | redis_client.delete(user_key) 67 | return True 68 | except Exception as e: 69 | print(f"Error clearing tokens from Redis: {str(e)}") 70 | return False 71 | 72 | class RedisWatchlistManager: 73 | """Helper class for managing watchlist data in Redis""" 74 | 75 | @staticmethod 76 | def store_watchlist(user_id, watchlist_data, expiry=86400): 77 | """Store watchlist data in Redis""" 78 | try: 79 | key = f"user:{user_id}:watchlists" 80 | redis_client.set(key, json.dumps(watchlist_data)) 81 | redis_client.expire(key, expiry) 82 | return True 83 | except Exception as e: 84 | print(f"Error storing watchlist in Redis: {str(e)}") 85 | return False 86 | 87 | @staticmethod 88 | def get_watchlist(user_id): 89 | """Get watchlist data from Redis""" 90 | try: 91 | key = f"user:{user_id}:watchlists" 92 | data = redis_client.get(key) 93 | return json.loads(data) if data else None 94 | except Exception as e: 95 | print(f"Error getting watchlist from Redis: {str(e)}") 96 | return None 97 | 98 | def init_extensions(app): 99 | """Initialize all extensions""" 100 | db.init_app(app) 101 | 102 | # Test Redis connection 103 | try: 104 | redis_client.ping() 105 | print("Redis connection successful") 106 | except Exception as e: 107 | print(f"Redis connection failed: {str(e)}") 108 | # You might want to handle this based on your needs 109 | # For example, fallback to a different cache or raise an error 110 | pass 111 | 112 | def cleanup_expired_sessions(): 113 | """Cleanup expired sessions from Redis""" 114 | try: 115 | pattern = "user:*" 116 | for key in redis_client.scan_iter(match=pattern): 117 | if redis_client.ttl(key) <= 0: 118 | redis_client.delete(key) 119 | except Exception as e: 120 | print(f"Error cleaning up sessions: {str(e)}") 121 | 122 | def get_market_status(): 123 | """Get market status from Redis""" 124 | try: 125 | status = redis_client.get('market_status') 126 | return json.loads(status) if status else None 127 | except Exception as e: 128 | print(f"Error getting market status: {str(e)}") 129 | return None 130 | 131 | # Error handler for Redis operations 132 | class RedisError(Exception): 133 | """Custom exception for Redis operations""" 134 | pass 135 | 136 | # Redis health check 137 | def check_redis_health(): 138 | """Check Redis connection and return status""" 139 | try: 140 | redis_client.ping() 141 | return { 142 | 'status': 'healthy', 143 | 'timestamp': datetime.now().isoformat() 144 | } 145 | except Exception as e: 146 | return { 147 | 'status': 'unhealthy', 148 | 'error': str(e), 149 | 'timestamp': datetime.now().isoformat() 150 | } 151 | 152 | # Add new Redis methods for order management 153 | class RedisOrderManager: 154 | """Helper class for managing order data in Redis""" 155 | 156 | @staticmethod 157 | def store_order_data(order_id, order_data, expiry=86400): 158 | """Store order data in Redis""" 159 | try: 160 | key = f"order:{order_id}" 161 | redis_client.setex(key, expiry, json.dumps(order_data)) 162 | return True 163 | except Exception as e: 164 | print(f"Error storing order data in Redis: {str(e)}") 165 | return False 166 | 167 | @staticmethod 168 | def get_order_data(order_id): 169 | """Get order data from Redis""" 170 | try: 171 | key = f"order:{order_id}" 172 | data = redis_client.get(key) 173 | return json.loads(data) if data else None 174 | except Exception as e: 175 | print(f"Error getting order data from Redis: {str(e)}") 176 | return None -------------------------------------------------------------------------------- /templates/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | About - Open Terminal 7 | 134 | 135 | 136 |
    137 |
    138 |

    About Open Terminal

    139 |

    Empowering Traders with Precision and Speed

    140 |
    141 |
    142 | 143 |
    144 |
    145 |
    146 |

    Realtime Watchlists

    147 |

    Stay updated with live market data and customizable watchlists. Monitor your favorite securities in real-time with advanced filtering and sorting capabilities.

    148 |
    149 | 150 |
    151 |

    Placing Orders from Watchlists

    152 |

    Easily place trades directly from your real-time watchlists. Execute trades with a single click while monitoring market movements.

    153 |
    154 | 155 |
    156 |

    Order Book & Holdings

    157 |

    Manage your Order Book, Trade Book, Position Book, and Holdings efficiently. Get a comprehensive view of your trading activity and portfolio.

    158 |
    159 | 160 |
    161 |

    Trailing Stoploss & MTM Square Off

    162 |

    Automatically manage risk with trailing stoploss orders and MTM-based square off. Protect your profits and limit losses with advanced order types.

    163 |
    164 | 165 |
    166 |

    Voice-Based Trading

    167 |

    Trade seamlessly using our voice-enabled trading system. Execute orders hands-free with natural language commands.

    168 |
    169 | 170 |
    171 |

    Keyboard-Based Orders

    172 |

    Execute orders swiftly using keyboard shortcuts for faster trades. Optimize your trading workflow with quick access commands.

    173 |
    174 |
    175 | 176 |
    177 | Back to Home 178 |
    179 |
    180 | 181 | 182 | -------------------------------------------------------------------------------- /templates/components/orders/_order_form.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 | 11 |
    12 | 21 | 22 | 30 |
    31 |
    32 | 33 | 34 |
    35 |
    36 | 37 | 40 |
    41 |
    42 | 43 | 47 | 48 |
    49 |
    50 | 51 | 52 |
    53 | 54 |
    55 |
    56 | 57 | LTP: 0.00 58 |
    59 |
    60 | 69 | 77 |
    78 |
    79 | 80 | 81 |
    82 |
    83 | 84 | 90 | 91 |
    92 | 93 | 94 |
    95 | 96 | 97 | 98 | 99 |
    100 |
    101 |
    102 | 103 | 104 | 115 | 116 | 117 | 131 | 132 | 133 | 136 | 137 | 138 | 141 |
    -------------------------------------------------------------------------------- /routes/dashboard/routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, session, redirect, url_for, flash, request, jsonify 2 | from models import User, Watchlist 3 | from .watchlist_service import WatchlistService 4 | from .settings_service import SettingsService 5 | from .market_data_service import MarketDataService 6 | 7 | dashboard_bp = Blueprint('dashboard', __name__) 8 | watchlist_service = WatchlistService() 9 | settings_service = SettingsService() 10 | market_data_service = MarketDataService() 11 | 12 | @dashboard_bp.route('/dashboard') 13 | def dashboard(): 14 | if 'client_id' not in session: 15 | flash('Please log in to access this page.', 'danger') 16 | return redirect(url_for('auth.login')) 17 | 18 | user = User.query.filter_by(client_id=session['client_id']).first() 19 | watchlists_data = watchlist_service.get_user_watchlists(user.id) 20 | settings = settings_service.get_user_settings(user.id) 21 | 22 | return render_template('dashboard.html', 23 | watchlists=watchlists_data, 24 | settings=settings) 25 | 26 | @dashboard_bp.route('/create_watchlist', methods=['POST']) 27 | def create_watchlist(): 28 | if 'client_id' not in session: 29 | return jsonify({'status': 'error', 'message': 'Not logged in'}), 401 30 | 31 | user = User.query.filter_by(client_id=session['client_id']).first() 32 | data = request.get_json() 33 | 34 | result = watchlist_service.create_watchlist(user.id, data.get('name')) 35 | if 'error' in result: 36 | return jsonify({'status': 'error', 'message': result['error']}), result.get('code', 500) 37 | 38 | return jsonify({ 39 | 'status': 'success', 40 | 'watchlist_id': result['watchlist_id'], 41 | 'data': result['data'] 42 | }) 43 | 44 | @dashboard_bp.route('/update_watchlist', methods=['POST']) 45 | def update_watchlist(): 46 | if 'client_id' not in session: 47 | return jsonify({'status': 'error', 'message': 'Not logged in'}), 401 48 | 49 | user = User.query.filter_by(client_id=session['client_id']).first() 50 | data = request.get_json() 51 | 52 | result = watchlist_service.update_watchlist( 53 | user.id, 54 | data.get('watchlist_id'), 55 | data.get('name') 56 | ) 57 | 58 | if 'error' in result: 59 | return jsonify({'status': 'error', 'message': result['error']}), result.get('code', 500) 60 | 61 | return jsonify({'status': 'success'}) 62 | 63 | @dashboard_bp.route('/add_watchlist_item', methods=['POST']) 64 | def add_watchlist_item(): 65 | if 'client_id' not in session: 66 | return jsonify({'status': 'error', 'message': 'Not logged in'}), 401 67 | 68 | user = User.query.filter_by(client_id=session['client_id']).first() 69 | data = request.get_json() 70 | 71 | result = watchlist_service.add_watchlist_item( 72 | user.id, 73 | data.get('watchlist_id'), 74 | data.get('symbol'), 75 | data.get('exch_seg') 76 | ) 77 | 78 | if 'error' in result: 79 | return jsonify({'status': 'error', 'message': result['error']}), result.get('code', 500) 80 | 81 | return jsonify({ 82 | 'status': 'success', 83 | 'data': result['data'] 84 | }) 85 | 86 | @dashboard_bp.route('/delete_watchlist', methods=['POST']) 87 | def delete_watchlist(): 88 | if 'client_id' not in session: 89 | return jsonify({'status': 'error', 'message': 'Not logged in'}), 401 90 | 91 | user = User.query.filter_by(client_id=session['client_id']).first() 92 | data = request.get_json() 93 | watchlist_id = data.get('watchlist_id') 94 | 95 | result = watchlist_service.delete_watchlist(user.id, watchlist_id) 96 | 97 | if 'error' in result: 98 | return jsonify({'status': 'error', 'message': result['error']}), result.get('code', 500) 99 | 100 | return jsonify({ 101 | 'status': 'success', 102 | 'watchlist_id': result['watchlist_id'] 103 | }) 104 | 105 | @dashboard_bp.route('/remove_watchlist_item', methods=['POST']) 106 | def remove_watchlist_item(): 107 | if 'client_id' not in session: 108 | return jsonify({'status': 'error', 'message': 'Not logged in'}), 401 109 | 110 | user = User.query.filter_by(client_id=session['client_id']).first() 111 | data = request.get_json() 112 | 113 | result = watchlist_service.remove_watchlist_item(user.id, data.get('item_id')) 114 | if 'error' in result: 115 | return jsonify({'status': 'error', 'message': result['error']}), result.get('code', 500) 116 | 117 | return jsonify({'status': 'success'}) 118 | 119 | @dashboard_bp.route('/update_watchlist_settings', methods=['POST']) 120 | def update_watchlist_settings(): 121 | if 'client_id' not in session: 122 | return jsonify({'status': 'error', 'message': 'Not logged in'}), 401 123 | 124 | user = User.query.filter_by(client_id=session['client_id']).first() 125 | result = settings_service.update_settings(user.id, request.get_json()) 126 | 127 | if 'error' in result: 128 | return jsonify({'status': 'error', 'message': result['error']}), result.get('code', 500) 129 | 130 | return jsonify({'status': 'success'}) 131 | 132 | @dashboard_bp.route('/search_symbols', methods=['GET']) 133 | def search_symbols(): 134 | query = request.args.get('q', '') 135 | if not query or len(query) < 2: 136 | return jsonify({'results': []}) 137 | 138 | results = watchlist_service.search_symbols(query) 139 | return jsonify({'results': results}) 140 | 141 | @dashboard_bp.route('/get_indices') 142 | def get_indices(): 143 | return jsonify(market_data_service.get_market_indices()) 144 | 145 | @dashboard_bp.route('/api/get_tokens', methods=['POST']) 146 | def get_tokens(): 147 | if 'client_id' not in session: 148 | return jsonify({'error': 'Not authenticated'}), 401 149 | 150 | result = market_data_service.get_user_tokens(session['client_id']) 151 | if 'error' in result: 152 | return jsonify({'error': result['error']}), result.get('code', 500) 153 | 154 | return jsonify(result) 155 | 156 | @dashboard_bp.route('/api/get_watchlist_tokens', methods=['GET']) 157 | def get_watchlist_tokens(): 158 | if 'client_id' not in session: 159 | return jsonify({'error': 'Not authenticated'}), 401 160 | 161 | result = market_data_service.get_watchlist_tokens(session['client_id']) 162 | if 'error' in result: 163 | return jsonify({'error': result['error']}), result.get('code', 500) 164 | 165 | return jsonify(result) -------------------------------------------------------------------------------- /routes/scalper/services/scalper_service.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import http.client 4 | from models import User, OrderLog, db, Instrument 5 | from typing import Dict 6 | from ...voice.utils.helpers import map_product_type_to_api 7 | from datetime import datetime 8 | import pytz 9 | 10 | logger = logging.getLogger('scalper') 11 | 12 | class ScalperService: 13 | def __init__(self): 14 | self.ist_tz = pytz.timezone('Asia/Kolkata') 15 | 16 | def get_current_ist_time(self): 17 | """Get current time in IST""" 18 | return datetime.now(self.ist_tz) 19 | 20 | def place_order(self, action: str, quantity: int, tradingsymbol: str, 21 | exchange: str, product_type: str, client_id: str) -> Dict: 22 | """Place an order using the Angel One API""" 23 | try: 24 | # Get user and auth token 25 | user = User.query.filter_by(client_id=client_id).first() 26 | if not user: 27 | raise ValueError("User not found") 28 | 29 | auth_token = user.access_token 30 | if not auth_token: 31 | raise ValueError("Authentication token not found") 32 | 33 | # Get symbol token from database 34 | instrument = Instrument.query.filter_by( 35 | symbol=tradingsymbol, 36 | exch_seg=exchange 37 | ).first() 38 | 39 | if not instrument: 40 | raise ValueError(f"Instrument not found for symbol {tradingsymbol} on {exchange}") 41 | 42 | # Map product type to Angel API format 43 | api_product_type = map_product_type_to_api(product_type) 44 | logger.info(f"Mapped product type {product_type} to {api_product_type}") 45 | 46 | # Prepare API request 47 | conn = http.client.HTTPSConnection("apiconnect.angelone.in") 48 | headers = { 49 | 'Authorization': f'Bearer {auth_token}', 50 | 'Content-Type': 'application/json', 51 | 'Accept': 'application/json', 52 | 'X-UserType': 'USER', 53 | 'X-SourceID': 'WEB', 54 | 'X-ClientLocalIP': 'CLIENT_LOCAL_IP', 55 | 'X-ClientPublicIP': 'CLIENT_PUBLIC_IP', 56 | 'X-MACAddress': 'MAC_ADDRESS', 57 | 'X-PrivateKey': user.api_key 58 | } 59 | 60 | # Prepare order payload according to Angel One API docs 61 | payload = { 62 | "variety": "NORMAL", 63 | "tradingsymbol": tradingsymbol, 64 | "symboltoken": instrument.token, 65 | "transactiontype": action, 66 | "exchange": exchange, 67 | "ordertype": "MARKET", 68 | "producttype": api_product_type, 69 | "duration": "DAY", 70 | "quantity": str(quantity), 71 | "price": "0", 72 | "triggerprice": "0" 73 | } 74 | 75 | # Log request 76 | logger.info(f"Placing scalping order - Request: {json.dumps(payload)}") 77 | 78 | try: 79 | # Make API request 80 | conn.request("POST", "/rest/secure/angelbroking/order/v1/placeOrder", 81 | json.dumps(payload), headers) 82 | 83 | response = conn.getresponse() 84 | response_data = json.loads(response.read().decode()) 85 | 86 | # Log response 87 | logger.info(f"Angel One API Response: {json.dumps(response_data)}") 88 | 89 | except Exception as api_error: 90 | logger.error(f"API request error: {str(api_error)}") 91 | raise ValueError(f"API request failed: {str(api_error)}") 92 | 93 | finally: 94 | conn.close() 95 | 96 | # Create order log with IST timestamp 97 | order_log = OrderLog( 98 | user_id=user.id, 99 | order_id=response_data.get('data', {}).get('orderid', 'UNKNOWN'), 100 | symbol=tradingsymbol, 101 | exchange=exchange, 102 | order_type='MARKET', 103 | transaction_type=action, 104 | product_type=product_type, # Store original product type 105 | quantity=quantity, 106 | status=response_data.get('status', 'FAILED'), 107 | message=response_data.get('message', ''), 108 | order_source='SCALPER', 109 | timestamp=self.get_current_ist_time() 110 | ) 111 | db.session.add(order_log) 112 | db.session.commit() 113 | logger.info(f"Order log created: {order_log.order_id}") 114 | 115 | if response_data.get('status'): 116 | return { 117 | "status": "success", 118 | "orderid": response_data['data']['orderid'], 119 | "message": response_data['message'] 120 | } 121 | else: 122 | logger.error(f"Order failed: {response_data.get('message')}") 123 | return { 124 | "status": "error", 125 | "message": response_data.get('message', 'Order placement failed') 126 | } 127 | 128 | except Exception as e: 129 | error_msg = str(e) 130 | logger.error(f"Error placing scalping order: {error_msg}") 131 | 132 | # Log failed order attempt 133 | try: 134 | if 'user' in locals() and user: 135 | order_log = OrderLog( 136 | user_id=user.id, 137 | order_id='FAILED', 138 | symbol=tradingsymbol, 139 | exchange=exchange, 140 | order_type='MARKET', 141 | transaction_type=action, 142 | product_type=product_type, 143 | quantity=quantity, 144 | status='FAILED', 145 | message=error_msg, 146 | order_source='SCALPER', 147 | timestamp=self.get_current_ist_time() 148 | ) 149 | db.session.add(order_log) 150 | db.session.commit() 151 | logger.info("Failed order logged") 152 | except Exception as log_error: 153 | logger.error(f"Error logging failed order: {str(log_error)}") 154 | 155 | return {"error": error_msg} 156 | -------------------------------------------------------------------------------- /routes/orders/validators/exchange_rules.py: -------------------------------------------------------------------------------- 1 | # routes/orders/validators/exchange_rules.py 2 | from typing import Dict 3 | from ..constants import ( 4 | EQUITY_SEGMENTS, DERIVATIVE_SEGMENTS, 5 | MARKET, LIMIT, SL_MARKET, SL_LIMIT 6 | ) 7 | 8 | class ExchangeRules: 9 | def apply_rules(self, order_data: Dict, exchange: str) -> Dict: 10 | """Apply exchange-specific rules to order data""" 11 | try: 12 | processed_data = order_data.copy() 13 | 14 | # Validate exchange 15 | if exchange not in (EQUITY_SEGMENTS + DERIVATIVE_SEGMENTS): 16 | raise ValueError(f"Invalid exchange: {exchange}") 17 | 18 | # Apply exchange specific rules 19 | if exchange in EQUITY_SEGMENTS: 20 | processed_data = self._apply_equity_rules(processed_data) 21 | elif exchange in DERIVATIVE_SEGMENTS: 22 | processed_data = self._apply_derivative_rules(processed_data) 23 | 24 | # Apply common validations 25 | processed_data = self._apply_common_rules(processed_data) 26 | 27 | return processed_data 28 | 29 | except Exception as e: 30 | raise ValueError(f"Exchange rules validation failed: {str(e)}") 31 | 32 | def _apply_equity_rules(self, order_data: Dict) -> Dict: 33 | """Apply rules for equity segment""" 34 | try: 35 | # Validate and convert quantity 36 | quantity = self._validate_integer("quantity", order_data['quantity']) 37 | 38 | # Check minimum quantity 39 | if quantity <= 0: 40 | raise ValueError("Quantity must be greater than 0") 41 | 42 | # Update order data 43 | order_data['quantity'] = str(quantity) 44 | 45 | # Handle prices 46 | if order_data.get('ordertype') == LIMIT: 47 | order_data['price'] = self._format_price(order_data.get('price', 0)) 48 | 49 | if order_data.get('variety') == 'STOPLOSS': 50 | order_data['triggerprice'] = self._format_price(order_data.get('triggerprice', 0)) 51 | 52 | return order_data 53 | 54 | except Exception as e: 55 | raise ValueError(f"Equity rules validation failed: {str(e)}") 56 | 57 | def _apply_derivative_rules(self, order_data: Dict) -> Dict: 58 | """Apply rules for derivative segment""" 59 | try: 60 | # Get lot size 61 | lot_size = int(order_data.get('lot_size', 1)) 62 | if lot_size <= 0: 63 | raise ValueError("Invalid lot size") 64 | 65 | # For derivatives, input quantity is in lots 66 | lots = self._validate_integer("quantity", order_data['quantity']) 67 | if lots <= 0: 68 | raise ValueError("Number of lots must be greater than 0") 69 | 70 | # Calculate actual quantity 71 | total_quantity = lots * lot_size 72 | order_data['quantity'] = str(total_quantity) 73 | 74 | # Validate tick size for price 75 | tick_size = float(order_data.get('tick_size', 0.05)) 76 | 77 | # Handle prices based on order type 78 | if order_data.get('ordertype') == LIMIT: 79 | price = float(order_data.get('price', 0)) 80 | if price % tick_size != 0: 81 | price = round(price - (price % tick_size), 2) 82 | order_data['price'] = str(price) 83 | 84 | # Handle trigger price for STOPLOSS orders 85 | if order_data.get('variety') == 'STOPLOSS': 86 | trigger_price = float(order_data.get('triggerprice', 0)) 87 | if trigger_price % tick_size != 0: 88 | trigger_price = round(trigger_price - (trigger_price % tick_size), 2) 89 | order_data['triggerprice'] = str(trigger_price) 90 | 91 | return order_data 92 | 93 | except Exception as e: 94 | raise ValueError(f"Derivative rules validation failed: {str(e)}") 95 | 96 | def _apply_common_rules(self, order_data: Dict) -> Dict: 97 | """Apply common rules for all exchanges""" 98 | # Ensure all numeric values are strings 99 | numeric_fields = ['quantity', 'price', 'triggerprice', 'disclosedquantity'] 100 | for field in numeric_fields: 101 | if field in order_data: 102 | order_data[field] = str(order_data[field]) 103 | 104 | # Validate product type 105 | product_type = order_data.get('producttype', '').upper() 106 | exchange = order_data.get('exchange', '') 107 | 108 | if exchange in EQUITY_SEGMENTS: 109 | if product_type not in ['DELIVERY', 'INTRADAY']: 110 | raise ValueError("Invalid product type for equity segment") 111 | elif exchange in DERIVATIVE_SEGMENTS: 112 | if product_type not in ['CARRYFORWARD', 'INTRADAY']: 113 | raise ValueError("Invalid product type for derivative segment") 114 | 115 | return order_data 116 | 117 | def _validate_integer(self, field_name: str, value: any) -> int: 118 | """Validate and convert to integer""" 119 | try: 120 | return int(float(value)) 121 | except (ValueError, TypeError): 122 | raise ValueError(f"Invalid {field_name}: must be a valid number") 123 | 124 | def _format_price(self, price: any) -> str: 125 | """Format price to string with 2 decimal places""" 126 | try: 127 | return str(round(float(price), 2)) 128 | except (ValueError, TypeError): 129 | raise ValueError("Invalid price: must be a valid number") 130 | 131 | def get_exchange_limits(self, exchange: str) -> Dict: 132 | """Get exchange-specific limits""" 133 | limits = { 134 | 'NSE': { 135 | 'max_order_value': 10000000, # 1 crore 136 | 'max_quantity': 500000, 137 | 'price_ticks': 0.05 138 | }, 139 | 'BSE': { 140 | 'max_order_value': 10000000, # 1 crore 141 | 'max_quantity': 500000, 142 | 'price_ticks': 0.05 143 | }, 144 | 'NFO': { 145 | 'max_order_value': 100000000, # 10 crore 146 | 'max_quantity': 10000, 147 | 'price_ticks': 0.05 148 | }, 149 | 'MCX': { 150 | 'max_order_value': 100000000, # 10 crore 151 | 'max_quantity': 10000, 152 | 'price_ticks': 0.05 153 | } 154 | } 155 | return limits.get(exchange, {}) -------------------------------------------------------------------------------- /Proposed Architecture.md: -------------------------------------------------------------------------------- 1 | # Proposed Architecture to Place Order 2 | 3 | ## 1. Directory Structure 4 | ```markdown 5 | openterminal/ 6 | ├── routes/ 7 | │ └── orders/ # Order management module 8 | │ ├── __init__.py 9 | │ ├── routes.py # Order endpoints 10 | │ ├── constants.py # Order-related constants 11 | │ ├── services/ 12 | │ │ ├── order_service.py # Core order logic 13 | │ │ ├── broker_service.py # Angel API integration 14 | │ │ └── market_feed.py # Real-time price updates 15 | │ ├── validators/ 16 | │ │ ├── order_validator.py # Order validation 17 | │ │ └── exchange_rules.py # Exchange-specific rules 18 | │ └── utils/ 19 | │ ├── formatters.py # Data formatting 20 | │ └── helpers.py # Helper functions 21 | 22 | ├── static/js/modules/orderEntry/ # Frontend order module 23 | │ ├── components/ 24 | │ │ ├── OrderForm.js # Form management 25 | │ │ ├── OrderModal.js # Modal logic 26 | │ │ ├── MarketDepth.js # Depth display 27 | │ │ ├── PriceInput.js # Price handling 28 | │ │ └── QuantityInput.js # Quantity handling 29 | │ ├── services/ 30 | │ │ ├── orderApi.js # API communication 31 | │ │ ├── priceService.js # Price updates 32 | │ │ └── orderState.js # State management 33 | │ └── utils/ 34 | │ ├── validators.js # Client validation 35 | │ └── formatters.js # Data formatting 36 | 37 | └── templates/orders/ # Order templates 38 | ├── _order_modal.html # Modal template 39 | ├── _market_depth.html # Depth template 40 | └── _order_form.html # Form template 41 | ``` 42 | 43 | ## 2. Key Components 44 | 45 | ### 2.1 Backend Services 46 | 47 | #### Order Service (`order_service.py`) 48 | - Main service for order processing 49 | - Handles order validation and placement 50 | - Manages interaction with broker API 51 | - Implements order type specific logic 52 | 53 | ```python 54 | class OrderService: 55 | def place_order(self, order_data) 56 | def validate_order(self, order_data) 57 | def process_order_response(self, response) 58 | def handle_order_errors(self, error) 59 | ``` 60 | 61 | #### Broker Service (`broker_service.py`) 62 | - Handles Angel One API integration 63 | - Manages authentication 64 | - Formats requests for broker API 65 | - Processes API responses 66 | 67 | ```python 68 | class BrokerService: 69 | def prepare_order_request(self, order_data) 70 | def send_order_request(self, request_data) 71 | def handle_broker_response(self, response) 72 | def validate_broker_session(self) 73 | ``` 74 | 75 | #### Market Feed Service (`market_feed.py`) 76 | - Real-time price updates 77 | - Market depth management 78 | - WebSocket integration 79 | - Price validation 80 | 81 | ### 2.2 Frontend Components 82 | 83 | #### Order Modal (`OrderModal.js`) 84 | - Manages modal state 85 | - Handles order form display 86 | - Integrates market depth 87 | - Manages user interactions 88 | 89 | #### Order Form (`OrderForm.js`) 90 | - Form state management 91 | - Field validation 92 | - Order submission 93 | - Error handling 94 | 95 | #### Market Depth Component (`MarketDepth.js`) 96 | - Displays depth data 97 | - Real-time updates 98 | - Price level visualization 99 | - Order book display 100 | 101 | ## 3. Data Flow 102 | 103 | ### 3.1 Order Placement Flow 104 | 1. User initiates order from watchlist 105 | 2. Order modal opens with symbol data 106 | 3. User inputs order details 107 | 4. Client-side validation 108 | 5. Order submission to backend 109 | 6. Backend validation 110 | 7. Broker API submission 111 | 8. Response handling and user feedback 112 | 113 | ### 3.2 Market Data Flow 114 | 1. WebSocket connection established 115 | 2. Real-time price updates received 116 | 3. Market depth data processed 117 | 4. UI updates with latest data 118 | 5. Price validation against order 119 | 120 | ## 4. Key Features 121 | 122 | ### 4.1 Order Types Support 123 | - Regular orders 124 | - Stop loss orders 125 | - Market/Limit orders 126 | - Product type handling (INTRADAY/DELIVERY/CARRYFORWARD) 127 | 128 | ### 4.2 Exchange-Specific Logic 129 | - NSE/BSE quantity handling 130 | - F&O lot size calculations 131 | - Exchange-specific validations 132 | - Trading hour validations 133 | 134 | ### 4.3 Validation Rules 135 | - Price range checks 136 | - Quantity validations 137 | - Order value limits 138 | - Exchange-specific rules 139 | 140 | ### 4.4 Error Handling 141 | - Network errors 142 | - Validation errors 143 | - Broker API errors 144 | - Session expiry 145 | 146 | ## 5. Integration Points 147 | 148 | ### 5.1 Watchlist Integration 149 | - Order initiation from watchlist 150 | - Symbol data transfer 151 | - Market data sharing 152 | - State management 153 | 154 | ### 5.2 Market Data Integration 155 | - Real-time price updates 156 | - Market depth data 157 | - WebSocket management 158 | - Data synchronization 159 | 160 | ### 5.3 Authentication Integration 161 | - Session management 162 | - Token handling 163 | - Authorization checks 164 | - User verification 165 | 166 | ## 6. Implementation Plan 167 | 168 | ### Phase 1: Core Structure 169 | 1. Set up directory structure 170 | 2. Create basic components 171 | 3. Implement service shells 172 | 4. Set up routing 173 | 174 | ### Phase 2: Order Management 175 | 1. Implement order form 176 | 2. Add validation logic 177 | 3. Create broker service 178 | 4. Build error handling 179 | 180 | ### Phase 3: Market Data 181 | 1. WebSocket integration 182 | 2. Market depth display 183 | 3. Real-time updates 184 | 4. Price validation 185 | 186 | ### Phase 4: Testing & Refinement 187 | 1. Unit testing 188 | 2. Integration testing 189 | 3. Error scenario testing 190 | 4. Performance optimization 191 | 192 | ## 7. Configuration 193 | 194 | ### 7.1 Environment Variables 195 | ```python 196 | BROKER_API_URL= 197 | BROKER_API_VERSION= 198 | WEBSOCKET_URL= 199 | LOG_LEVEL= 200 | ``` 201 | 202 | ### 7.2 Constants 203 | ```python 204 | ORDER_TYPES = ['REGULAR', 'STOPLOSS'] 205 | PRODUCT_TYPES = ['INTRADAY', 'DELIVERY', 'CARRYFORWARD'] 206 | EXCHANGES = ['NSE', 'BSE', 'NFO', 'MCX', 'CDS'] 207 | ``` 208 | 209 | ## 8. Error Handling 210 | 211 | ### 8.1 Error Types 212 | - ValidationError 213 | - BrokerAPIError 214 | - NetworkError 215 | - AuthenticationError 216 | 217 | ### 8.2 Error Responses 218 | ```json 219 | { 220 | "status": "error", 221 | "code": "ERROR_CODE", 222 | "message": "User friendly message", 223 | "details": "Technical details" 224 | } 225 | ``` 226 | 227 | ## 9. Logging 228 | 229 | ### 9.1 Log Levels 230 | - INFO: Normal operations 231 | - WARNING: Potential issues 232 | - ERROR: Operation failures 233 | - DEBUG: Development details 234 | 235 | ### 9.2 Log Format 236 | ```python 237 | { 238 | "timestamp": "", 239 | "level": "", 240 | "order_id": "", 241 | "user_id": "", 242 | "action": "", 243 | "details": {} 244 | } 245 | ``` -------------------------------------------------------------------------------- /templates/funds.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block content %} 4 |
    5 |

    Fund Details

    6 | 7 | 8 |
    9 | 10 |
    11 |
    12 | 13 | 14 | 15 |
    16 |
    17 |
    Net
    18 |
    ₹{{ fund_data.net | float | round(2) }}
    19 |
    20 |
    21 | 22 | 23 |
    24 |
    25 | 26 | 27 | 28 |
    29 |
    30 |
    Collateral
    31 |
    ₹{{ fund_data.collateral | float | round(2) }}
    32 |
    33 |
    34 | 35 | 36 |
    37 |
    38 | 39 | 40 | 41 |
    42 |
    43 |
    M2M Unrealized
    44 |
    ₹{{ fund_data.m2munrealized | float | round(2) }}
    45 |
    46 |
    47 | 48 | 49 |
    50 |
    51 | 52 | 53 | 54 |
    55 |
    56 |
    M2M Realized
    57 |
    ₹{{ fund_data.m2mrealized | float | round(2) }}
    58 |
    59 |
    60 | 61 | 62 |
    63 |
    64 | 65 | 66 | 67 |
    68 |
    69 |
    Utilized Debits
    70 |
    ₹{{ fund_data.utiliseddebits | float | round(2) }}
    71 |
    72 |
    73 |
    74 |
    75 | 76 | 154 | 155 | 170 | {% endblock %} -------------------------------------------------------------------------------- /routes/dashboard/market_data_service.py: -------------------------------------------------------------------------------- 1 | from models import User, Watchlist 2 | from extensions import redis_client 3 | from datetime import datetime 4 | import json 5 | from .utils import cache_key, get_cached_data, set_cached_data 6 | 7 | class MarketDataService: 8 | def __init__(self): 9 | self.exchange_map = { 10 | 'NSE': 1, # nse_cm 11 | 'NFO': 2, # nse_fo 12 | 'BSE': 3, # bse_cm 13 | 'BFO': 4, # bse_fo 14 | 'MCX': 5, # mcx_fo 15 | 'NCX': 7, # ncx_fo 16 | 'CDS': 13 # cde_fo 17 | } 18 | # Add index tokens 19 | self.index_tokens = [ 20 | {"token": "99926000", "symbol": "Nifty 50", "name": "NIFTY", "exch_seg": "NSE"}, 21 | {"token": "99919000", "symbol": "SENSEX", "name": "SENSEX", "exch_seg": "BSE"} 22 | ] 23 | 24 | def get_user_tokens(self, client_id): 25 | """Get WebSocket tokens for market data streaming""" 26 | try: 27 | # Try Redis first 28 | user_data = redis_client.hgetall(f"user:{client_id}") 29 | 30 | if user_data and user_data.get('feed_token'): 31 | return { 32 | 'feed_token': user_data['feed_token'], 33 | 'api_key': user_data['api_key'], 34 | 'client_code': client_id 35 | } 36 | 37 | # Fallback to database 38 | user = User.query.filter_by(client_id=client_id).first() 39 | if not user or not user.feed_token: 40 | return {'error': 'Tokens not available', 'code': 404} 41 | 42 | return { 43 | 'feed_token': user.feed_token, 44 | 'api_key': user.api_key, 45 | 'client_code': client_id 46 | } 47 | 48 | except Exception as e: 49 | return {'error': str(e), 'code': 500} 50 | 51 | def get_watchlist_tokens(self, client_id): 52 | """Get all watchlist tokens for WebSocket subscription""" 53 | try: 54 | user = User.query.filter_by(client_id=client_id).first() 55 | if not user: 56 | return {'error': 'User not found', 'code': 404} 57 | 58 | redis_key = cache_key('user', f'{user.id}:watchlists') 59 | watchlists_data = get_cached_data(redis_key) 60 | 61 | if not watchlists_data: 62 | # Update Redis cache from database 63 | watchlists = Watchlist.query.filter_by(user_id=user.id).all() 64 | watchlists_data = self._format_watchlist_data(watchlists) 65 | set_cached_data(redis_key, watchlists_data) 66 | 67 | # Add index tokens to subscription data 68 | subscription_data = self._prepare_subscription_data(watchlists_data, include_indices=True) 69 | return {'status': 'success', 'subscription_data': subscription_data} 70 | 71 | except Exception as e: 72 | return {'error': str(e), 'code': 500} 73 | 74 | def get_market_indices(self): 75 | """Get market indices with caching""" 76 | try: 77 | indices_key = cache_key('market', 'indices') 78 | cached_data = get_cached_data(indices_key) 79 | 80 | if cached_data: 81 | return cached_data 82 | 83 | # Return structure for real-time data 84 | return { 85 | 'nifty': { 86 | 'token': '99926000', 87 | 'value': '--', 88 | 'change': '--', 89 | 'change_percent': '--' 90 | }, 91 | 'sensex': { 92 | 'token': '99919000', 93 | 'value': '--', 94 | 'change': '--', 95 | 'change_percent': '--' 96 | }, 97 | 'last_updated': datetime.now().isoformat() 98 | } 99 | 100 | except Exception as e: 101 | print(f"Error fetching indices: {str(e)}") 102 | return self._get_fallback_indices() 103 | 104 | def update_market_indices(self, new_data): 105 | """Update market indices in cache""" 106 | try: 107 | indices_key = cache_key('market', 'indices') 108 | current_data = get_cached_data(indices_key, {}) 109 | 110 | if current_data: 111 | current_data.update(new_data) 112 | else: 113 | current_data = new_data 114 | 115 | current_data['last_updated'] = datetime.now().isoformat() 116 | set_cached_data(indices_key, current_data, expire=60) 117 | return True 118 | 119 | except Exception as e: 120 | print(f"Error updating indices cache: {str(e)}") 121 | return False 122 | 123 | def _format_watchlist_data(self, watchlists): 124 | """Format watchlist data for caching""" 125 | return [{ 126 | 'id': watchlist.id, 127 | 'name': watchlist.name, 128 | 'items_list': [{ 129 | 'id': item.id, 130 | 'symbol': item.symbol, 131 | 'token': item.token, 132 | 'exch_seg': item.exch_seg 133 | } for item in watchlist.items] 134 | } for watchlist in watchlists] 135 | 136 | def _prepare_subscription_data(self, watchlists, include_indices=False): 137 | """Prepare WebSocket subscription data""" 138 | exchange_tokens = {} 139 | 140 | # Add watchlist tokens 141 | for watchlist in watchlists: 142 | for item in watchlist.get('items_list', []): 143 | exch_type = self.exchange_map.get(item['exch_seg']) 144 | if exch_type: 145 | if exch_type not in exchange_tokens: 146 | exchange_tokens[exch_type] = [] 147 | if item['token'] not in exchange_tokens[exch_type]: 148 | exchange_tokens[exch_type].append(item['token']) 149 | 150 | # Add index tokens 151 | if include_indices: 152 | for index in self.index_tokens: 153 | exch_type = self.exchange_map.get(index['exch_seg']) 154 | if exch_type: 155 | if exch_type not in exchange_tokens: 156 | exchange_tokens[exch_type] = [] 157 | if index['token'] not in exchange_tokens[exch_type]: 158 | exchange_tokens[exch_type].append(index['token']) 159 | 160 | return { 161 | "action": 1, 162 | "params": { 163 | "mode": 3, 164 | "tokenList": [ 165 | { 166 | "exchangeType": exchange_type, 167 | "tokens": tokens 168 | } 169 | for exchange_type, tokens in exchange_tokens.items() 170 | ] 171 | } 172 | } 173 | 174 | def _get_fallback_indices(self): 175 | """Return fallback data when unable to fetch indices""" 176 | return { 177 | 'nifty': { 178 | 'token': '99926000', 179 | 'value': '--', 180 | 'change': '--', 181 | 'change_percent': '--' 182 | }, 183 | 'sensex': { 184 | 'token': '99919000', 185 | 'value': '--', 186 | 'change': '--', 187 | 'change_percent': '--' 188 | }, 189 | 'last_updated': datetime.now().isoformat() 190 | } 191 | --------------------------------------------------------------------------------