Add statistics dashboard and strategy UI enhancements

Statistics HUD:
- Full statistics dashboard with running strategies list
- Strategy performance metrics (P&L, balance, trades, win rate)
- Mini equity curve chart placeholder
- Real-time stats updates via SocketIO

Strategy Improvements:
- Orphaned strategy cleanup function
- User existence validation
- Enhanced strategy card display
- Improved new strategy popup

UI/UX Enhancements:
- Updated control panel layout
- Indicator blocks improvements
- Additional CSS styling
- Welcome template
- Communication.js strategy event handling

Backend:
- Strategy instance tracking updates
- Paper strategy instance improvements
- User session handling updates
- App.py route additions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-03-02 04:39:41 -04:00
parent c895a3615d
commit 78dfd71303
16 changed files with 901 additions and 36 deletions

View File

@ -421,6 +421,68 @@ class Strategies:
logger.error(f"Failed to delete strategy '{tbl_key}': {e}", exc_info=True)
return {"success": False, "message": f"Failed to delete strategy: {str(e)}"}
def cleanup_orphaned_strategies(self) -> dict:
"""
Removes strategies that belong to non-existent users from the database.
This is a maintenance operation to clean up orphaned data.
:return: A dictionary with cleanup results.
"""
try:
# Get all strategies without filtering
all_strategies = self.data_cache.get_all_rows_from_datacache(cache_name='strategies')
if all_strategies is None or all_strategies.empty:
return {"success": True, "removed": 0, "message": "No strategies found."}
removed_count = 0
removed_names = []
for idx, row in all_strategies.iterrows():
creator = row.get('creator')
tbl_key = row.get('tbl_key')
name = row.get('name', 'unknown')
# Check for invalid or non-existent creator
should_remove = False
reason = ""
if creator is None:
should_remove = True
reason = "null creator"
elif isinstance(creator, bytes):
should_remove = True
reason = "corrupted creator (bytes)"
elif isinstance(creator, str) and not creator.isdigit():
should_remove = True
reason = f"invalid creator format: {creator}"
else:
try:
creator_id = int(creator)
if not self._user_exists(creator_id):
should_remove = True
reason = f"user {creator_id} does not exist"
except (ValueError, TypeError):
should_remove = True
reason = "creator conversion error"
if should_remove and tbl_key:
logger.info(f"Removing orphaned strategy '{name}' (tbl_key: {tbl_key}) - {reason}")
self.delete_strategy(tbl_key)
removed_count += 1
removed_names.append(name)
return {
"success": True,
"removed": removed_count,
"removed_strategies": removed_names,
"message": f"Cleaned up {removed_count} orphaned strategies."
}
except Exception as e:
logger.error(f"Failed to cleanup orphaned strategies: {e}", exc_info=True)
return {"success": False, "removed": 0, "message": f"Cleanup failed: {str(e)}"}
def get_all_strategy_names(self, user_id: int) -> list | None:
"""
Return a list of all public and user strategy names stored in the cache or database.
@ -432,6 +494,63 @@ class Strategies:
return strategies_df['name'].tolist()
return None
def _user_exists(self, user_id: int) -> bool:
"""
Check if a user exists in the users cache/database.
:param user_id: The user ID to check.
:return: True if the user exists, False otherwise.
"""
if user_id is None:
return False
try:
# Try to get the username for this user_id
username = self.data_cache.get_datacache_item(
item_name='user_name',
cache_name='users',
filter_vals=('id', int(user_id))
)
return username is not None
except (ValueError, TypeError):
# Invalid user_id format (e.g., corrupted data)
return False
def _filter_valid_strategies(self, strategies_df: pd.DataFrame) -> pd.DataFrame:
"""
Filter out strategies with non-existent or invalid creators.
:param strategies_df: DataFrame of strategies to filter.
:return: Filtered DataFrame with only valid strategies.
"""
if strategies_df is None or strategies_df.empty:
return strategies_df
valid_indices = []
for idx, row in strategies_df.iterrows():
creator = row.get('creator')
# Check for valid creator
try:
# Handle corrupted data (binary garbage, etc.)
if creator is None:
continue
if isinstance(creator, bytes):
logger.warning(f"Skipping strategy with corrupted creator field (bytes): {row.get('name', 'unknown')}")
continue
if isinstance(creator, str) and not creator.isdigit():
logger.warning(f"Skipping strategy with invalid creator field: {row.get('name', 'unknown')}")
continue
creator_id = int(creator)
if self._user_exists(creator_id):
valid_indices.append(idx)
else:
logger.debug(f"Filtering out strategy '{row.get('name', 'unknown')}' - creator {creator_id} does not exist")
except (ValueError, TypeError) as e:
logger.warning(f"Skipping strategy with invalid creator: {row.get('name', 'unknown')} - {e}")
continue
return strategies_df.loc[valid_indices].reset_index(drop=True)
def get_all_strategies(self, user_id: int | None, form: str, include_all: bool = False):
"""
Return stored strategies in various formats.
@ -470,8 +589,11 @@ class Strategies:
else:
strategies_df = public_df
# Filter out strategies from non-existent or invalid users
strategies_df = self._filter_valid_strategies(strategies_df)
# Return None if no strategies found
if strategies_df.empty:
if strategies_df is None or strategies_df.empty:
return None
# Return the strategies in the requested format

View File

@ -359,6 +359,11 @@ class StrategyInstance:
:return: Result of the execution.
"""
try:
# Log the generated code once for debugging
if not hasattr(self, '_code_logged'):
logger.info(f"Strategy {self.strategy_id} generated code:\n{self.generated_code}")
self._code_logged = True
# Compile the generated code with a meaningful filename
compiled_code = compile(self.generated_code, '<strategy_code>', 'exec')
exec(compiled_code, self.exec_context)

View File

@ -37,13 +37,15 @@ class BaseUser:
Retrieves the user ID based on the username.
:param user_name: The name of the user.
:return: The ID of the user as an integer.
:return: The ID of the user as an integer (native Python int).
"""
return self.data.get_datacache_item(
user_id = self.data.get_datacache_item(
item_name='id',
cache_name='users',
filter_vals=('user_name', user_name)
)
# Convert numpy.int64 to native int for SQLite compatibility
return int(user_id) if user_id is not None else None
def get_username(self, user_id: int) -> str:
"""

View File

@ -128,12 +128,16 @@ def strategy_execution_loop():
logger.info(f"Strategy {strategy_id} generated {len(events)} events: {events}")
# Emit events to the user's room
user_name = brighter_trades.users.get_username(user_id=user_id)
logger.info(f"Emitting to user_id={user_id}, user_name={user_name}")
if user_name:
socketio.emit('strategy_events', sanitize_for_json({
'strategy_id': strategy_id,
'mode': mode,
'events': events
}), room=user_name)
logger.info(f"Emitted strategy_events to room={user_name}")
else:
logger.warning(f"Could not find username for user_id={user_id}")
except Exception as e:
logger.warning(f"Could not get price for {symbol}: {e}")
@ -190,6 +194,14 @@ def resolve_user_name(payload: dict | None) -> str | None:
@app.route('/')
def welcome():
"""
Serves the welcome/landing page.
"""
return render_template('welcome.html')
@app.route('/app')
# @cross_origin(supports_credentials=True)
def index():
"""
@ -341,7 +353,12 @@ def settings():
params = data.get('indicator', {})
else:
setting = request.form.get('setting')
params = request.form.to_dict()
# Use to_dict(flat=False) to get lists for multi-value fields (like checkboxes)
params = request.form.to_dict(flat=False)
# Convert single-value lists back to single values, except for 'indicator'
for key, value in params.items():
if key != 'indicator' and isinstance(value, list) and len(value) == 1:
params[key] = value[0]
if not setting:
return jsonify({"success": False, "message": "No setting provided"}), 400
@ -354,7 +371,7 @@ def settings():
return jsonify({"success": True}), 200
# Redirect if this is a form submission (non-async request)
return redirect('/')
return redirect('/app')
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@ -394,6 +411,16 @@ def history():
return jsonify({'error': str(e)}), 500
@app.route('/docs')
def docs():
"""
Documentation page - placeholder for now.
"""
# TODO: Link to actual documentation when available
flash('Documentation coming soon! Redirecting to the app.')
return redirect('/app')
@app.route('/signup')
def signup():
return render_template('sign_up.html', title='title')

View File

@ -112,6 +112,9 @@ class PaperStrategyInstance(StrategyInstance):
This method translates the Blockly-generated order call to
the PaperBroker interface.
"""
logger.info(f"trade_order called: type={trade_type}, size={size}, order_type={order_type}")
logger.info(f" stop_loss={stop_loss}, take_profit={take_profit}, source={source}")
# Extract symbol from source
symbol = 'BTC/USDT' # Default
if source:

View File

@ -44,6 +44,15 @@ class StratUIManager {
return;
}
// Remove any existing warning banner
const existingWarning = this.formElement.querySelector('.running-strategy-warning');
if (existingWarning) {
existingWarning.remove();
}
// Track the strategy being edited (for restart prompt after save)
this._editingStrategyId = null;
// Update form based on action
if (action === 'new') {
headerTitle.textContent = "Create New Strategy";
@ -59,6 +68,41 @@ class StratUIManager {
nameBox.value = strategyData.name;
publicCheckbox.checked = strategyData.public === 1;
feeBox.value = strategyData.fee || 0;
// Store the strategy ID for later use
this._editingStrategyId = strategyData.tbl_key;
// Check if strategy is currently running and show warning
if (UI.strats && UI.strats.isStrategyRunning(strategyData.tbl_key)) {
const runningInfo = UI.strats.getRunningInfo(strategyData.tbl_key);
const modeText = runningInfo ? runningInfo.mode : 'unknown';
// Create warning banner
const warningBanner = document.createElement('div');
warningBanner.className = 'running-strategy-warning';
warningBanner.innerHTML = `
<span style="margin-right: 8px;"></span>
<span>This strategy is currently running in <strong>${modeText}</strong> mode.
Changes will not take effect until you restart it.</span>
`;
warningBanner.style.cssText = `
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 5px;
padding: 10px;
margin: 10px;
color: #856404;
font-size: 13px;
display: flex;
align-items: center;
`;
// Insert after header
const header = this.formElement.querySelector('#draggable_header');
if (header && header.nextSibling) {
header.parentNode.insertBefore(warningBanner, header.nextSibling);
}
}
}
// Display the form
@ -149,10 +193,8 @@ class StratUIManager {
if (isRunning) {
UI.strats.stopStrategy(strat.tbl_key);
} else {
// Show mode selection in hover panel or use default
const modeSelect = document.getElementById(`mode-select-${strat.tbl_key}`);
const mode = modeSelect ? modeSelect.value : 'paper';
UI.strats.runStrategy(strat.tbl_key, mode);
// Use runStrategyWithOptions to honor testnet checkbox and show warnings
UI.strats.runStrategyWithOptions(strat.tbl_key);
}
});
strategyItem.appendChild(runButton);
@ -533,6 +575,12 @@ class StratWorkspaceManager {
scaleSpeed: 1.2
}
});
// Add tooltips to toolbox categories
this._setupCategoryTooltips();
// Note: Blockly has built-in copy/paste support (Ctrl+C, Ctrl+V, Ctrl+X, Delete)
console.log('Blockly workspace initialized and modules loaded.');
}
@ -676,6 +724,48 @@ class StratWorkspaceManager {
};
}
/**
* Adds tooltips to toolbox category labels.
* @private
*/
_setupCategoryTooltips() {
// Category tooltips mapping
const categoryTooltips = {
'Indicators': 'Use your configured technical indicators in strategy logic',
'Balances': 'Access strategy and account balance information',
'Order Metrics': 'Monitor order status, volume, and fill rates',
'Trade Metrics': 'Monitor active trades, P&L, and trade history',
'Time Metrics': 'Time-based conditions for your strategy',
'Market Data': 'Access real-time prices and candle data',
'Logical': 'Build conditions with comparisons and logic operators',
'Trade Order': 'Execute trades with stop-loss, take-profit, and limits',
'Control': 'Control strategy flow: pause, resume, exit, and schedule',
'Values and flags': 'Store values, set flags, and send notifications',
'Risk Management': 'Control leverage, margin, and position limits',
'Math': 'Arithmetic, statistics, and mathematical functions'
};
// Wait a moment for Blockly to render the toolbox
setTimeout(() => {
// Find all category labels in the toolbox
const toolboxDiv = document.querySelector('.blocklyToolboxDiv');
if (!toolboxDiv) return;
const categoryRows = toolboxDiv.querySelectorAll('.blocklyTreeRow');
categoryRows.forEach(row => {
const labelSpan = row.querySelector('.blocklyTreeLabel');
if (labelSpan) {
const categoryName = labelSpan.textContent;
if (categoryTooltips[categoryName]) {
row.setAttribute('title', categoryTooltips[categoryName]);
row.style.cursor = 'help';
}
}
});
console.log('Category tooltips added');
}, 100);
}
/**
* Restores the Blockly workspace from an XML string.
* @param {string} workspaceXmlText - The XML text representing the workspace.
@ -772,6 +862,7 @@ class Strategies {
this.comms.on('strategy_run_error', this.handleStrategyRunError.bind(this));
this.comms.on('strategy_stop_error', this.handleStrategyStopError.bind(this));
this.comms.on('strategy_status', this.handleStrategyStatus.bind(this));
this.comms.on('strategy_events', this.handleStrategyEvents.bind(this));
// Fetch saved strategies using DataManager
this.dataManager.fetchSavedStrategies(this.comms, this.data);
@ -864,6 +955,35 @@ class Strategies {
} else {
console.warn("Updated strategy not found in local records:", updatedStrategyKey);
}
// Check if the strategy was running and prompt for restart
if (this.isStrategyRunning(updatedStrategyKey)) {
const runningInfo = this.getRunningInfo(updatedStrategyKey);
const strategyName = data.strategy.name || 'Strategy';
const modeText = runningInfo ? runningInfo.mode : 'current';
const shouldRestart = confirm(
`"${strategyName}" was updated successfully!\n\n` +
`This strategy is currently running in ${modeText} mode.\n` +
`The changes will NOT take effect until you restart.\n\n` +
`Would you like to restart the strategy now to apply changes?`
);
if (shouldRestart) {
// Stop and restart the strategy
const mode = runningInfo.mode;
const testnet = runningInfo.testnet !== undefined ? runningInfo.testnet : true;
const initialBalance = runningInfo.initial_balance || 10000;
// Stop first
this.stopStrategy(updatedStrategyKey, mode);
// Restart after a brief delay to allow stop to complete
setTimeout(() => {
this.runStrategy(updatedStrategyKey, mode, initialBalance, testnet);
}, 1000);
}
}
} else {
console.error("Failed to update strategy:", data.message);
alert(`Strategy update failed: ${data.message}`);
@ -1256,9 +1376,21 @@ class Strategies {
testnet: data.testnet,
exchange: data.exchange,
max_position_pct: data.max_position_pct,
circuit_breaker_pct: data.circuit_breaker_pct
circuit_breaker_pct: data.circuit_breaker_pct,
start_time: new Date().toISOString()
});
// Notify statistics module
if (UI.statistics) {
UI.statistics.registerRunningStrategy(data.strategy_id, {
name: data.strategy_name,
mode: actualMode,
testnet: data.testnet,
initial_balance: data.initial_balance,
start_time: new Date().toISOString()
});
}
// Update the UI to reflect running state
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
@ -1294,6 +1426,11 @@ class Strategies {
const runKey = this._makeRunningKey(data.strategy_id, stopMode);
this.runningStrategies.delete(runKey);
// Notify statistics module
if (UI.statistics) {
UI.statistics.unregisterStrategy(data.strategy_id);
}
// Update the UI to reflect stopped state
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
}
@ -1359,6 +1496,79 @@ class Strategies {
}
}
/**
* Handles real-time strategy execution events from the server.
* @param {Object} data - The event data containing strategy_id, mode, and events array.
*/
handleStrategyEvents(data) {
console.log("Strategy events received:", data);
if (!data || !data.strategy_id || !data.events) {
return;
}
const { strategy_id, mode, events } = data;
const runKey = this._makeRunningKey(strategy_id, mode);
const running = this.runningStrategies.get(runKey);
if (!running) {
console.warn(`Received events for strategy ${strategy_id} but it's not in runningStrategies`);
return;
}
// Process each event
let needsUIUpdate = false;
for (const event of events) {
switch (event.type) {
case 'tick_complete':
if (typeof event.balance === 'number') {
running.balance = event.balance;
needsUIUpdate = true;
}
if (typeof event.trades === 'number') {
running.trade_count = event.trades;
needsUIUpdate = true;
}
if (typeof event.profit_loss === 'number') {
running.profit_loss = event.profit_loss;
needsUIUpdate = true;
}
break;
case 'trade_executed':
console.log(`Trade executed: ${event.side} ${event.amount} @ ${event.price}`);
running.trade_count = (running.trade_count || 0) + 1;
needsUIUpdate = true;
// TODO: Add to trade history display
break;
case 'signal_triggered':
console.log(`Signal triggered: ${event.signal}`);
// TODO: Add to activity feed
break;
case 'error':
console.error(`Strategy error: ${event.message}`);
alert(`Strategy ${running.strategy_name || strategy_id} error: ${event.message}`);
break;
case 'strategy_exited':
this.runningStrategies.delete(runKey);
needsUIUpdate = true;
break;
default:
console.log(`Unknown event type: ${event.type}`, event);
}
}
// Update the map and refresh UI if needed
if (needsUIUpdate) {
this.runningStrategies.set(runKey, running);
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
}
}
/**
* Checks if a strategy is currently running (in any mode).
* @param {string} strategyId - The strategy tbl_key.

View File

@ -18,6 +18,26 @@ export function defineIndicatorBlocks() {
return;
}
// Check if there are any indicators to display
const indicatorNames = Object.keys(indicatorOutputs);
if (indicatorNames.length === 0) {
// Add helpful message when no indicators exist
const labelElement = document.createElement('label');
labelElement.setAttribute('text', 'No indicators configured yet.');
toolboxCategory.appendChild(labelElement);
const labelElement2 = document.createElement('label');
labelElement2.setAttribute('text', 'Add indicators from the Indicators panel');
toolboxCategory.appendChild(labelElement2);
const labelElement3 = document.createElement('label');
labelElement3.setAttribute('text', 'on the right side of the screen.');
toolboxCategory.appendChild(labelElement3);
console.log('No indicators available - added help message to toolbox.');
return;
}
for (let indicatorName in indicatorOutputs) {
const outputs = indicatorOutputs[indicatorName];

View File

@ -396,14 +396,47 @@ height: 500px;
}
.content {
padding: 0 18px;
max-height: 0;
min-height: 50px;
height:500px; /* Max height in the html style defines the distance a panel will slide down. This defines the max*/
padding: 5px 18px;
max-height: 60px; /* Preview height when minimized - shows key buttons/info */
overflow: hidden;
transition: max-height 0.2s ease-out;
background-color: #f1f1f1;
}
/* When panel is expanded */
.collapsible.active + .content {
overflow-y: auto;
max-height: 400px; /* Expanded height */
}
/* Hide scrollbar by default, show on hover */
.content::-webkit-scrollbar {
width: 8px;
background: transparent;
}
.content::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 4px;
}
.content:hover::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3);
}
.content:hover::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.5);
}
/* Firefox scrollbar hiding */
.content {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
}
.content:hover {
scrollbar-color: rgba(0, 0, 0, 0.3) transparent;
}
.bg_red{
background-color:#9F180F;
}

View File

@ -72,6 +72,12 @@ class Comms {
this.emit(data.reply, data.data);
}
});
// Handle strategy execution events (tick_complete, trade_executed, etc.)
this.socket.on('strategy_events', (data) => {
console.log('Strategy events received:', data);
this.emit('strategy_events', data);
});
}
/**
@ -307,8 +313,15 @@ class Comms {
* @param {string} tradingPair - The trading pair to subscribe to.
*/
setExchangeCon(interval, tradingPair) {
tradingPair = tradingPair.toLowerCase();
const ws = `wss://stream.binance.com:9443/ws/${tradingPair}@kline_${interval}`;
// Convert trading pair to Binance WebSocket format
// e.g., "BTC/USD" -> "btcusdt", "ETH/USDT" -> "ethusdt"
let binanceSymbol = tradingPair.toLowerCase().replace('/', '');
// Binance doesn't have USD pairs, only USDT
if (binanceSymbol.endsWith('usd') && !binanceSymbol.endsWith('usdt')) {
binanceSymbol = binanceSymbol.replace(/usd$/, 'usdt');
}
console.log(`Connecting to Binance stream: ${binanceSymbol}@kline_${interval}`);
const ws = `wss://stream.binance.com:9443/ws/${binanceSymbol}@kline_${interval}`;
this.exchangeCon = new WebSocket(ws);
this.exchangeCon.onmessage = (event) => {

View File

@ -59,8 +59,10 @@ class Data {
}
candle_update(new_candle){
// This is called everytime a candle update comes from the local server.
window.UI.charts.update_main_chart(new_candle);
//console.log('Candle update:');
// Guard against race condition where updates arrive before chart is initialized
if (window.UI && window.UI.charts && window.UI.charts.update_main_chart) {
window.UI.charts.update_main_chart(new_candle);
}
this.last_price = new_candle.close;
}
registerCallback_i_updates(call_back){

View File

@ -28,10 +28,12 @@
coll[i].addEventListener("click", function() {
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.maxHeight){
content.style.maxHeight = null;
if (this.classList.contains("active")) {
// Expand to full content height (max 400px from CSS)
content.style.maxHeight = Math.min(content.scrollHeight, 400) + "px";
} else {
content.style.maxHeight = content.scrollHeight + "px";
// Collapse to preview height (60px from CSS)
content.style.maxHeight = null;
}
});
}

View File

@ -31,6 +31,7 @@
<script src="{{ url_for('static', filename='signals.js') }}"></script>
<script src="{{ url_for('static', filename='trade.js') }}"></script>
<script src="{{ url_for('static', filename='backtesting.js') }}"></script>
<script src="{{ url_for('static', filename='Statistics.js') }}?v=1"></script>
<script src="{{ url_for('static', filename='general.js') }}"></script>
</head>

View File

@ -1,4 +1,4 @@
<div class="content" id="indicator_panel" style="max-height: 70px;">
<div class="content" id="indicator_panel">
<!-- Indicator Panel Section -->
<div id="edit_indcr_panel" style="display: grid; grid-template-columns: 1fr 1fr;">

View File

@ -108,12 +108,13 @@
<xml id="toolbox_advanced" style="display: none">
<!-- Indicators Category -->
<category name="Indicators" colour="230">
<category name="Indicators" colour="230" tooltip="Use your configured technical indicators in strategy logic">
<!-- Indicator blocks will be added here -->
</category>
<!-- Balances Subcategory -->
<category name="Balances" colour="#E69500">
<category name="Balances" colour="#E69500" tooltip="Access strategy and account balance information">
<label text="Track your trading capital"></label>
<block type="starting_balance">
<comment pinned="false" h="80" w="160">Retrieve the starting balance of the strategy.</comment>
</block>
@ -132,7 +133,8 @@
</category>
<!-- Order Metrics Subcategory -->
<category name="Order Metrics" colour="#E69500">
<category name="Order Metrics" colour="#E69500" tooltip="Monitor order status, volume, and fill rates">
<label text="Track your order performance"></label>
<block type="order_volume">
<comment pinned="false" h="80" w="160">Get the cumulative volume of filled or unfilled orders.</comment>
</block>
@ -148,7 +150,8 @@
</category>
<!-- Trade Metrics Subcategory -->
<category name="Trade Metrics" colour="#E69500">
<category name="Trade Metrics" colour="#E69500" tooltip="Monitor active trades, P&L, and trade history">
<label text="Track your trading activity"></label>
<block type="active_trades">
<comment pinned="false" h="80" w="160">Get the number of active trades currently open by the strategy.</comment>
</block>
@ -170,14 +173,16 @@
</category>
<!-- Time Metrics Subcategory -->
<category name="Time Metrics" colour="#E69500">
<category name="Time Metrics" colour="#E69500" tooltip="Time-based conditions for your strategy">
<label text="Use time in your logic"></label>
<block type="time_since_start">
<comment pinned="false" h="80" w="160">Get the time elapsed since the strategy started.</comment>
</block>
</category>
<!-- Market Data Subcategory -->
<category name="Market Data" colour="#E69500">
<category name="Market Data" colour="#E69500" tooltip="Access real-time prices and candle data">
<label text="Get live market prices"></label>
<block type="current_price">
<comment pinned="false" h="80" w="160">Get the current market price of a specified symbol.</comment>
</block>
@ -196,7 +201,8 @@
</category>
<!-- Logical Blocks Category -->
<category name="Logical" colour="#5C81A6">
<category name="Logical" colour="#5C81A6" tooltip="Build conditions with comparisons and logic operators">
<label text="Create conditional logic"></label>
<block type="comparison">
<comment pinned="false" h="80" w="160">Compare two values using operators like >, <, ==.</comment>
</block>
@ -212,7 +218,8 @@
</category>
<!-- Trade Order Blocks Category -->
<category name="Trade Order" colour="#3366CC">
<category name="Trade Order" colour="#3366CC" tooltip="Execute trades with stop-loss, take-profit, and limits">
<label text="Place and manage orders"></label>
<block type="trade_action">
<comment pinned="false" h="80" w="160">Execute a Buy/Sell trade based on a condition with specified size and options.</comment>
</block>
@ -244,7 +251,8 @@
</category>
<!-- Control Blocks Category -->
<category name="Control" colour="#FF9E00">
<category name="Control" colour="#FF9E00" tooltip="Control strategy flow: pause, resume, exit, and schedule">
<label text="Manage strategy execution"></label>
<block type="pause_strategy">
<comment pinned="false" h="80" w="160">Pause the strategy if the condition is true.</comment>
</block>
@ -266,7 +274,8 @@
</category>
<!-- Values and flags Blocks Category -->
<category name="Values and flags" colour="#6C8EBF">
<category name="Values and flags" colour="#6C8EBF" tooltip="Store values, set flags, and send notifications">
<label text="Variables and state management"></label>
<block type="notify_user">
<comment pinned="false" h="80" w="160">Send a notification message to the user.</comment>
</block>
@ -288,7 +297,8 @@
</category>
<!-- Risk Management Category -->
<category name="Risk Management" colour="#B22222">
<category name="Risk Management" colour="#B22222" tooltip="Control leverage, margin, and position limits">
<label text="Protect your capital"></label>
<block type="set_leverage">
<comment pinned="false" h="80" w="160">Define the leverage ratio for trades executed by the strategy.</comment>
</block>
@ -304,7 +314,8 @@
</category>
<!-- Math Category -->
<category name="Math" colour="#FFA500">
<category name="Math" colour="#FFA500" tooltip="Arithmetic, statistics, and mathematical functions">
<label text="Calculate and compute values"></label>
<block type="math_operation">
<comment pinned="false" h="80" w="160">Perform basic arithmetic operations between two values.</comment>
</block>

View File

@ -1,3 +1,250 @@
<div class="content">
<h3>Statistics</h3>
<div class="content" id="statistics_content">
<!-- Running Strategies Section -->
<div class="stats-section">
<h4>Running Strategies</h4>
<div id="running_strategies_list">
<p class="no-data-msg">No strategies running</p>
</div>
</div>
<hr>
<!-- Selected Strategy Stats -->
<div class="stats-section" id="selected_strategy_stats" style="display: none;">
<h4>Strategy Performance</h4>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Starting Balance</span>
<span class="stat-value" id="stat_starting_balance">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Current Balance</span>
<span class="stat-value" id="stat_current_balance">-</span>
</div>
<div class="stat-item">
<span class="stat-label">P&L</span>
<span class="stat-value" id="stat_pnl">-</span>
</div>
<div class="stat-item">
<span class="stat-label">P&L %</span>
<span class="stat-value" id="stat_pnl_pct">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Total Trades</span>
<span class="stat-value" id="stat_total_trades">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Open Positions</span>
<span class="stat-value" id="stat_open_positions">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Win Rate</span>
<span class="stat-value" id="stat_win_rate">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Runtime</span>
<span class="stat-value" id="stat_runtime">-</span>
</div>
</div>
<!-- Mini Equity Curve -->
<div id="mini_equity_chart" style="height: 100px; margin-top: 10px;"></div>
</div>
<hr>
<!-- Trade History Section -->
<div class="stats-section">
<h4>Recent Trades</h4>
<div id="trade_history_list" class="trade-history">
<p class="no-data-msg">No trades yet</p>
</div>
</div>
<!-- Activity Log -->
<div class="stats-section">
<h4>Activity Log</h4>
<div id="activity_log" class="activity-log">
<p class="no-data-msg">No activity</p>
</div>
</div>
</div>
<style>
#statistics_content {
min-height: 200px;
}
.stats-section {
margin-bottom: 10px;
}
.stats-section h4 {
margin: 5px 0;
color: #333;
font-size: 13px;
border-bottom: 1px solid #ddd;
padding-bottom: 3px;
}
.no-data-msg {
color: #888;
font-style: italic;
font-size: 12px;
margin: 5px 0;
}
/* Running strategies list */
.running-strategy-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 8px;
margin: 3px 0;
background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
border-radius: 5px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
}
.running-strategy-item:hover {
background: linear-gradient(135deg, #c8e6c9, #a5d6a7);
}
.running-strategy-item.selected {
background: linear-gradient(135deg, #bbdefb, #90caf9);
border: 1px solid #2196f3;
}
.strategy-name {
font-weight: bold;
color: #2e7d32;
}
.strategy-mode {
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
background: #4caf50;
color: white;
}
.strategy-mode.live {
background: #f44336;
}
.strategy-mode.live.testnet {
background: #ff9800;
}
.strategy-balance {
font-family: monospace;
color: #1565c0;
}
/* Stats grid */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
}
.stat-item {
display: flex;
justify-content: space-between;
padding: 3px 5px;
background: #f5f5f5;
border-radius: 3px;
font-size: 11px;
}
.stat-label {
color: #666;
}
.stat-value {
font-weight: bold;
font-family: monospace;
}
.stat-value.positive {
color: #2e7d32;
}
.stat-value.negative {
color: #c62828;
}
/* Trade history */
.trade-history {
max-height: 120px;
overflow-y: auto;
}
.trade-item {
display: grid;
grid-template-columns: 50px 1fr 60px 70px;
gap: 5px;
padding: 4px 6px;
margin: 2px 0;
background: #fafafa;
border-radius: 3px;
font-size: 11px;
border-left: 3px solid #ccc;
}
.trade-item.buy {
border-left-color: #4caf50;
}
.trade-item.sell {
border-left-color: #f44336;
}
.trade-side {
font-weight: bold;
}
.trade-side.buy {
color: #2e7d32;
}
.trade-side.sell {
color: #c62828;
}
/* Activity log */
.activity-log {
max-height: 80px;
overflow-y: auto;
font-size: 10px;
}
.activity-item {
padding: 2px 5px;
margin: 1px 0;
background: #f9f9f9;
border-radius: 2px;
display: flex;
gap: 5px;
}
.activity-time {
color: #888;
font-family: monospace;
}
.activity-msg {
color: #333;
}
.activity-item.error {
background: #ffebee;
color: #c62828;
}
.activity-item.trade {
background: #e8f5e9;
}
</style>

167
src/templates/welcome.html Normal file
View File

@ -0,0 +1,167 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BrighterTrading - Welcome</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #fff;
}
.welcome-container {
text-align: center;
max-width: 800px;
padding: 40px;
}
.logo-container {
margin-bottom: 40px;
}
.logo-container svg {
width: 150px;
height: 150px;
filter: drop-shadow(0 0 20px rgba(0, 212, 255, 0.3));
}
h1 {
font-size: 3rem;
margin-bottom: 15px;
background: linear-gradient(90deg, #00d4ff, #00ff88);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.tagline {
font-size: 1.2rem;
color: #a0a0a0;
margin-bottom: 50px;
}
.buttons-container {
display: flex;
gap: 30px;
justify-content: center;
flex-wrap: wrap;
}
.welcome-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 50px;
border-radius: 16px;
text-decoration: none;
transition: all 0.3s ease;
min-width: 220px;
}
.welcome-btn svg {
width: 48px;
height: 48px;
margin-bottom: 15px;
}
.welcome-btn span {
font-size: 1.1rem;
font-weight: 600;
}
.btn-docs {
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.2);
color: #fff;
}
.btn-docs:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.btn-start {
background: linear-gradient(135deg, #00d4ff 0%, #00ff88 100%);
border: none;
color: #1a1a2e;
}
.btn-start:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(0, 212, 255, 0.4);
}
.btn-description {
font-size: 0.85rem;
font-weight: 400;
opacity: 0.8;
margin-top: 8px;
}
.footer-text {
margin-top: 60px;
color: #666;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="welcome-container">
<div class="logo-container">
<!-- Trading/Chart SVG Icon -->
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="60" width="12" height="30" rx="2" fill="#00d4ff"/>
<rect x="28" y="40" width="12" height="50" rx="2" fill="#00ff88"/>
<rect x="46" y="25" width="12" height="65" rx="2" fill="#00d4ff"/>
<rect x="64" y="45" width="12" height="45" rx="2" fill="#00ff88"/>
<rect x="82" y="15" width="12" height="75" rx="2" fill="#00d4ff"/>
<path d="M16 55 L34 35 L52 20 L70 40 L88 10" stroke="url(#lineGradient)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<linearGradient id="lineGradient" x1="16" y1="55" x2="88" y2="10">
<stop offset="0%" stop-color="#00d4ff"/>
<stop offset="100%" stop-color="#00ff88"/>
</linearGradient>
</defs>
</svg>
</div>
<h1>BrighterTrading</h1>
<p class="tagline">Visual strategy builder for cryptocurrency trading</p>
<div class="buttons-container">
<a href="/docs" class="welcome-btn btn-docs">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h6v6h6v10H6z"/>
<path d="M8 12h8v2H8zm0 4h8v2H8z"/>
</svg>
<span>Documentation</span>
<span class="btn-description">Walkthroughs & guides</span>
</a>
<a href="/app" class="welcome-btn btn-start">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z"/>
</svg>
<span>Get Started</span>
<span class="btn-description">Open the trading platform</span>
</a>
</div>
<p class="footer-text">Build, test, and deploy trading strategies with visual blocks</p>
</div>
</body>
</html>