The strategy popup is now draggable and resizeable.

This commit is contained in:
Rob 2024-09-23 16:38:22 -03:00
parent 034b8ab925
commit 4e3e8e5abf
7 changed files with 458 additions and 140 deletions

View File

@ -322,12 +322,31 @@ class BrighterTrades:
:param data: A dictionary containing the attributes of the new strategy. :param data: A dictionary containing the attributes of the new strategy.
:return: An error message if the required attribute is missing, or the incoming data for chaining on success. :return: An error message if the required attribute is missing, or the incoming data for chaining on success.
""" """
if 'name' not in data: # Extract user_name from the data and get user_id
return "The new strategy must have a 'name' attribute." user_name = data.get('user_name')
if not user_name:
return {"success": False, "message": "User not specified"}
self.strategies.new_strategy(data) # Fetch the user_id using the user_name
self.config.set_setting('strategies', self.strategies.get_strategies('dict')) user_id = self.get_user_info(user_name=user_name, info='User_id')
return data if not user_id:
return {"success": False, "message": "User ID not found"}
# Prepare the strategy data for insertion
strategy_data = {
"creator": user_id,
"name": data['name'],
"workspace": data['workspace'],
"code": data['code'],
"stats": data.get('stats', {}),
"public": data.get('public', 0), # Default to private if not specified
"fee": data.get('fee', None) # Default to None if not specified
}
# Save the new strategy (in both cache and database)
self.strategies.new_strategy(strategy_data)
return {"success": True, "message": "Strategy created successfully", "data": strategy_data}
def delete_strategy(self, strategy_name: str) -> None: def delete_strategy(self, strategy_name: str) -> None:
""" """

View File

@ -1,5 +1,6 @@
import json import json
from DataCache_v2 import DataCache from DataCache_v3 import DataCache
import datetime as dt
class Strategy: class Strategy:
@ -169,59 +170,90 @@ class Strategy:
class Strategies: class Strategies:
def __init__(self, data: DataCache, trades): def __init__(self, data: DataCache, trades):
# Handles database connections """
self.data = data Initializes the Strategies class.
# Reference to the trades object that maintains all trading actions and data. :param data: Instance of DataCache to manage cache and database interactions.
:param trades: Reference to the trades object that maintains trading actions and data.
"""
self.data = data # Database interaction instance
self.trades = trades self.trades = trades
self.strat_list = [] # List to hold strategy objects
self.strat_list = [] # Create a cache for strategies with necessary columns
self.data.create_cache(name='strategies',
cache_type='table',
size_limit=100,
eviction_policy='deny',
default_expiration=dt.timedelta(hours=24),
columns=["id", "creator", "name", "workspace", "code", "stats", "public", "fee"])
def get_all_strategy_names(self) -> list | None: def new_strategy(self, data: dict):
"""Return a list of all strategies in the database""" """
# # Load existing Strategies from file. Add a new strategy to the cache and database.
# loaded_strategies = self.data.get_setting('strategies')
# if loaded_strategies is None:
# # Populate the list and file with defaults defined in this class.
# loaded_strategies = self.get_strategy_defaults()
# config.set_setting('strategies', loaded_strategies)
#
# for entry in loaded_strategies:
# # Initialise all the strategy objects with data from file.
# self.strat_list.append(Strategy(**entry))
return None :param data: A dictionary containing strategy data such as name, code, and workspace.
"""
def new_strategy(self, data): # Create a new Strategy object and add it to the list
# Create an instance of the new Strategy.
self.strat_list.append(Strategy(**data)) self.strat_list.append(Strategy(**data))
def delete_strategy(self, name): # Insert the strategy into the database and cache
self.data.insert_row_into_datacache(
cache_name='strategies',
columns=("creator", "name", "workspace", "code", "stats", "public", "fee"),
values=(data.get('creator'), data['name'], data['workspace'], data['code'], data.get('stats', {}),
data.get('public', False), data.get('fee', 0))
)
def delete_strategy(self, name: str):
"""
Delete a strategy from the cache and database by name.
:param name: The name of the strategy to delete.
"""
obj = self.get_strategy_by_name(name) obj = self.get_strategy_by_name(name)
if obj: if obj:
self.strat_list.remove(obj) self.strat_list.remove(obj)
def get_strategies(self, form): # Remove the strategy from cache and database
# Return a python object of all the strategies stored in this instance. self.data.remove_row_from_datacache(
cache_name='strategies',
filter_vals=[('name', name)]
)
def get_all_strategy_names(self) -> list | None:
"""
Return a list of all strategy names stored in the cache or database.
"""
# Fetch all strategy names from the cache or database
strategies_df = self.data.get_rows_from_datacache(cache_name='strategies', filter_vals=[])
if not strategies_df.empty:
return strategies_df['name'].tolist()
return None
def get_strategies(self, form: str):
"""
Return strategies stored in this instance in various formats.
:param form: The desired format ('obj', 'json', or 'dict').
:return: A list of strategies in the requested format.
"""
if form == 'obj': if form == 'obj':
return self.strat_list return self.strat_list
# Return a JSON object of all the strategies stored in this instance.
elif form == 'json': elif form == 'json':
strats = self.strat_list return [strat.to_json() for strat in self.strat_list]
json_str = []
for strat in strats:
json_str.append(strat.to_json())
return json_str
# Return a dictionary object of all the strategies stored in this instance.
elif form == 'dict': elif form == 'dict':
strats = self.strat_list return [strat.__dict__ for strat in self.strat_list]
s_list = [] return None
for st in strats:
dic = st.__dict__
s_list.append(dic)
return s_list
def get_strategy_by_name(self, name): def get_strategy_by_name(self, name: str):
"""
Retrieve a strategy object by name.
:param name: The name of the strategy to retrieve.
:return: The strategy object if found, otherwise False.
"""
for obj in self.strat_list: for obj in self.strat_list:
if obj.name == name: if obj.name == name:
return obj return obj

View File

@ -105,39 +105,46 @@ def index():
@sock.route('/ws') @sock.route('/ws')
def ws(socket_conn): def ws(socket_conn):
""" """
Open a websocket to handle two-way communication with UI without browser refreshes. Open a WebSocket to handle two-way communication with UI without browser refreshes.
""" """
def json_msg_received(msg_obj): def json_msg_received(msg_obj):
""" """
Handle incoming json msg's. Handle incoming JSON messages with authentication.
""" """
# Validate input # Validate input
if 'message_type' not in msg_obj: if 'message_type' not in msg_obj or 'data' not in msg_obj:
return return
msg_type, msg_data = msg_obj['message_type'], msg_obj['data'] msg_type, msg_data = msg_obj['message_type'], msg_obj['data']
print(f'\n[MSG_RECEIVED]\nMSG Type:{msg_type}\n{msg_data}') # Extract user_name from the incoming message data
user_name = msg_data.get('user_name')
if not user_name:
socket_conn.send(json.dumps({"success": False, "message": "User not specified"}))
return
# Check if the user is logged in
if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'):
socket_conn.send(json.dumps({"success": False, "message": "User not logged in"}))
return
# Process the incoming message based on the type
resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data) resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data)
socket_conn.send(json.dumps(resp)) # Send the response back to the client
if resp:
socket_conn.send(json.dumps(resp))
return # Main loop to receive messages and handle them
# The rendered page connects to the exchange_interface and relays the candle data back here
# this socket also handles data and processing requests
while True: while True:
msg = socket_conn.receive() msg = socket_conn.receive()
if msg: if msg:
# If msg is in json, convert the message into a dictionary then feed the message handler.
# Otherwise, log the output.
try: try:
json_msg = json.loads(msg) json_msg = json.loads(msg)
json_msg_received(json_msg) json_msg_received(json_msg)
except json.JSONDecodeError: except json.JSONDecodeError:
print(f'Msg received from client: {msg}') print(f'Msg received from client (not JSON): {msg}')
@app.route('/settings', methods=['POST']) @app.route('/settings', methods=['POST'])

View File

@ -30,6 +30,13 @@ class Strategies {
// Define Python generators after workspace initialization // Define Python generators after workspace initialization
this.definePythonGenerators(); this.definePythonGenerators();
} }
resizeWorkspace() {
const blocklyDiv = document.getElementById('blocklyDiv');
if (this.workspace) {
// Adjust workspace dimensions to match the blocklyDiv
Blockly.svgResize(this.workspace);
}
}
// Generate Python code from the Blockly workspace and return as JSON // Generate Python code from the Blockly workspace and return as JSON
generateStrategyJson() { generateStrategyJson() {
// Initialize Python generator with the current workspace // Initialize Python generator with the current workspace
@ -38,15 +45,43 @@ class Strategies {
// Generate Python code from the Blockly workspace // Generate Python code from the Blockly workspace
const pythonCode = Blockly.Python.workspaceToCode(this.workspace); const pythonCode = Blockly.Python.workspaceToCode(this.workspace);
// Create a strategy object with generated code // Serialize the workspace to XML format
const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace);
const workspaceXmlText = Blockly.Xml.domToText(workspaceXml);
const json = { const json = {
name: document.getElementById('name_box').value, name: document.getElementById('name_box').value,
code: pythonCode // This is the generated Python code code: pythonCode, // This is the generated Python code
workspace: workspaceXmlText // Serialized workspace XML
}; };
return JSON.stringify(json); return JSON.stringify(json);
} }
restoreWorkspaceFromXml(workspaceXmlText) {
// Convert the text back into an XML DOM object
const workspaceXml = Blockly.Xml.textToDom(workspaceXmlText);
// Clear the current workspace before loading a new one
if (this.workspace) {
this.workspace.clear();
}
// Load the workspace from the XML DOM object
Blockly.Xml.domToWorkspace(workspaceXml, this.workspace);
}
fetchSavedStrategies() {
fetch('/get_strategies', {
method: 'GET'
})
.then(response => response.json())
.then(data => {
this.strategies = data.strategies;
this.update_html();
})
.catch(error => console.error('Error fetching strategies:', error));
}
// Show the "Create New Strategy" form (open the workspace) // Show the "Create New Strategy" form (open the workspace)
open_form() { open_form() {
const formElement = document.getElementById("new_strat_form"); const formElement = document.getElementById("new_strat_form");
@ -70,23 +105,40 @@ class Strategies {
} }
} }
// Submit the strategy and send it to the server
submit() { submit() {
// Generate the Python code as JSON // Generate the Python code as JSON
const strategyJson = this.generateStrategyJson(); const strategyJson = this.generateStrategyJson();
// Send the strategy to the server // Get the fee and public checkbox values
fetch('/new_strategy', { const fee = parseFloat(document.getElementById('fee_box').value) || 0;
method: 'POST', if (fee < 0) {
headers: { 'Content-Type': 'application/json' }, alert("Fee cannot be negative");
body: strategyJson return;
}) }
.then(response => response.json())
.then(data => console.log('Strategy submitted successfully:', data)) const is_public = document.getElementById('public_checkbox').checked ? 1 : 0;
.catch(error => console.error('Error submitting strategy:', error));
// Merge additional fields into the strategy data
const strategyData = {
...JSON.parse(strategyJson), // Spread the existing strategy JSON data
fee, // Add the fee
public: is_public // Add the public status
};
// Send the strategy to the server via WebSocket through the Comms class
if (window.UI.data.comms) { // Assuming the Comms instance is accessible via window.UI.data.comms
window.UI.data.comms.sendToApp('new_strategy', strategyData);
} else {
console.error("Comms instance not available.");
}
} }
// Update the display of the created strategies toggleFeeBox() {
const publicCheckbox = document.getElementById('public_checkbox');
const feeBox = document.getElementById('fee_box');
feeBox.disabled = !publicCheckbox.checked; // Enable feeBox only if publicCheckbox is checked
}
// Update the UI with saved strategies
update_html() { update_html() {
let stratsHtml = ''; let stratsHtml = '';
const onClick = "window.UI.strats.del(this.value);"; const onClick = "window.UI.strats.del(this.value);";

View File

@ -1,5 +1,7 @@
class Comms { class Comms {
constructor() { constructor() {
this.connectionOpen = false;
this.appCon = null; // WebSocket connection for app communication
// Callback collections that will receive various updates. // Callback collections that will receive various updates.
this.candleUpdateCallbacks = []; this.candleUpdateCallbacks = [];
@ -170,33 +172,55 @@ class Comms {
} }
/** /**
* Sends a message to the application server. * Sends a message to the application server via WebSocket.
* Automatically includes the user_name from `window.UI.data.user_name` for authentication.
* @param {string} messageType - The type of the message. * @param {string} messageType - The type of the message.
* @param {Object} data - The data to be sent with the message. * @param {Object} data - The data to be sent with the message.
*/ */
sendToApp(messageType, data) { sendToApp(messageType, data) {
console.log('window.UI.data.comms.sendToApp(): Sending->'); const user_name = window.UI.data.user_name; // Access user_name from window.UI.data
console.log(JSON.stringify({ message_type: messageType, data: data }));
if (!user_name) {
console.error('User not logged in. Cannot send message.');
return;
}
const messageData = {
message_type: messageType,
data: {
...data, // Include the existing data
user_name: user_name // Add user_name for authentication
}
};
console.log('Comms: Sending->', JSON.stringify(messageData));
if (this.connectionOpen) { if (this.connectionOpen) {
this.appCon.send(JSON.stringify({ message_type: messageType, data: data })); this.appCon.send(JSON.stringify(messageData));
} else { } else {
setTimeout(() => { setTimeout(() => {
window.UI.data.comms.appCon.send(JSON.stringify({ message_type: messageType, data: data })); if (this.appCon) {
this.appCon.send(JSON.stringify(messageData));
}
}, 1000); }, 1000);
} }
} }
/** /**
* Sets up the WebSocket connection to the application server. * Sets up the WebSocket connection to the application server.
*/ */
setAppCon() { setAppCon() {
this.appCon = new WebSocket('ws://localhost:5000/ws'); this.appCon = new WebSocket('ws://localhost:5000/ws');
// On connection open
this.appCon.onopen = () => { this.appCon.onopen = () => {
console.log("WebSocket connection established");
this.appCon.send("Connection OK"); this.appCon.send("Connection OK");
this.connectionOpen = true; this.connectionOpen = true;
}; };
// Handle incoming messages
this.appCon.addEventListener('message', (event) => { this.appCon.addEventListener('message', (event) => {
if (event.data) { if (event.data) {
const message = JSON.parse(event.data); const message = JSON.parse(event.data);
@ -207,24 +231,29 @@ class Comms {
} }
if (message && message.reply !== undefined) { if (message && message.reply !== undefined) {
// Handle different reply types from the server
if (message.reply === 'updates') { if (message.reply === 'updates') {
const { i_updates, s_updates, stg_updts, trade_updts } = message.data; const { i_updates, s_updates, stg_updts, trade_updts } = message.data;
// Handle indicator updates
if (i_updates) { if (i_updates) {
this.indicatorUpdate(i_updates); this.indicatorUpdate(i_updates);
window.UI.signals.i_update(i_updates); window.UI.signals.i_update(i_updates);
} }
// Handle signal updates
if (s_updates) { if (s_updates) {
const updates = s_updates; window.UI.signals.update_signal_states(s_updates);
window.UI.signals.update_signal_states(updates); window.UI.alerts.publish_alerts('signal_changes', s_updates);
window.UI.alerts.publish_alerts('signal_changes', updates);
} }
// Handle strategy updates
if (stg_updts) { if (stg_updts) {
const stg_updts = stg_updts;
window.UI.strats.update_received(stg_updts); window.UI.strats.update_received(stg_updts);
} }
// Handle trade updates
if (trade_updts) { if (trade_updts) {
const trade_updts = trade_updts;
window.UI.trade.update_received(trade_updts); window.UI.trade.update_received(trade_updts);
} }
} else if (message.reply === 'signals') { } else if (message.reply === 'signals') {
@ -248,8 +277,20 @@ class Comms {
} }
} }
}); });
// On connection close
this.appCon.onclose = () => {
console.log("WebSocket connection closed");
this.connectionOpen = false;
};
// On WebSocket error
this.appCon.onerror = (error) => {
console.error("WebSocket error:", error);
};
} }
/** /**
* Sets up a WebSocket connection to the exchange for receiving candlestick data. * Sets up a WebSocket connection to the exchange for receiving candlestick data.
* @param {string} interval - The interval of the candlestick data. * @param {string} interval - The interval of the candlestick data.

View File

@ -1,15 +1,15 @@
class User_Interface { class User_Interface {
constructor() { constructor() {
// Initialize all components needed by the user interface // Initialize all components needed by the user interface
this.strats = new Strategies('strats_display'); // Handles strategies display and interaction this.strats = new Strategies('strats_display');
this.exchanges = new Exchanges(); // Manages exchange-related data this.exchanges = new Exchanges();
this.data = new Data(); // Stores and maintains application-wide data this.data = new Data();
this.controls = new Controls(); // Handles user controls this.controls = new Controls();
this.signals = new Signals(this.data.indicators); // Manages signals used in strategies this.signals = new Signals(this.data.indicators);
this.alerts = new Alerts("alert_list"); // Manages the alerts system this.alerts = new Alerts("alert_list");
this.trade = new Trade(); // Manages trade-related data and operations this.trade = new Trade();
this.users = new Users(); // Handles user login and account details this.users = new Users();
this.indicators = new Indicators(this.data.comms); // Manages indicators and interaction with charts this.indicators = new Indicators(this.data.comms);
// Register a callback function for when indicator updates are received from the data object // Register a callback function for when indicator updates are received from the data object
this.data.registerCallback_i_updates(this.indicators.update); this.data.registerCallback_i_updates(this.indicators.update);
@ -18,41 +18,124 @@ class User_Interface {
this.initializeAll(); this.initializeAll();
} }
/**
* Initializes all components after the DOM has loaded.
* This includes initializing charts, signals, strategies, and other interface components.
* Components that rely on external libraries like Blockly are initialized after they are ready.
*/
initializeAll() { initializeAll() {
// Use an arrow function to preserve the value of 'this' (User_Interface instance)
window.addEventListener('load', () => { window.addEventListener('load', () => {
// Initialize the Charts component with the required data this.initializeChartsAndIndicators();
let chart_init_data = { this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats));
chart1_id: this.data.chart1_id, // ID of the first chart element
chart2_id: this.data.chart2_id, // ID of the second chart element
chart3_id: this.data.chart3_id, // ID of the third chart element
trading_pair: this.data.trading_pair, // Active trading pair
price_history: this.data.price_history // Historical price data
};
this.charts = new Charts(chart_init_data); // Initialize the charts
// Initialize indicators and link them to the charts
let ind_init_data = {
indicators: this.data.indicators, // List of indicators
indicator_data: this.data.indicator_data // Data for rendering indicators
};
this.indicators.addToCharts(this.charts, ind_init_data); // Add indicators to the charts
// Initialize various components that don't depend on external libraries
this.signals.request_signals(); // Request and initialize trading signals
this.alerts.set_target(); // Set up alert notifications
this.controls.init_TP_selector(); // Initialize trade parameter selectors
this.trade.initialize(); // Initialize trade-related data and UI elements
this.exchanges.initialize(); // Initialize exchange-related data
this.strats.initialize(); // Initialize strategies once Blockly is ready
}); });
} }
initializeChartsAndIndicators() {
let chart_init_data = {
chart1_id: this.data.chart1_id,
chart2_id: this.data.chart2_id,
chart3_id: this.data.chart3_id,
trading_pair: this.data.trading_pair,
price_history: this.data.price_history
};
this.charts = new Charts(chart_init_data);
let ind_init_data = {
indicators: this.data.indicators,
indicator_data: this.data.indicator_data
};
this.indicators.addToCharts(this.charts, ind_init_data);
this.signals.request_signals();
this.alerts.set_target();
this.controls.init_TP_selector();
this.trade.initialize();
this.exchanges.initialize();
this.strats.initialize();
}
/**
* Make a popup resizable, and optionally pass a resize callback (like for Blockly workspaces).
* @param {string} popupId - The ID of the popup to make resizable.
* @param {function|null} resizeCallback - Optional callback to run when resizing (for Blockly or other elements).
*/
initializeResizablePopup(popupId, resizeCallback = null) {
const popupElement = document.getElementById(popupId);
this.dragElement(popupElement);
this.makeResizable(popupElement, resizeCallback);
}
/**
* Make an element draggable by dragging its header.
* @param {HTMLElement} elm - The element to make draggable.
*/
dragElement(elm) {
const header = document.getElementById("draggable_header");
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (header) {
header.onmousedown = dragMouseDown;
} else {
elm.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
elm.style.top = (elm.offsetTop - pos2) + "px";
elm.style.left = (elm.offsetLeft - pos1) + "px";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
}
/**
* Make an element resizable and optionally call a resize callback (like for Blockly workspace).
* @param {HTMLElement} elm - The element to make resizable.
* @param {function|null} resizeCallback - Optional callback to resize specific content.
*/
makeResizable(elm, resizeCallback = null) {
const resizer = document.getElementById("resize-br");
let originalWidth = 0;
let originalHeight = 0;
let originalMouseX = 0;
let originalMouseY = 0;
resizer.addEventListener('mousedown', function(e) {
e.preventDefault();
originalWidth = parseFloat(getComputedStyle(elm, null).getPropertyValue('width').replace('px', ''));
originalHeight = parseFloat(getComputedStyle(elm, null).getPropertyValue('height').replace('px', ''));
originalMouseX = e.pageX;
originalMouseY = e.pageY;
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
const resize = (e) => {
elm.style.width = originalWidth + (e.pageX - originalMouseX) + 'px';
elm.style.height = originalHeight + (e.pageY - originalMouseY) + 'px';
// Optionally call the resize callback (for Blockly or other resizable components)
if (resizeCallback) {
resizeCallback();
}
};
const stopResize = () => {
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResize);
};
}
} }
// Instantiate the User_Interface class and assign it to a global variable for access in other parts of the app // Instantiate the User_Interface class and assign it to a global variable for access in other parts of the app

View File

@ -1,24 +1,111 @@
<!-- Strategy Form Popup --> <!-- Strategy Form Popup -->
<div class="form-popup" id="new_strat_form" style="display: none; overflow: auto;"> <div class="form-popup" id="new_strat_form" style="display: none; overflow: hidden; position: absolute; width: 400px; height: 640px; border-radius: 10px;">
<form class="form-container" style="display: grid; grid-template-columns: 1fr; grid-template-rows: auto;">
<!-- Panel 1 of 1 -->
<div id="strat_pan_1" class="form_panels" style="display: grid; grid-template-columns:repeat(2,1fr); grid-template-rows: repeat(6,1fr); gap: 10px;">
<!-- Panel title -->
<h1 style="grid-column: 1 / span 2; grid-row: 1;">Create New Strategy</h1>
<div style="grid-column: 1 / span 2; grid-row: 2;">Name: <input class="ietextbox" type="text" id="name_box" name="name_box" value="Indicator name"></div>
<!-- Blockly workspace --> <!-- Draggable Header Section -->
<div id="blocklyDiv" style="grid-column: 1 / span 2; grid-row: 3 / span 3; height: 300px; width: 100%;"></div> <div id="draggable_header" style="cursor: move; padding: 10px; background-color: #3E3AF2; color: white; border-bottom: 1px solid #ccc;">
<h1 style="margin: 0;">Create New Strategy</h1>
<!-- Buttons -->
<div style="grid-column: 1 / span 2; grid-row: 6; text-align: center;">
<button type="button" class="btn cancel" onclick="UI.strats.close_form()">Close</button>
<button type="button" class="btn next" onclick="UI.strats.submit()">Create Strategy</button>
</div>
</div>
</form>
</div> </div>
<!-- Main Content (Scrollable) -->
<form class="form-container" style="display: grid; grid-template-columns: 1fr; grid-template-rows: auto; height: calc(100% - 60px); overflow-y: auto;">
<!-- Panel 1 of 1 -->
<div id="strat_pan_1" class="form_panels" style="display: grid; grid-template-columns: 1fr; grid-template-rows: auto auto; gap: 10px; padding: 10px;">
<!-- Blockly workspace -->
<div id="blocklyDiv" style="grid-column: 1; height: 300px; width: 100%;"></div>
<!-- Name of the strategy -->
<div style="grid-column: 1;">
<label for="name_box" style="display: inline-block; width: 20%;">Name:</label>
<input class="ietextbox" type="text" id="name_box" name="name_box" value="Indicator name" style="width: 75%; display: inline-block;">
</div>
<!-- Public checkbox -->
<div style="grid-column: 1;">
<label for="public_checkbox" style="display: inline-block; width: 20%;">Public:</label>
<input type="checkbox" id="public_checkbox" name="public_checkbox" style="display: inline-block;" onclick="UI.strats.toggleFeeBox()">
</div>
<!-- Fee input field -->
<div style="grid-column: 1;">
<label for="fee_box" style="display: inline-block; width: 20%;">Fee(%):</label>
<input class="ietextbox" type="number" id="fee_box" name="fee_box" placeholder="0.00" step="0.01" min="0" style="width: 65px; display: inline-block;" disabled>
</div>
<!-- Buttons -->
<div style="grid-column: 1; text-align: center;">
<button type="button" class="btn cancel" onclick="UI.strats.close_form()">Close</button>
<button type="button" class="btn next" onclick="UI.strats.submit()">Create Strategy</button>
</div>
</div>
</form>
<!-- Single Resize Handle in Bottom Right -->
<div class="resize-handle" id="resize-br"></div>
</div>
<!-- Add CSS to hide scrollbars but allow scrolling and fix the resize handle -->
<style>
/* Hide scrollbars but still allow scrolling */
.form-popup {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.form-popup::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* Style the draggable header section */
#draggable_header {
cursor: move;
padding: 10px;
background-color: #3E3AF2;
color: white;
border-bottom: 1px solid #ccc;
position: relative;
z-index: 10;
}
/* Style the form container to be scrollable and take up remaining space */
.form-container {
height: calc(100% - 60px); /* Takes up the remaining height minus header height */
overflow-y: auto; /* Makes it scrollable */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.form-container::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* Label and input field styling */
.form_panels label {
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
.ietextbox {
width: 75%;
display: inline-block;
}
/* Resize handle styling */
.resize-handle {
width: 20px;
height: 20px;
background-color: #000;
position: absolute;
bottom: 0;
right: 0;
cursor: se-resize;
z-index: 10;
border-radius: 3px;
}
</style>
<xml id="toolbox" style="display: none"> <xml id="toolbox" style="display: none">
<category name="Indicators" colour="230"> <category name="Indicators" colour="230">
<!-- Indicator blocks go here --> <!-- Indicator blocks go here -->
@ -45,6 +132,3 @@
</category> </category>
</xml> </xml>