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:
parent
45395c86c5
commit
33de4affb4
|
|
@ -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)
|
||||
|
|
|
|||
15
src/Users.py
15
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,
|
||||
|
|
|
|||
34
src/app.py
34
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')
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
55
src/trade.py
55
src/trade.py
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue