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:
parent
c895a3615d
commit
78dfd71303
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
31
src/app.py
31
src/app.py
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -59,8 +59,10 @@ class Data {
|
|||
}
|
||||
candle_update(new_candle){
|
||||
// This is called everytime a candle update comes from the local server.
|
||||
// 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);
|
||||
//console.log('Candle update:');
|
||||
}
|
||||
this.last_price = new_candle.close;
|
||||
}
|
||||
registerCallback_i_updates(call_back){
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue