Fix auth UX, exchange config, and trade validation

Auth/Login improvements:
- Add dedicated login page (login_page.html)
- Redirect to /app after successful login/signup instead of welcome
- Add flash message categories to prevent cross-page message pollution
- Filter flash messages by category in templates

Exchange configuration fixes:
- Fix JS discarding API keys for public exchanges (now optional, not cleared)
- Add explicit key/secret validation in backend before saving
- Properly update configured_exchanges when keys provided

Trade dialog enhancements:
- Add exchange selector populated from connected exchanges
- Add testnet checkbox with exchange support detection
- Add production trading confirmation dialog
- Show TESTNET/LIVE badges on trade cards
- Validate exchange is configured before creating live trades

Bug fixes:
- Add try/except around get_user_data in is_logged_in()
- Fix DataFrame index access for user data retrieval

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-03-02 20:59:07 -04:00
parent 45395c86c5
commit 33de4affb4
10 changed files with 597 additions and 65 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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')

View File

@ -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');

View File

@ -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 += `<span>Target: ${trade.target || 'N/A'}</span>`;
if (trade.is_paper) {
hoverHtml += `<span class="trade-paper-indicator">Paper Trade</span>`;
} else if (trade.testnet) {
hoverHtml += `<span class="trade-testnet-indicator" style="color: #28a745;">Testnet</span>`;
} else {
hoverHtml += `<span class="trade-prod-indicator" style="color: #dc3545;">Production (Live)</span>`;
}
hoverHtml += `</div>`;
@ -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
};

View File

@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='brighterStyles.css') }}">
<title>{{ title }} | BrighterTrades</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css" rel="stylesheet">
</head>
<body class="bg-dark text-light">
<div class="bg-animation"></div>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-5 col-md-7">
<div class="card bg-transparent border-0" data-aos="fade-up" data-aos-duration="1000">
<div class="card-body p-4">
<!-- BrighterTrades Logo -->
<div class="text-center mb-4">
<img src="{{ url_for('static', filename='logo_BrighterTrades.webp') }}" alt="BrighterTrades Logo" class="img-fluid logo" style="max-width: 150px; margin-top: 20px;">
</div>
<h2 class="card-title text-center mb-4">Sign In</h2>
<!-- Flash Messages - Only show login-related messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{% if category in ['login_error', 'error'] %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% elif category == 'success' %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
<!-- Login Form -->
<form id="login_form" action="/login" method="POST">
<!-- Username Field -->
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" class="form-control" id="username" name="username" placeholder="Enter your username" required autofocus>
</div>
</div>
<!-- Password Field -->
<div class="mb-4">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter your password" required>
</div>
</div>
<!-- Submit Button -->
<button type="submit" class="btn btn-primary w-100 py-2">
<i class="fas fa-sign-in-alt me-2"></i> Sign In
</button>
</form>
<hr class="my-4 text-light">
<!-- Link to Signup Page -->
<p class="text-center mb-2">
Don't have an account? <a href="/signup" class="text-primary">Sign up here</a>
</p>
<!-- Link back to Welcome -->
<p class="text-center">
<a href="/" class="text-muted"><i class="fas fa-arrow-left me-1"></i> Back to Welcome</a>
</p>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js"></script>
<script>
AOS.init({
offset: 100,
duration: 600,
easing: 'ease-out-cubic',
anchorPlacement: 'top-center',
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<style>
body {
font-family: 'Roboto', sans-serif;
background-color: #121212;
color: #e0e0e0;
position: relative;
overflow-x: hidden;
padding-top: 80px;
}
.bg-animation {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
z-index: -1;
}
.card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
}
.input-group-text {
background-color: #1f1f1f;
border: none;
color: #e0e0e0;
}
.form-control {
background-color: #2a2a2a;
border: 1px solid #444;
color: #e0e0e0;
}
.form-control:focus {
background-color: #333;
border-color: #0d6efd;
color: #fff;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
.form-control::placeholder {
color: #888;
}
.btn-primary {
background-color: #0d6efd;
border: none;
transition: background-color 0.3s ease;
}
.btn-primary:hover {
background-color: #0b5ed7;
}
.logo {
display: block;
margin: 0 auto;
max-height: 100px;
}
a.text-primary {
color: #0d6efd !important;
}
a.text-muted {
color: #888 !important;
text-decoration: none;
}
a.text-muted:hover {
color: #aaa !important;
}
</style>
</body>
</html>

View File

@ -1,7 +1,7 @@
<div class="form-popup" id="new_trade_form">
<form action="/new_trade" class="form-container">
<!-- Panel 1 of 1 (8 rows, 2 columns) -->
<div id="trade_pan_1" class="form_panels" style="display: grid; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(8, 1fr); gap: 10px;">
<!-- Panel 1 of 1 (10 rows, 2 columns) -->
<div id="trade_pan_1" class="form_panels" style="display: grid; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(10, auto); gap: 10px;">
<!-- Panel title (row 1) -->
<h1 style="grid-column: 1 / span 2; grid-row: 1;">Create New Trade</h1>
@ -9,38 +9,59 @@
<label for="tradeTarget" style="grid-column: 1; grid-row: 2;"><b>Trade Mode:</b></label>
<select name="tradeTarget" id="tradeTarget" style="grid-column: 2; grid-row: 2;">
<option value="test_exchange">Paper Trade</option>
<option value="binance">Binance (Live)</option>
<!-- Live exchanges populated dynamically -->
</select>
<!-- Side Input field (row 3) -->
<label for="side" style="grid-column: 1; grid-row: 3;"><b>Side:</b></label>
<select name="side" id="side" style="grid-column: 2; grid-row: 3;">
<!-- Testnet Toggle (row 3) - only shown for live exchanges -->
<div id="testnet-row" style="grid-column: 1 / span 2; grid-row: 3; display: none; padding: 5px; background: #2a2a2a; border-radius: 4px;">
<label style="display: flex; align-items: center; gap: 8px; font-size: 12px;">
<input type="checkbox" id="tradeTestnet" checked>
<span><b>Testnet Mode</b> (Recommended for testing)</span>
</label>
<small id="testnet-warning" style="color: #ff6600; font-size: 10px; display: block; margin-top: 3px;">
Unchecking uses REAL MONEY on the exchange
</small>
<small id="testnet-unavailable" style="color: #888; font-size: 10px; display: none; margin-top: 3px;">
Testnet not available for this exchange
</small>
</div>
<!-- Symbol Selection (row 4) -->
<label for="tradeSymbol" style="grid-column: 1; grid-row: 4;"><b>Symbol:</b></label>
<div style="grid-column: 2; grid-row: 4; display: flex; align-items: center; gap: 5px;">
<input type="text" id="tradeSymbol" name="tradeSymbol" placeholder="BTC/USDT" style="flex: 1;">
<button type="button" class="btn-small" onclick="UI.trade.useChartSymbol()" title="Use chart symbol" style="padding: 4px 8px; font-size: 11px;">Chart</button>
</div>
<!-- Side Input field (row 5) -->
<label for="side" style="grid-column: 1; grid-row: 5;"><b>Side:</b></label>
<select name="side" id="side" style="grid-column: 2; grid-row: 5;">
<option value="buy">Buy</option>
<option value="sell">Sell</option>
</select>
<!-- Order Type Input field (row 4) -->
<label for="orderType" style="grid-column: 1; grid-row: 4;"><b>Order Type:</b></label>
<select name="orderType" id="orderType" style="grid-column: 2; grid-row: 4;">
<!-- Order Type Input field (row 6) -->
<label for="orderType" style="grid-column: 1; grid-row: 6;"><b>Order Type:</b></label>
<select name="orderType" id="orderType" style="grid-column: 2; grid-row: 6;">
<option value="MARKET">Market</option>
<option value="LIMIT">Limit</option>
</select>
<!-- Price Input field (row 5) -->
<label id="price-label" for="price" style="grid-column: 1; grid-row: 5;"><b>Price:</b></label>
<input type="number" min="0" value="0.1" step="0.00000001" name="price" id="price" style="grid-column: 2; grid-row: 5; display: none;">
<output name="currentPrice" id="currentPrice" style="grid-column: 2; grid-row: 5;"></output>
<!-- Price Input field (row 7) -->
<label id="price-label" for="price" style="grid-column: 1; grid-row: 7;"><b>Price:</b></label>
<input type="number" min="0" value="0.1" step="0.00000001" name="price" id="price" style="grid-column: 2; grid-row: 7; display: none;">
<output name="currentPrice" id="currentPrice" style="grid-column: 2; grid-row: 7;"></output>
<!-- Quantity Input field (row 6) -->
<label for="quantity" style="grid-column: 1; grid-row: 6;"><b>Quantity:</b></label>
<input type="number" min="0" value="0" step="0.00000001" name="quantity" id="quantity" style="grid-column: 2; grid-row: 6;">
<!-- Quantity Input field (row 8) -->
<label for="quantity" style="grid-column: 1; grid-row: 8;"><b>Quantity:</b></label>
<input type="number" min="0" value="0" step="0.00000001" name="quantity" id="quantity" style="grid-column: 2; grid-row: 8;">
<!-- Value field (row 7) -->
<label for="tradeValue" style="grid-column: 1; grid-row: 7;"><b>Est. Value:</b></label>
<output name="tradeValue" id="tradeValue" for="quantity price" style="grid-column: 2; grid-row: 7;">0</output>
<!-- Value field (row 9) -->
<label for="tradeValue" style="grid-column: 1; grid-row: 9;"><b>Est. Value:</b></label>
<output name="tradeValue" id="tradeValue" for="quantity price" style="grid-column: 2; grid-row: 9;">0</output>
<!-- Buttons (row 8) -->
<div style="grid-column: 1 / span 2; grid-row: 8;">
<!-- Buttons (row 10) -->
<div style="grid-column: 1 / span 2; grid-row: 10;">
<button type="button" class="btn cancel" onclick="UI.trade.close_tradeForm()">Close</button>
<button type="button" class="btn next" onclick="UI.trade.submitNewTrade()">Create Trade</button>
</div>

View File

@ -42,14 +42,22 @@
<h2 class="card-title text-center mb-4">Join BrighterTrades</h2>
<!-- Flash Messages for Feedback -->
<!-- Flash Messages - Only show signup-related messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
<i class="fas fa-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% if category in ['signup_error', 'error', 'message'] %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% elif category == 'success' %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{# Ignore login_error and other unrelated categories #}
{% endfor %}
{% endif %}
{% endwith %}
@ -107,12 +115,11 @@
<!-- Divider -->
<hr class="my-4 text-light">
<!-- Social Signup Options -->
<!-- Social Signup Options (Coming Soon) -->
<div class="text-center">
<p class="mb-2">Or sign up with</p>
<a href="#" class="btn btn-outline-light btn-floating mx-1"><i class="fab fa-google"></i></a>
<a href="#" class="btn btn-outline-light btn-floating mx-1"><i class="fab fa-linkedin"></i></a>
<a href="#" class="btn btn-outline-light btn-floating mx-1"><i class="fab fa-github"></i></a>
<p class="mb-2 text-muted"><small>Social login coming soon</small></p>
<button class="btn btn-outline-secondary btn-floating mx-1" disabled title="Coming soon"><i class="fab fa-google"></i></button>
<button class="btn btn-outline-secondary btn-floating mx-1" disabled title="Coming soon"><i class="fab fa-github"></i></button>
</div>
<!-- Link to Login Page -->

View File

@ -161,7 +161,83 @@
</a>
</div>
<!-- Auth Links -->
<div class="auth-links">
<a href="/login" class="auth-link">Sign In</a>
<span class="auth-divider">|</span>
<a href="/signup" class="auth-link">Create Account</a>
</div>
<p class="footer-text">Build, test, and deploy trading strategies with visual blocks</p>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-container">
{% for category, message in messages %}
{% if category == 'success' %}
<div class="flash-message flash-success">
{{ message }}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
<style>
.auth-links {
margin-top: 30px;
}
.auth-link {
color: #a0a0a0;
text-decoration: none;
font-size: 0.95rem;
transition: color 0.2s ease;
}
.auth-link:hover {
color: #00d4ff;
}
.auth-divider {
color: #555;
margin: 0 15px;
}
.flash-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.flash-message {
padding: 15px 25px;
border-radius: 8px;
margin-bottom: 10px;
font-size: 0.95rem;
animation: slideIn 0.3s ease;
}
.flash-success {
background: rgba(0, 255, 136, 0.2);
border: 1px solid #00ff88;
color: #00ff88;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>
</body>
</html>

View File

@ -23,7 +23,7 @@ class Trade:
status: str | None = None, stats: dict[str, Any] | None = None,
order: Any | None = None, fee: float = 0.001, strategy_id: str | None = None,
is_paper: bool = False, creator: int | None = None, created_at: str | None = None,
tbl_key: str | None = None):
tbl_key: str | None = None, testnet: bool = False):
"""
Initializes a Trade instance with all necessary attributes.
"""
@ -39,6 +39,7 @@ class Trade:
self.fee = fee
self.strategy_id = strategy_id
self.is_paper = is_paper
self.testnet = testnet
self.creator = creator
self.created_at = created_at or dt.datetime.now(dt.timezone.utc).isoformat()
@ -82,6 +83,7 @@ class Trade:
'stats': self.stats,
'order': self.order,
'is_paper': self.is_paper,
'testnet': self.testnet,
'creator': self.creator,
'created_at': self.created_at
}
@ -246,20 +248,40 @@ class Trades:
order_price REAL,
base_order_qty REAL NOT NULL,
time_in_force TEXT DEFAULT 'GTC',
fee REAL DEFAULT 0.1,
fee REAL DEFAULT 0.001,
status TEXT DEFAULT 'inactive',
stats_json TEXT,
strategy_id TEXT,
is_paper INTEGER DEFAULT 0,
testnet INTEGER DEFAULT 0,
created_at TEXT,
tbl_key TEXT UNIQUE
)
"""
self.data_cache.db.execute_sql(create_sql, params=[])
logger.info("Created trades table in database")
else:
# Ensure testnet column exists for existing databases
self._ensure_testnet_column()
except Exception as e:
logger.error(f"Error ensuring trades table exists: {e}", exc_info=True)
def _ensure_testnet_column(self) -> None:
"""Add testnet column to trades table if it doesn't exist."""
try:
# Check if testnet column exists
result = self.data_cache.db.execute_sql(
"PRAGMA table_info(trades)", params=[]
)
columns = {row[1] for row in result} if result else set()
if 'testnet' not in columns:
self.data_cache.db.execute_sql(
"ALTER TABLE trades ADD COLUMN testnet INTEGER DEFAULT 0", params=[]
)
logger.info("Added testnet column to trades table")
except Exception as e:
logger.debug(f"Could not add testnet column: {e}")
def _create_cache(self) -> None:
"""Create the trades cache in DataCache."""
try:
@ -284,6 +306,7 @@ class Trades:
"stats_json",
"strategy_id",
"is_paper",
"testnet",
"created_at",
"tbl_key"
]
@ -323,6 +346,7 @@ class Trades:
fee=float(row.get('fee', 0.001)),
strategy_id=row.get('strategy_id'),
is_paper=bool(row.get('is_paper', 0)),
testnet=bool(row.get('testnet', 0)),
creator=row.get('creator'),
created_at=row.get('created_at'),
tbl_key=row.get('tbl_key')
@ -347,7 +371,7 @@ class Trades:
columns = (
"creator", "unique_id", "target", "symbol", "side", "order_type",
"order_price", "base_order_qty", "time_in_force", "fee", "status",
"stats_json", "strategy_id", "is_paper", "created_at", "tbl_key"
"stats_json", "strategy_id", "is_paper", "testnet", "created_at", "tbl_key"
)
stats_json = json.dumps(trade.stats) if trade.stats else '{}'
@ -367,6 +391,7 @@ class Trades:
stats_json,
trade.strategy_id,
int(trade.is_paper),
int(trade.testnet),
trade.created_at,
trade.tbl_key
)
@ -432,7 +457,7 @@ class Trades:
def new_trade(self, target: str, symbol: str, price: float, side: str,
order_type: str, qty: float, user_id: int = None,
strategy_id: str = None) -> tuple[str, str | None]:
strategy_id: str = None, testnet: bool = False) -> tuple[str, str | None]:
"""
Creates a new trade (paper or live).
@ -444,11 +469,29 @@ class Trades:
:param qty: The quantity to trade.
:param user_id: The user creating the trade.
:param strategy_id: Optional strategy ID if from a strategy.
:param testnet: Whether to use testnet/sandbox mode for live trades.
:return: Tuple of (status, trade_id or error message).
"""
# Determine if this is a paper trade
is_paper = target in ['test_exchange', 'paper', 'Paper Trade']
# For live trades, validate exchange is configured BEFORE creating trade
if not is_paper:
if not self.exchange_connected():
return 'Error', 'No exchange interface connected. Cannot place live trades.'
# Check if user has this exchange configured
user_name = self._get_user_name(user_id) if user_id else None
if not user_name:
return 'Error', 'You must be logged in to place live trades.'
try:
exchange = self.exchange_interface.get_exchange(ename=target, uname=user_name)
if not exchange or not exchange.configured:
return 'Error', f'Exchange "{target}" is not configured with API keys. Please configure it in the Exchanges panel first.'
except ValueError as e:
return 'Error', f'Exchange "{target}" is not connected. Please add it in the Exchanges panel first.'
# For market orders, fetch the current price from exchange
effective_price = float(price) if price else 0.0
if order_type and order_type.upper() == 'MARKET' and self.exchange_interface:
@ -489,6 +532,7 @@ class Trades:
order_type=order_type.upper() if order_type else 'MARKET',
strategy_id=strategy_id,
is_paper=is_paper,
testnet=testnet,
creator=user_id,
fee=effective_fee
)
@ -503,6 +547,9 @@ class Trades:
logger.info(f"Paper trade created: {trade.unique_id} {side} {qty} {symbol} @ {effective_price}")
else:
# Live trade: place order on exchange
mode_str = "testnet" if testnet else "production"
logger.info(f"Live trade ({mode_str}): {trade.unique_id} {side} {qty} {symbol} @ {effective_price}")
if not self.exchange_connected():
return 'Error', 'No exchange connected'