diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index 6360c12..d0323c5 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -1067,7 +1067,8 @@ class BrighterTrades: api_keys=api_keys) if success: self.users.active_exchange(exchange=exchange_name, user_name=user_name, cmd='set') - if api_keys: + # Check if api_keys has actual key/secret values (not just empty dict) + if api_keys and api_keys.get('key') and api_keys.get('secret'): self.users.update_api_keys(api_keys=api_keys, exchange=exchange_name, user_name=user_name) result['status'] = 'success' result['message'] = f'Successfully connected to {exchange_name}.' @@ -1076,7 +1077,8 @@ class BrighterTrades: result['message'] = f'Failed to connect to {exchange_name}.' else: # Exchange is already connected, check if API keys need updating - if api_keys: + # Check if api_keys has actual key/secret values (not just empty dict) + if api_keys and api_keys.get('key') and api_keys.get('secret'): # Get current API keys current_keys = self.users.get_api_keys(user_name, exchange_name) @@ -1146,6 +1148,7 @@ class BrighterTrades: order_type = get_value('orderType') or get_value('order_type', 'MARKET') quantity = get_value('quantity', 0.0) strategy_id = get_value('strategy_id') + testnet = data.get('testnet', False) # Validate required fields if not symbol: @@ -1162,15 +1165,17 @@ class BrighterTrades: order_type=order_type, qty=quantity, user_id=user_id, - strategy_id=strategy_id + strategy_id=strategy_id, + testnet=testnet ) if status == 'Error': logger.warning(f'Error placing the trade: {result}') return {"success": False, "message": result} + mode_str = 'paper' if target == 'test_exchange' else ('testnet' if testnet else 'production') logger.info(f'Trade order received: target={target}, symbol={symbol}, ' - f'side={side}, type={order_type}, quantity={quantity}, price={price}') + f'side={side}, type={order_type}, quantity={quantity}, price={price}, mode={mode_str}') # Get the created trade trade_obj = self.trades.get_trade_by_id(result) diff --git a/src/Users.py b/src/Users.py index 91cfd15..94db5d2 100644 --- a/src/Users.py +++ b/src/Users.py @@ -147,7 +147,12 @@ class UserAccountManagement(BaseUser): if user_name is None: return False - user = self.get_user_data(user_name) + try: + user = self.get_user_data(user_name) + except ValueError: + # User doesn't exist in database (may have been deleted) + return False + if user is None or user.empty: return False @@ -320,7 +325,7 @@ class UserAccountManagement(BaseUser): raise ValueError("Default user template not found in the database.") # Make a deep copy of the default user to preserve the original template - new_user = copy.deepcopy(default_user) + new_user = copy.deepcopy(default_user).reset_index(drop=True) # Modify the deep copied user template with the provided attributes for attr in attrs: @@ -393,7 +398,7 @@ class UserExchangeManagement(UserAccountManagement): if user is None or user.empty or 'api_keys' not in user.columns: return {} - user_keys = user.loc[0, 'api_keys'] + user_keys = user.iloc[0]['api_keys'] user_keys = json.loads(user_keys) if user_keys else {} return user_keys.get(exchange) @@ -406,7 +411,7 @@ class UserExchangeManagement(UserAccountManagement): :param user_name: The name of the user. """ user = self.get_user_data(user_name) - user_keys = user.loc[0, 'api_keys'] + user_keys = user.iloc[0]['api_keys'] user_keys = json.loads(user_keys) if user_keys else {} user_keys.update({exchange: api_keys}) @@ -414,7 +419,7 @@ class UserExchangeManagement(UserAccountManagement): field_name='api_keys', new_data=json.dumps(user_keys)) - configured_exchanges = json.loads(user.loc[0, 'configured_exchanges']) + configured_exchanges = json.loads(user.iloc[0]['configured_exchanges']) if exchange not in configured_exchanges: configured_exchanges.append(exchange) self.modify_user_data(username=user_name, diff --git a/src/app.py b/src/app.py index c1bef3f..206417d 100644 --- a/src/app.py +++ b/src/app.py @@ -483,6 +483,12 @@ def signout(): return redirect('/') +@app.route('/login', methods=['GET']) +def login_page(): + """Display the login page.""" + return render_template('login_page.html', title='Login') + + @app.route('/login', methods=['POST']) def login(): # Get the user_name and password from the form data @@ -491,8 +497,8 @@ def login(): # Validate the input if not username or not password: - flash('Please provide both user_name and password.') - return redirect('/') + flash('Please provide both username and password.', 'login_error') + return redirect('/login') # Attempt to log in the user success = brighter_trades.log_user_in_out(user_name=username, cmd='login', password=password) @@ -500,11 +506,11 @@ def login(): if success: # Store the user_name in the session session['user'] = username - flash('Login successful!') + flash('Login successful!', 'success') + return redirect('/app') else: - flash('Invalid user_name or password.') - - return redirect('/') + flash('Invalid username or password. Please try again.', 'login_error') + return redirect('/login') @app.route('/signup_submit', methods=['POST']) @@ -517,23 +523,23 @@ def signup_submit(): try: validate_email(email) except EmailNotValidError as e: - flash(message=f"Invalid email format: {e}") - return redirect('/signup') # Redirect back to signup page + flash(f"Invalid email format: {e}", 'signup_error') + return redirect('/signup') # Validate user_name and password if not username or not password: - flash(message="Missing user_name or password") - return redirect('/signup') # Redirect back to signup page + flash("Please provide both username and password.", 'signup_error') + return redirect('/signup') # Create a new user success = brighter_trades.create_new_user(email=email, username=username, password=password) if success: session['user'] = username - flash(message="Signup successful! You are now logged in.") - return redirect('/') # Redirect to the main page + flash("Signup successful! You are now logged in.", 'success') + return redirect('/app') else: - flash(message="An error has occurred during the signup process.") - return redirect('/signup') # Redirect back to signup page + flash("Username or email already exists. Please try a different one.", 'signup_error') + return redirect('/signup') diff --git a/src/static/exchanges.js b/src/static/exchanges.js index 99edfc1..0db2358 100644 --- a/src/static/exchanges.js +++ b/src/static/exchanges.js @@ -119,9 +119,18 @@ class Exchanges { const isKeyValid = this.validateApiKey(key); const isSecretKeyValid = this.validateApiKey(secret_key); - // If it's a public exchange, we don't require API keys. + // For public exchanges, API keys are optional (for higher rate limits) + // For non-public exchanges, API keys are required if (isPublicExchange) { - keys = {}; // Clear keys for public exchanges + // If user provided keys, use them; otherwise connect without keys + if (!key && !secret_key) { + keys = {}; // No keys provided, connect as public + } else if (!isKeyValid || !isSecretKeyValid) { + // User provided partial keys - warn them + this.showStatus('Please enter both API key and secret, or leave both empty for public access.', 'error'); + return; + } + // If both keys are valid, keep them (keys object already set) } else if (!isKeyValid || !isSecretKeyValid) { // Validate keys for non-public exchanges this.showStatus('Please enter a valid API key and secret key.', 'error'); diff --git a/src/static/trade.js b/src/static/trade.js index acc993c..daafb49 100644 --- a/src/static/trade.js +++ b/src/static/trade.js @@ -12,7 +12,23 @@ class TradeUIManager { this.orderTypeSelect = null; this.targetSelect = null; this.sideSelect = null; + this.symbolInput = null; + this.testnetCheckbox = null; + this.testnetRow = null; this.onCloseTrade = null; + + // Exchanges known to support testnet/sandbox mode + this.testnetSupportedExchanges = [ + 'binance', 'binanceus', 'binanceusdm', 'binancecoinm', + 'kucoin', 'kucoinfutures', + 'bybit', + 'okx', 'okex', + 'bitget', + 'bitmex', + 'deribit', + 'phemex', + 'mexc' + ]; } /** @@ -29,7 +45,10 @@ class TradeUIManager { tradeValueId = 'tradeValue', orderTypeId = 'orderType', tradeTargetId = 'tradeTarget', - sideId = 'side' + sideId = 'side', + symbolId = 'tradeSymbol', + testnetId = 'tradeTestnet', + testnetRowId = 'testnet-row' } = config; this.targetEl = document.getElementById(targetId); @@ -49,6 +68,9 @@ class TradeUIManager { this.orderTypeSelect = document.getElementById(orderTypeId); this.targetSelect = document.getElementById(tradeTargetId); this.sideSelect = document.getElementById(sideId); + this.symbolInput = document.getElementById(symbolId); + this.testnetCheckbox = document.getElementById(testnetId); + this.testnetRow = document.getElementById(testnetRowId); // Set up event listeners this._setupFormListeners(); @@ -94,13 +116,91 @@ class TradeUIManager { this.qtyInput.addEventListener('change', updateTradeValue); this.qtyInput.addEventListener('input', updateTradeValue); } + + // Trade target (exchange) changes affect testnet visibility + if (this.targetSelect) { + this.targetSelect.addEventListener('change', () => { + this._updateTestnetVisibility(); + }); + } + } + + /** + * Updates testnet checkbox visibility based on selected exchange. + */ + _updateTestnetVisibility() { + if (!this.testnetRow || !this.targetSelect) return; + + const selectedTarget = this.targetSelect.value; + const isPaperTrade = selectedTarget === 'test_exchange'; + + if (isPaperTrade) { + // Hide testnet row for paper trading + this.testnetRow.style.display = 'none'; + } else { + // Show testnet row for live exchanges + this.testnetRow.style.display = 'block'; + + // Check if this exchange supports testnet + const exchangeId = selectedTarget.toLowerCase(); + const supportsTestnet = this.testnetSupportedExchanges.includes(exchangeId); + + const warningEl = document.getElementById('testnet-warning'); + const unavailableEl = document.getElementById('testnet-unavailable'); + + if (supportsTestnet) { + // Enable testnet checkbox + if (this.testnetCheckbox) { + this.testnetCheckbox.disabled = false; + this.testnetCheckbox.checked = true; + } + if (warningEl) warningEl.style.display = 'block'; + if (unavailableEl) unavailableEl.style.display = 'none'; + } else { + // Disable testnet checkbox - this exchange doesn't support it + if (this.testnetCheckbox) { + this.testnetCheckbox.disabled = true; + this.testnetCheckbox.checked = false; + } + if (warningEl) warningEl.style.display = 'none'; + if (unavailableEl) unavailableEl.style.display = 'block'; + } + } + } + + /** + * Populates the exchange selector with connected exchanges. + * @param {string[]} connectedExchanges - List of connected exchange names. + */ + populateExchangeSelector(connectedExchanges) { + if (!this.targetSelect) return; + + // Clear existing options except Paper Trade + while (this.targetSelect.options.length > 1) { + this.targetSelect.remove(1); + } + + // Add connected exchanges + if (connectedExchanges && connectedExchanges.length > 0) { + for (const exchange of connectedExchanges) { + // Skip 'default' exchange used internally + if (exchange.toLowerCase() === 'default') continue; + + const option = document.createElement('option'); + option.value = exchange.toLowerCase(); + option.textContent = `${exchange} (Live)`; + this.targetSelect.appendChild(option); + } + } } /** * Displays the trade creation form. * @param {number} currentPrice - Optional current price to prefill. + * @param {string} symbol - Optional trading pair to prefill. + * @param {string[]} connectedExchanges - Optional list of connected exchanges. */ - displayForm(currentPrice = null) { + displayForm(currentPrice = null, symbol = null, connectedExchanges = null) { if (!this.formElement) { console.error("Form element not initialized."); return; @@ -116,6 +216,27 @@ class TradeUIManager { if (this.currentPriceDisplay) this.currentPriceDisplay.value = currentPrice; } + // Set symbol if available + if (this.symbolInput && symbol) { + this.symbolInput.value = symbol; + } + + // Populate exchange selector + if (connectedExchanges) { + this.populateExchangeSelector(connectedExchanges); + } + + // Reset to paper trade and hide testnet row + if (this.targetSelect) { + this.targetSelect.value = 'test_exchange'; + } + if (this.testnetRow) { + this.testnetRow.style.display = 'none'; + } + if (this.testnetCheckbox) { + this.testnetCheckbox.checked = true; + } + this.formElement.style.display = 'grid'; } @@ -212,6 +333,20 @@ class TradeUIManager { paperBadge.className = 'trade-paper-badge'; paperBadge.textContent = 'PAPER'; tradeItem.appendChild(paperBadge); + } else if (trade.testnet) { + // Testnet badge for live trades + const testnetBadge = document.createElement('span'); + testnetBadge.className = 'trade-testnet-badge'; + testnetBadge.textContent = 'TESTNET'; + testnetBadge.style.cssText = 'background: #28a745; color: white; padding: 1px 4px; border-radius: 3px; font-size: 9px; position: absolute; top: 4px; left: 4px;'; + tradeItem.appendChild(testnetBadge); + } else if (!trade.is_paper) { + // Production badge for live trades (not paper, not testnet) + const prodBadge = document.createElement('span'); + prodBadge.className = 'trade-prod-badge'; + prodBadge.textContent = 'LIVE'; + prodBadge.style.cssText = 'background: #dc3545; color: white; padding: 1px 4px; border-radius: 3px; font-size: 9px; position: absolute; top: 4px; left: 4px;'; + tradeItem.appendChild(prodBadge); } // Trade info container @@ -277,6 +412,10 @@ class TradeUIManager { hoverHtml += `Target: ${trade.target || 'N/A'}`; if (trade.is_paper) { hoverHtml += `Paper Trade`; + } else if (trade.testnet) { + hoverHtml += `Testnet`; + } else { + hoverHtml += `Production (Live)`; } hoverHtml += ``; @@ -688,7 +827,23 @@ class Trade { if (this.data?.price_history && this.data.price_history.length > 0) { currentPrice = this.data.price_history[this.data.price_history.length - 1].close; } - this.uiManager.displayForm(currentPrice); + + // Get symbol from chart + const symbol = this.data?.trading_pair || ''; + + // Get connected exchanges from UI.exchanges + const connectedExchanges = window.UI?.exchanges?.connected_exchanges || []; + + this.uiManager.displayForm(currentPrice, symbol, connectedExchanges); + } + + /** + * Sets the symbol input to the current chart symbol. + */ + useChartSymbol() { + if (this.uiManager.symbolInput && this.data?.trading_pair) { + this.uiManager.symbolInput.value = this.data.trading_pair; + } } /** @@ -714,10 +869,14 @@ class Trade { } const target = this.uiManager.targetSelect?.value || 'test_exchange'; - const symbol = this.data?.trading_pair || ''; + const symbol = this.uiManager.symbolInput?.value || this.data?.trading_pair || ''; const orderType = this.uiManager.orderTypeSelect?.value || 'MARKET'; const side = this.uiManager.sideSelect?.value || 'buy'; + // Get testnet setting (only relevant for live exchanges) + const isPaperTrade = target === 'test_exchange'; + const testnet = isPaperTrade ? false : (this.uiManager.testnetCheckbox?.checked ?? true); + let price; if (orderType === 'MARKET') { price = parseFloat(this.uiManager.currentPriceDisplay?.value || 0); @@ -729,7 +888,7 @@ class Trade { // Validation if (!symbol) { - alert('Please select a trading pair first.'); + alert('Please enter a trading pair.'); return; } if (quantity <= 0) { @@ -741,6 +900,22 @@ class Trade { return; } + // Show confirmation for production live trades + if (!isPaperTrade && !testnet) { + const proceed = confirm( + "WARNING: PRODUCTION MODE\n\n" + + "You are about to execute a LIVE trade with REAL MONEY.\n\n" + + `Exchange: ${target}\n` + + `Symbol: ${symbol}\n` + + `Side: ${side.toUpperCase()}\n` + + `Quantity: ${quantity}\n\n` + + "Are you sure you want to proceed?" + ); + if (!proceed) { + return; + } + } + const tradeData = { target, symbol, @@ -748,6 +923,7 @@ class Trade { side, orderType, quantity, + testnet, user_name: this.data?.user_name }; diff --git a/src/templates/login_page.html b/src/templates/login_page.html new file mode 100644 index 0000000..feef57c --- /dev/null +++ b/src/templates/login_page.html @@ -0,0 +1,180 @@ + + +
+ + + + +
+ + Don't have an account? Sign up here +
+ + ++ Back to Welcome +
+