I decoupled the comms class from a few other classes. I updated some of the ui in regard to this indicator readouts. An just about to attempt to break up the strategies class.

This commit is contained in:
Rob 2024-10-09 09:09:52 -03:00
parent 86843e8cb4
commit 89e0f8b849
27 changed files with 2369 additions and 1019 deletions

View File

@ -1,14 +1,11 @@
numpy==1.24.3 numpy<2.0.0
flask==2.3.2 flask==3.0.3
flask_cors==3.0.10
flask_sock==0.7.0
config~=0.5.1 config~=0.5.1
PyYAML~=6.0 PyYAML==6.0.2
requests==2.30.0 requests==2.30.0
pandas==2.0.1 pandas==2.2.3
passlib~=1.7.4 passlib~=1.7.4
SQLAlchemy==2.0.13 ccxt==4.4.8
ccxt==4.3.65
email-validator~=2.2.0 pytz==2024.2
TA-Lib~=0.4.32 backtrader==1.9.78.123
bcrypt~=4.2.0

View File

@ -48,7 +48,8 @@ class BrighterTrades:
self.strategies = Strategies(self.data, self.trades) self.strategies = Strategies(self.data, self.trades)
# Object responsible for testing trade and strategies data. # Object responsible for testing trade and strategies data.
self.backtester = Backtester() self.backtester = Backtester(data_cache=self.data, strategies=self.strategies)
self.backtests = {} # In-memory storage for backtests (replace with DB access in production)
def create_new_user(self, email: str, username: str, password: str) -> bool: def create_new_user(self, email: str, username: str, password: str) -> bool:
""" """
@ -219,6 +220,7 @@ class BrighterTrades:
chart_view = self.users.get_chart_view(user_name=user_name) chart_view = self.users.get_chart_view(user_name=user_name)
indicator_types = self.indicators.get_available_indicator_types() indicator_types = self.indicators.get_available_indicator_types()
available_indicators = self.indicators.get_indicator_list(user_name) available_indicators = self.indicators.get_indicator_list(user_name)
exchange = self.exchanges.get_exchange(ename=chart_view.get('exchange'), uname=user_name)
if not chart_view: if not chart_view:
chart_view = {'timeframe': '', 'exchange_name': '', 'market': ''} chart_view = {'timeframe': '', 'exchange_name': '', 'market': ''}
@ -234,7 +236,9 @@ class BrighterTrades:
'exchange_name': chart_view.get('exchange_name'), 'exchange_name': chart_view.get('exchange_name'),
'trading_pair': chart_view.get('market'), 'trading_pair': chart_view.get('market'),
'user_name': user_name, 'user_name': user_name,
'public_exchanges': self.exchanges.get_public_exchanges() 'public_exchanges': self.exchanges.get_public_exchanges(),
'intervals': exchange.intervals if exchange else [],
'symbols': exchange.get_symbols() if exchange else {}
} }
return js_data return js_data
@ -569,6 +573,12 @@ class BrighterTrades:
""" Return a JSON object of all the trades in the trades instance.""" """ Return a JSON object of all the trades in the trades instance."""
return self.trades.get_trades('dict') return self.trades.get_trades('dict')
def delete_backtest(self, msg_data):
""" Delete an existing backtest. """
backtest_name = msg_data.get('name')
if backtest_name in self.backtests:
del self.backtests[backtest_name]
def adjust_setting(self, user_name: str, setting: str, params: Any): def adjust_setting(self, user_name: str, setting: str, params: Any):
""" """
Adjusts the specified setting for a user. Adjusts the specified setting for a user.
@ -638,12 +648,14 @@ class BrighterTrades:
# self.candles.set_cache(user_name=user_name) # self.candles.set_cache(user_name=user_name)
return return
def process_incoming_message(self, msg_type: str, msg_data: dict | str) -> dict | None: def process_incoming_message(self, msg_type: str, msg_data: dict | str, socket_conn) -> dict | None:
""" """
Processes an incoming message and performs the corresponding actions based on the message type and data. Processes an incoming message and performs the corresponding actions based on the message type and data.
:param msg_type: The type of the incoming message. :param msg_type: The type of the incoming message.
:param msg_data: The data associated with the incoming message. :param msg_data: The data associated with the incoming message.
:param socket_conn: The WebSocket connection to send updates back to the client.
:return: dict|None - A dictionary containing the response message and data, or None if no response is needed or :return: dict|None - A dictionary containing the response message and data, or None if no response is needed or
no data is found to ensure the WebSocket channel isn't burdened with unnecessary no data is found to ensure the WebSocket channel isn't burdened with unnecessary
communication. communication.
@ -706,6 +718,17 @@ class BrighterTrades:
r_data = self.connect_or_config_exchange(user_name=user, exchange_name=exchange, api_keys=keys) r_data = self.connect_or_config_exchange(user_name=user, exchange_name=exchange, api_keys=keys)
return standard_reply("Exchange_connection_result", r_data) return standard_reply("Exchange_connection_result", r_data)
# Handle backtest operations
if msg_type == 'submit_backtest':
user_id = self.get_user_info(user_name=msg_data['user_name'], info='User_id')
# Pass socket_conn to the backtest handler
result = self.backtester.handle_backtest_message(user_id, msg_data, socket_conn)
return standard_reply("backtest_submitted", result)
if msg_type == 'delete_backtest':
self.delete_backtest(msg_data)
return standard_reply("backtest_deleted", {})
if msg_type == 'reply': if msg_type == 'reply':
# If the message is a reply log the response to the terminal. # If the message is a reply log the response to the terminal.
print(f"\napp.py:Received reply: {msg_data}") print(f"\napp.py:Received reply: {msg_data}")

View File

@ -8,7 +8,6 @@ from typing import Any, Tuple, List, Optional
import pandas as pd import pandas as pd
import numpy as np import numpy as np
import json
from indicators import Indicator, indicators_registry from indicators import Indicator, indicators_registry
from shared_utilities import unix_time_millis from shared_utilities import unix_time_millis

View File

@ -562,7 +562,8 @@ class UserIndicatorManagement(UserExchangeManagement):
else: else:
raise ValueError(f'{specific_property} is not a specific property of chart_views') raise ValueError(f'{specific_property} is not a specific property of chart_views')
self.modify_user_data(username=user_name, field_name='chart_views', new_data=chart_view) chart_view_str = json.dumps(chart_view)
self.modify_user_data(username=user_name, field_name='chart_views', new_data=chart_view_str)
class Users(UserIndicatorManagement): class Users(UserIndicatorManagement):

View File

@ -129,8 +129,8 @@ def ws(socket_conn):
socket_conn.send(json.dumps({"success": False, "message": "User not logged in"})) socket_conn.send(json.dumps({"success": False, "message": "User not logged in"}))
return return
# Process the incoming message based on the type # Process the incoming message based on the type, passing socket_conn
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=socket_conn)
# Send the response back to the client # Send the response back to the client
if resp: if resp:

View File

@ -1,53 +1,312 @@
from dataclasses import dataclass, field, asdict import ast
from itertools import count import json
import re
import backtrader as bt
@dataclass import datetime as dt
class Order: from DataCache_v3 import DataCache
symbol: str from Strategies import Strategies
clientOrderId: hex import threading
transactTime: float import numpy as np
price: float
origQty: float
executedQty: float
cummulativeQuoteQty: float
status: str
timeInForce: str
type: str
side: str
orderId: int = field(default_factory=count().__next__)
class Backtester: class Backtester:
def __init__(self): def __init__(self, data_cache: DataCache, strategies: Strategies):
self.orders = [] """ Initialize the Backtesting class with a cache for back-tests """
self.data_cache = data_cache
self.strategies = strategies
# Create a cache for storing back-tests
self.data_cache.create_cache('tests', cache_type='row', size_limit=100,
default_expiration=dt.timedelta(days=1),
eviction_policy='evict')
def create_test_order(self, symbol, side, type, timeInForce, quantity, price=None, newClientOrderId=None): def get_default_chart_view(self, user_name):
order = Order(symbol=symbol, clientOrderId=newClientOrderId, transactTime=1507725176595, price=price, """Fetch default chart view if no specific source is provided."""
origQty=quantity, executedQty=quantity, cummulativeQuoteQty=(quantity * price), status='FILLED', return self.data_cache.get_datacache_item(
timeInForce=timeInForce, type=type, side=side) item_name='chart_view', cache_name='users', filter_vals=('user_name', user_name))
self.orders.append(order)
return asdict(order)
def create_order(self, symbol, side, type, timeInForce, quantity, price=None, newClientOrderId=None): def cache_backtest(self, user_name, backtest_name, backtest_data):
order = Order(symbol=symbol, clientOrderId=newClientOrderId, transactTime=1507725176595, price=price, """ Cache the backtest data for a user """
origQty=quantity, executedQty=quantity, cummulativeQuoteQty=(quantity * price), status='FILLED', columns = ('user_name', 'strategy_name', 'start_time', 'capital', 'commission', 'results')
timeInForce=timeInForce, type=type, side=side) values = (
self.orders.append(order) backtest_data.get('user_name'),
return asdict(order) backtest_data.get('strategy'),
backtest_data.get('start_date'),
backtest_data.get('capital', 10000), # Default capital if not provided
backtest_data.get('commission', 0.001), # Default commission
None # No results yet; will be filled in after backtest completion
)
cache_key = f"backtest:{user_name}:{backtest_name}"
self.data_cache.insert_row_into_cache('tests', columns, values, key=cache_key)
def get_order(self, symbol, orderId): def map_user_strategy(self, user_strategy):
for order in self.orders: """Maps user strategy details into a Backtrader-compatible strategy class."""
if order.symbol == symbol:
if order.orderId == orderId:
return asdict(order)
return None
def get_precision(self, symbol=None): class MappedStrategy(bt.Strategy):
return 3 params = (
('initial_cash', user_strategy['params'].get('initial_cash', 10000)),
('commission', user_strategy['params'].get('commission', 0.001)),
)
def get_min_notional_qty(self, symbol=None): def __init__(self):
return 10 # Extract unique sources (exchange, symbol, timeframe) from blocks
self.sources = self.extract_sources(user_strategy)
def get_min_qty(self, symbol=None): # Map of source to data feed (used later in next())
return 0.001 self.source_data_feed_map = {}
def extract_sources(self, user_strategy):
"""Extracts unique sources from the strategy."""
sources = []
for block in user_strategy.get('blocks', []):
if block.get('type') in ['last_candle_value', 'trade_action']:
source = self.extract_source_from_block(block)
if source and source not in sources:
sources.append(source)
elif block.get('type') == 'target_market':
target_source = self.extract_target_market(block)
if target_source and target_source not in sources:
sources.append(target_source)
return sources
def extract_source_from_block(self, block):
"""Extract source (exchange, symbol, timeframe) from a strategy block."""
source = {}
if block.get('type') == 'last_candle_value':
source = block.get('SOURCE', None)
# If SOURCE is missing, use the trade target or default
if not source:
source = self.get_default_chart_view(self.user_name) # Fallback to default
return source
def extract_target_market(self, block):
"""Extracts target market data (timeframe, exchange, symbol) from the trade_action block."""
target_market = block.get('target_market', {})
return {
'timeframe': target_market.get('TF', '5m'),
'exchange': target_market.get('EXC', 'Binance'),
'symbol': target_market.get('SYM', 'BTCUSD')
}
def next(self):
"""Execute trading logic using the compiled strategy."""
try:
exec(self.compiled_logic, {'self': self, 'data_feeds': self.source_data_feed_map})
except Exception as e:
print(f"Error executing trading logic: {e}")
return MappedStrategy
def prepare_data_feed(self, start_date: str, sources: list, user_name: str):
"""
Prepare multiple data feeds based on the start date and list of sources.
"""
try:
# Convert the start date to a datetime object
start_dt = dt.datetime.strptime(start_date, '%Y-%m-%dT%H:%M')
# Dictionary to map each source to its corresponding data feed
data_feeds = {}
for source in sources:
# Ensure exchange details contain required keys (fallback if missing)
asset = source.get('asset', 'BTCUSD')
timeframe = source.get('timeframe', '5m')
exchange = source.get('exchange', 'Binance')
# Fetch OHLC data from DataCache based on the source
ex_details = [asset, timeframe, exchange, user_name]
data = self.data_cache.get_records_since(start_dt, ex_details)
# Return the data as a Pandas DataFrame compatible with Backtrader
data_feeds[tuple(ex_details)] = data
return data_feeds
except Exception as e:
print(f"Error preparing data feed: {e}")
return None
def run_backtest(self, strategy, data_feed_map, msg_data, user_name, callback, socket_conn):
"""
Runs a backtest using Backtrader on a separate thread and calls the callback with the results when finished.
Also sends progress updates to the client via WebSocket.
"""
def execute_backtest():
cerebro = bt.Cerebro()
# Add the mapped strategy to the backtest
cerebro.addstrategy(strategy)
# Add all the data feeds to Cerebro
total_bars = 0 # Total number of data points (bars) across all feeds
for source, data_feed in data_feed_map.items():
bt_feed = bt.feeds.PandasData(dataname=data_feed)
cerebro.adddata(bt_feed)
strategy.source_data_feed_map[source] = bt_feed
total_bars = max(total_bars, len(data_feed)) # Get the total bars from the largest feed
# Capture initial capital
initial_capital = cerebro.broker.getvalue()
# Progress tracking variables
current_bar = 0
last_progress = 0
# Custom next function to track progress (if you have a large dataset)
def track_progress():
nonlocal current_bar, last_progress
current_bar += 1
progress = (current_bar / total_bars) * 100
# Send progress update every 10% increment
if progress >= last_progress + 10:
last_progress += 10
socket_conn.send(json.dumps({"progress": int(last_progress)}))
# Attach the custom next method to the strategy
strategy.next = track_progress
# Run the backtest
print("Running backtest...")
start_time = dt.datetime.now()
cerebro.run()
end_time = dt.datetime.now()
# Extract performance metrics
final_value = cerebro.broker.getvalue()
run_duration = (end_time - start_time).total_seconds()
# Send 100% completion
socket_conn.send(json.dumps({"progress": 100}))
# Prepare the results to pass into the callback
callback({
"initial_capital": initial_capital,
"final_portfolio_value": final_value,
"run_duration": run_duration
})
# Map the user strategy and prepare the data feeds
sources = strategy.extract_sources()
data_feed_map = self.prepare_data_feed(msg_data['start_date'], sources, user_name)
# Run the backtest in a separate thread
thread = threading.Thread(target=execute_backtest)
thread.start()
def handle_backtest_message(self, user_id, msg_data, socket_conn):
user_name = msg_data.get('user_name')
backtest_name = f"{msg_data['strategy']}_backtest"
# Cache the backtest data
self.cache_backtest(user_name, backtest_name, msg_data)
# Fetch the strategy using user_id and strategy_name
strategy_name = msg_data.get('strategy')
user_strategy = self.strategies.get_strategy_by_name(user_id=user_id, name=strategy_name)
if not user_strategy:
return {"error": f"Strategy {strategy_name} not found for user {user_name}"}
# Extract sources from the strategy JSON
sources = self.extract_sources_from_strategy_json(user_strategy.get('strategy_json'))
if not sources:
return {"error": "No valid sources found in the strategy."}
# Prepare the data feed map based on extracted sources
data_feed_map = self.prepare_data_feed(msg_data['start_date'], sources, user_name)
if data_feed_map is None:
return {"error": "Data feed could not be prepared. Please check the data source."}
# Map the user strategy to a Backtrader strategy class
mapped_strategy = self.map_user_strategy(user_strategy)
# Define the callback function to handle backtest completion
def backtest_callback(results):
self.store_backtest_results(user_name, backtest_name, results)
self.update_strategy_stats(user_id, strategy_name, results)
# Run the backtest and pass the callback function, msg_data, and user_name
self.run_backtest(mapped_strategy, data_feed_map, msg_data, user_name, backtest_callback, socket_conn)
return {"reply": "backtest_started"}
def extract_sources_from_strategy_json(self, strategy_json):
sources = []
# Parse the JSON strategy to extract sources
def traverse_blocks(blocks):
for block in blocks:
if block['type'] == 'source':
source = {
'timeframe': block['fields'].get('TF'),
'exchange': block['fields'].get('EXC'),
'symbol': block['fields'].get('SYM')
}
sources.append(source)
# Recursively traverse inputs and statements
if 'inputs' in block:
traverse_blocks(block['inputs'].values())
if 'statements' in block:
traverse_blocks(block['statements'].values())
if 'next' in block:
traverse_blocks([block['next']])
traverse_blocks(strategy_json)
return sources
def update_strategy_stats(self, user_id, strategy_name, results):
""" Update the strategy stats with the backtest results """
strategy = self.strategies.get_strategy_by_name(user_id=user_id, name=strategy_name)
if strategy:
initial_capital = results['initial_capital']
final_value = results['final_portfolio_value']
returns = np.array(results['returns'])
equity_curve = np.array(results['equity_curve'])
trades = results['trades']
total_return = (final_value - initial_capital) / initial_capital * 100
risk_free_rate = 0.0
mean_return = np.mean(returns)
std_return = np.std(returns)
sharpe_ratio = (mean_return - risk_free_rate) / std_return if std_return != 0 else 0
running_max = np.maximum.accumulate(equity_curve)
drawdowns = (equity_curve - running_max) / running_max
max_drawdown = np.min(drawdowns) * 100
num_trades = len(trades)
wins = sum(1 for trade in trades if trade['profit'] > 0)
losses = num_trades - wins
win_loss_ratio = wins / losses if losses != 0 else wins
stats = {
'total_return': total_return,
'sharpe_ratio': sharpe_ratio,
'max_drawdown': max_drawdown,
'number_of_trades': num_trades,
'win_loss_ratio': win_loss_ratio,
}
strategy.update_stats(stats)
else:
print(f"Strategy {strategy_name} not found for user {user_id}.")
def store_backtest_results(self, user_name, backtest_name, results):
""" Store the backtest results in the cache """
cache_key = f"backtest:{user_name}:{backtest_name}"
filter_vals = [('tbl_key', cache_key)]
backtest_data = self.data_cache.get_rows_from_cache('tests', filter_vals)
if not backtest_data.empty:
backtest_data['results'] = results
self.data_cache.insert_row_into_cache('tests', backtest_data.keys(), backtest_data.values(), key=cache_key)
else:
print(f"Backtest {backtest_name} not found in cache.")

View File

@ -207,6 +207,7 @@ class MACD(Indicator):
self.properties.setdefault('fast_p', 12) self.properties.setdefault('fast_p', 12)
self.properties.setdefault('slow_p', 26) self.properties.setdefault('slow_p', 26)
self.properties.setdefault('signal_p', 9) self.properties.setdefault('signal_p', 9)
self.properties.setdefault('color_1', generate_random_color()) # Upper band self.properties.setdefault('color_1', generate_random_color()) # Upper band
self.properties.setdefault('color_2', generate_random_color()) # Middle band self.properties.setdefault('color_2', generate_random_color()) # Middle band
self.properties.setdefault('color_3', generate_random_color()) # Lower band self.properties.setdefault('color_3', generate_random_color()) # Lower band

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,190 @@
class Backtesting { class Backtesting {
constructor() { constructor(ui) {
this.height = height; this.ui = ui;
} this.comms = ui.data.comms;
this.tests = []; // Stores the list of saved backtests
this.target_id = 'backtest_display'; // The container to display backtests
// Register handlers for backtesting messages
this.comms.on('backtest_results', this.handleBacktestResults.bind(this));
this.comms.on('progress', this.handleProgress.bind(this));
this.comms.on('backtests_list', this.handleBacktestsList.bind(this));
this.comms.on('backtest_deleted', this.handleBacktestDeleted.bind(this));
this.comms.on('updates', this.handleUpdates.bind(this));
}
handleBacktestResults(data) {
console.log("Backtest results received:", data.results);
// Logic to stop running animation and display results
this.stopRunningAnimation(data.results);
}
handleProgress(data) {
console.log("Backtest progress:", data.progress);
// Logic to update progress bar
this.updateProgressBar(data.progress);
}
handleBacktestsList(data) {
console.log("Backtests list received:", data.tests);
// Logic to update backtesting UI
this.set_data(data.tests);
}
handleBacktestDeleted(data) {
console.log(`Backtest "${data.name}" was successfully deleted.`);
// Logic to refresh list of backtests
this.fetchSavedTests();
}
handleUpdates(data) {
const { trade_updts } = data;
if (trade_updts) {
this.ui.trade.update_received(trade_updts);
}
}
updateProgressBar(progress) {
const progressBar = document.getElementById('progress_bar');
if (progressBar) {
progressBar.style.width = `${progress}%`;
progressBar.textContent = `${progress}%`;
}
}
showRunningAnimation() {
const resultsContainer = document.getElementById('backtest-results');
const resultsDisplay = document.getElementById('results_display');
const progressContainer = document.getElementById('backtest-progress-container');
const progressBar = document.getElementById('progress_bar');
resultsContainer.style.display = 'none';
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
progressBar.textContent = '0%';
resultsDisplay.innerHTML = '';
}
displayTestResults(results) {
const resultsContainer = document.getElementById('backtest-results');
const resultsDisplay = document.getElementById('results_display');
resultsContainer.style.display = 'block';
resultsDisplay.innerHTML = `<pre>${JSON.stringify(results, null, 2)}</pre>`;
}
stopRunningAnimation(results) {
const progressContainer = document.getElementById('backtest-progress-container');
progressContainer.style.display = 'none';
this.displayTestResults(results);
}
fetchSavedTests() {
this.comms.sendToApp('get_backtests', { user_name: this.ui.data.user_name });
}
updateHTML() {
let html = '';
for (const test of this.tests) {
html += `
<div class="backtest-item">
<button class="delete-button" onclick="this.ui.backtesting.deleteTest('${test.name}')">&#10008;</button>
<div class="backtest-name" onclick="this.ui.backtesting.runTest('${test.name}')">${test.name}</div>
</div>`;
}
document.getElementById(this.target_id).innerHTML = html;
}
runTest(testName) {
const testData = { name: testName, user_name: this.ui.data.user_name };
this.comms.sendToApp('run_backtest', testData);
}
deleteTest(testName) {
const testData = { name: testName, user_name: this.ui.data.user_name };
this.comms.sendToApp('delete_backtest', testData);
}
populateStrategyDropdown() {
const strategyDropdown = document.getElementById('strategy_select');
strategyDropdown.innerHTML = '';
const strategies = this.ui.strats.getAvailableStrategies();
console.log("Available strategies:", strategies);
strategies.forEach(strategy => {
const option = document.createElement('option');
option.value = strategy.name;
option.text = strategy.name;
strategyDropdown.appendChild(option);
});
if (strategies.length > 0) {
const firstStrategyName = strategies[0].name;
console.log("Setting default strategy to:", firstStrategyName);
strategyDropdown.value = firstStrategyName;
}
}
openForm(testName = null) {
const formElement = document.getElementById("backtest_form");
if (!formElement) {
console.error('Form element not found');
return;
}
this.populateStrategyDropdown();
if (testName) {
const testData = this.tests.find(test => test.name === testName);
if (testData) {
document.querySelector("#backtest_draggable_header h1").textContent = "Edit Backtest";
document.getElementById('strategy_select').value = testData.strategy;
document.getElementById('start_date').value = testData.start_date;
}
} else {
document.querySelector("#backtest_draggable_header h1").textContent = "Create New Backtest";
this.clearForm();
}
formElement.style.display = "grid";
}
closeForm() {
document.getElementById("backtest_form").style.display = "none";
}
clearForm() {
document.getElementById('strategy_select').value = '';
document.getElementById('start_date').value = '';
}
submitTest() {
const strategy = document.getElementById('strategy_select').value;
const start_date = document.getElementById('start_date').value;
const capital = parseFloat(document.getElementById('initial_capital').value) || 10000;
const commission = parseFloat(document.getElementById('commission').value) || 0.001;
if (!strategy) {
alert("Please select a strategy.");
return;
}
const now = new Date();
const startDate = new Date(start_date);
if (startDate > now) {
alert("Start date cannot be in the future.");
return;
}
const testData = {
strategy,
start_date,
capital,
commission,
user_name: this.ui.data.user_name
};
this.comms.sendToApp('submit_backtest', testData);
}
} }

View File

@ -317,20 +317,22 @@ height: 500px;
border-style:none; border-style:none;
} }
#indicator_output{ #indicator_output{
overflow-y: scroll; color: blueviolet;
width: 300px; position: absolute;
height:50px; height: fit-content;
padding: 3px; width: 150px;
border-style: solid; margin-top: 30px;
border-radius: 8px;
background-image: linear-gradient(93deg, #ffffffde, #fffefe29);
} }
#chart_controls{ #chart_controls{
border-style:none; border-style:none;
width: 775px; width: 600px;
padding: 15px; padding: 15px;
display: grid; display: grid;
grid-template-columns:350px 2fr 1fr 1fr; grid-template-columns: 4fr 2fr 2fr 2fr;
margin-left: 250;
} }
#indicators{ #indicators{
display: none; display: none;

View File

@ -27,12 +27,13 @@ class Charts {
// - Create the candle stick series for our chart // - Create the candle stick series for our chart
this.candleSeries = this.chart_1.addCandlestickSeries(); this.candleSeries = this.chart_1.addCandlestickSeries();
//Initialise the candlestick series // Initialize the candlestick series if price_history is available
this.price_history.then((ph) => { if (this.price_history && this.price_history.length > 0) {
//Initialise the candle data this.candleSeries.setData(this.price_history);
this.candleSeries.setData(ph); console.log('Candle series init:', this.price_history);
console.log('Candle series init:', ph) } else {
}) console.error('Price history is not available or is empty.');
}
this.bind_charts(this.chart_1); this.bind_charts(this.chart_1);
} }

View File

@ -2,14 +2,12 @@ class Comms {
constructor() { constructor() {
this.connectionOpen = false; this.connectionOpen = false;
this.appCon = null; // WebSocket connection for app communication this.appCon = null; // WebSocket connection for app communication
this.eventHandlers = {}; // Event handlers for message types
// Callback collections that will receive various updates. // Callback collections that will receive various updates.
this.candleUpdateCallbacks = []; this.candleUpdateCallbacks = [];
this.candleCloseCallbacks = []; this.candleCloseCallbacks = [];
this.indicatorUpdateCallbacks = []; this.indicatorUpdateCallbacks = [];
// Flags
this.connectionOpen = false;
} }
/** /**
@ -32,6 +30,30 @@ class Comms {
} }
} }
/**
* Register an event handler for a specific message type.
* @param {string} messageType - The type of the message to handle.
* @param {function} handler - The handler function to register.
*/
on(messageType, handler) {
if (!this.eventHandlers[messageType]) {
this.eventHandlers[messageType] = [];
}
this.eventHandlers[messageType].push(handler);
}
/**
* Emit an event to all registered handlers.
* @param {string} messageType - The type of the message.
* @param {Object} data - The data to pass to the handlers.
*/
emit(messageType, data) {
const handlers = this.eventHandlers[messageType];
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
/* Callback declarations */ /* Callback declarations */
candleUpdate(newCandle) { candleUpdate(newCandle) {
@ -40,7 +62,7 @@ class Comms {
} }
} }
candleClose(newCandle) { candleClose(newCandle) {
this.sendToApp('candle_data', newCandle); this.sendToApp('candle_data', newCandle);
for (const callback of this.candleCloseCallbacks) { for (const callback of this.candleCloseCallbacks) {
@ -149,7 +171,7 @@ class Comms {
} }
} }
/** /**
* Sends a request to update an indicator's properties. * Sends a request to update an indicator's properties.
* @param {Object} indicatorData - An object containing the updated properties of the indicator. * @param {Object} indicatorData - An object containing the updated properties of the indicator.
* @returns {Promise<Object>} - The response from the server. * @returns {Promise<Object>} - The response from the server.
@ -206,7 +228,6 @@ class Comms {
} }
} }
setAppCon() { setAppCon() {
this.appCon = new WebSocket('ws://localhost:5000/ws'); this.appCon = new WebSocket('ws://localhost:5000/ws');
@ -228,71 +249,8 @@ class Comms {
} }
if (message && message.reply !== undefined) { if (message && message.reply !== undefined) {
// Handle different reply types from the server // Emit the event to registered handlers
if (message.reply === 'updates') { this.emit(message.reply, message.data);
const { i_updates, s_updates, stg_updts, trade_updts } = message.data;
// Handle indicator updates
if (i_updates) {
this.indicatorUpdate(i_updates);
window.UI.signals.i_update(i_updates);
}
// Handle signal updates
if (s_updates) {
window.UI.signals.update_signal_states(s_updates);
window.UI.alerts.publish_alerts('signal_changes', s_updates);
}
// Handle strategy updates
if (stg_updts) {
window.UI.strats.update_received(stg_updts);
}
// Handle trade updates
if (trade_updts) {
window.UI.trade.update_received(trade_updts);
}
} else if (message.reply === 'signals') {
window.UI.signals.set_data(message.data);
} else if (message.reply === 'strategies') {
window.UI.strats.set_data(message.data);
} else if (message.reply === 'trades') {
window.UI.trade.set_data(message.data);
} else if (message.reply === 'signal_created') {
const list_of_one = [message.data];
window.UI.signals.set_data(list_of_one);
} else if (message.reply === 'trade_created') {
const list_of_one = [message.data];
window.UI.trade.set_data(list_of_one);
} else if (message.reply === 'Exchange_connection_result') {
window.UI.exchanges.postConnection(message.data);
} else if (message.reply === 'strategy_created') {
// Handle the strategy creation response
if (message.data.success) {
// Success - Notify the user and update the UI
alert(message.data.message); // Display a success message
console.log("New strategy data:", message.data); // Log or handle the new strategy data
// Optionally, refresh the list of strategies
window.UI.strats.fetchSavedStrategies();
} else {
// Failure - Notify the user of the error
alert(`Error: ${message.data.message}`);
console.error("Strategy creation error:", message.data.message);
}
} else {
console.log(message.reply);
console.log(message.data);
}
} }
} }
}); });
@ -309,8 +267,6 @@ class Comms {
}; };
} }
/** /**
* 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.

427
src/static/custom_blocks.js Normal file
View File

@ -0,0 +1,427 @@
// Define custom Blockly blocks and Python code generation
export function defineCustomBlocks() {
// Custom block for retrieving last candle values
Blockly.defineBlocksWithJsonArray([{
"type": "last_candle_value",
"message0": "Last candle %1 value (Src): %2",
"args0": [
{
"type": "field_dropdown",
"name": "CANDLE_PART",
"options": [
["Open", "open"],
["High", "high"],
["Low", "low"],
["Close", "close"]
]
},
{
"type": "input_value", // Accept an optional source block connection
"name": "SOURCE",
"check": "source" // The connected block must be a source block
}
],
"inputsInline": true, // Place the fields on the same line
"output": "Number",
"colour": 230,
"tooltip": "Get the value of the last candle from the specified or default source.",
"helpUrl": ""
}]);
// Comparison Block (for Candle Close > Candle Open, etc.)
Blockly.defineBlocksWithJsonArray([
{
"type": "comparison",
"message0": "%1 %2 %3",
"args0": [
{
"type": "input_value",
"name": "LEFT"
},
{
"type": "field_dropdown",
"name": "OPERATOR",
"options": [
[">", ">"],
["<", "<"],
["==", "=="]
]
},
{
"type": "input_value",
"name": "RIGHT"
}
],
"inputsInline": true,
"output": "Boolean",
"colour": 160,
"tooltip": "Compare two values.",
"helpUrl": ""
}
]);
Blockly.defineBlocksWithJsonArray([{
"type": "trade_action",
"message0": "if %1 then %2 units %3 with Stop Loss %4 and Take Profit %5 (Options) %6",
"args0": [
{"type": "input_value", "name": "CONDITION", "check": "Boolean"},
{"type": "field_dropdown", "name": "TRADE_TYPE", "options": [["Buy", "buy"], ["Sell", "sell"]]},
{"type": "input_value", "name": "SIZE", "check": "Number"},
{"type": "input_value", "name": "STOP_LOSS", "check": "Number"},
{"type": "input_value", "name": "TAKE_PROFIT", "check": "Number"},
{"type": "input_statement", "name": "TRADE_OPTIONS", "check": "trade_option"}
],
"previousStatement": null,
"nextStatement": null,
"colour": 230,
"tooltip": "Executes a trade with size, optional stop loss, take profit, and trade options",
"helpUrl": ""
}]);
// Stop Loss Block
Blockly.defineBlocksWithJsonArray([{
"type": "stop_loss",
"message0": "Stop Loss %1",
"args0": [
{
"type": "input_value",
"name": "STOP_LOSS",
"check": "Number"
}
],
"output": "Number",
"colour": 230,
"tooltip": "Sets a stop loss value",
"helpUrl": ""
}]);
// Take Profit Block
Blockly.defineBlocksWithJsonArray([{
"type": "take_profit",
"message0": "Take Profit %1",
"args0": [
{
"type": "input_value",
"name": "TAKE_PROFIT",
"check": "Number"
}
],
"output": "Number",
"colour": 230,
"tooltip": "Sets a take profit value",
"helpUrl": ""
}]);
// Logical AND Block
Blockly.defineBlocksWithJsonArray([{
"type": "logical_and",
"message0": "%1 AND %2",
"args0": [
{
"type": "input_value",
"name": "LEFT",
"check": "Boolean"
},
{
"type": "input_value",
"name": "RIGHT",
"check": "Boolean"
}
],
"inputsInline": true,
"output": "Boolean",
"colour": 210,
"tooltip": "Logical AND of two conditions",
"helpUrl": ""
}]);
// Logical OR Block
Blockly.defineBlocksWithJsonArray([{
"type": "logical_or",
"message0": "%1 OR %2",
"args0": [
{
"type": "input_value",
"name": "LEFT",
"check": "Boolean"
},
{
"type": "input_value",
"name": "RIGHT",
"check": "Boolean"
}
],
"inputsInline": true,
"output": "Boolean",
"colour": 210,
"tooltip": "Logical OR of two conditions",
"helpUrl": ""
}]);
// Block to check if a condition is false
Blockly.defineBlocksWithJsonArray([{
"type": "is_false",
"message0": "%1 is false",
"args0": [
{
"type": "input_value",
"name": "CONDITION",
"check": "Boolean"
}
],
"output": "Boolean",
"colour": 160,
"tooltip": "Checks if the condition is false",
"helpUrl": ""
}]);
// Order Type Block with Limit Price
Blockly.defineBlocksWithJsonArray([{
"type": "order_type",
"message0": "Order Type %1 %2",
"args0": [
{
"type": "field_dropdown",
"name": "ORDER_TYPE",
"options": [
["Market", "market"],
["Limit", "limit"]
]
},
{
"type": "input_value",
"name": "LIMIT_PRICE", // Input for limit price when Limit order is selected
"check": "Number"
}
],
"previousStatement": "trade_option",
"nextStatement": "trade_option",
"colour": 230,
"tooltip": "Select order type (Market or Limit) with optional limit price",
"helpUrl": ""
}]);
Blockly.defineBlocksWithJsonArray([{
"type": "value_input",
"message0": "Value %1",
"args0": [
{
"type": "field_number",
"name": "VALUE",
"value": 0,
"min": 0
}
],
"output": "Number",
"colour": 230,
"tooltip": "Enter a numerical value",
"helpUrl": ""
}]);
// Time In Force (TIF) Block
Blockly.defineBlocksWithJsonArray([{
"type": "time_in_force",
"message0": "Time in Force %1",
"args0": [
{
"type": "field_dropdown",
"name": "TIF",
"options": [
["GTC (Good Till Canceled)", "gtc"],
["FOK (Fill or Kill)", "fok"],
["IOC (Immediate or Cancel)", "ioc"]
]
}
],
"previousStatement": "trade_option",
"nextStatement": "trade_option",
"colour": 230,
"tooltip": "Select time in force for the order",
"helpUrl": ""
}]);
// Dynamically populate the block options using the available data
Blockly.defineBlocksWithJsonArray([{
"type": "source",
"message0": "src: TF %1 Ex %2 Sym %3",
"args0": [
{
"type": "field_dropdown",
"name": "TF",
"options": function() {
// Dynamically fetch available timeframes from bt_data.intervals
return bt_data.intervals.map(interval => [interval, interval]);
}
},
{
"type": "field_dropdown",
"name": "EXC",
"options": function() {
// Dynamically fetch available exchanges from window.UI.exchanges.connected_exchanges
return window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]);
}
},
{
"type": "field_dropdown",
"name": "SYM",
"options": function() {
// Dynamically fetch available symbols from bt_data.symbols
return bt_data.symbols.map(symbol => [symbol, symbol]);
}
}
],
"output": "source", // This output allows it to be connected to other blocks expecting a 'source'
"colour": 230,
"tooltip": "Choose the data feed source for the trade or value.",
"helpUrl": ""
}]);
Blockly.defineBlocksWithJsonArray([{
"type": "target_market",
"message0": "Target market: TF %1 Ex %2 Sym %3",
"args0": [
{
"type": "field_dropdown",
"name": "TF",
"options": function() {
return bt_data.intervals.map(interval => [interval, interval]);
}
},
{
"type": "field_dropdown",
"name": "EXC",
"options": function() {
return window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]);
}
},
{
"type": "field_dropdown",
"name": "SYM",
"options": function() {
return bt_data.symbols.map(symbol => [symbol, symbol]);
}
}
],
"previousStatement": "trade_option", // Allow it to be used as a trade option
"nextStatement": "trade_option", // Chain it with other trade options
"colour": 230,
"tooltip": "Choose the target market for executing trades.",
"helpUrl": ""
}]);
Blockly.defineBlocksWithJsonArray([{
"type": "strategy_profit_loss",
"message0": "Strategy is %1",
"args0": [
{
"type": "field_dropdown",
"name": "DIRECTION",
"options": [
["up", "up"],
["down", "down"]
]
}
],
"output": "Boolean",
"colour": 230,
"tooltip": "Check if the strategy is up or down.",
"helpUrl": ""
}]);
Blockly.defineBlocksWithJsonArray([{
"type": "current_balance",
"message0": "Current Balance",
"output": "Number",
"colour": 230,
"tooltip": "Retrieve the current balance of the strategy.",
"helpUrl": ""
}]);
Blockly.defineBlocksWithJsonArray([{
"type": "starting_balance",
"message0": "Starting Balance",
"output": "Number",
"colour": 230,
"tooltip": "Retrieve the starting balance of the strategy.",
"helpUrl": ""
}]);
Blockly.defineBlocksWithJsonArray([{
"type": "arithmetic_operator",
"message0": "%1 %2 %3",
"args0": [
{
"type": "input_value",
"name": "LEFT",
"check": "Number"
},
{
"type": "field_dropdown",
"name": "OPERATOR",
"options": [
["+", "ADD"],
["-", "SUBTRACT"],
["*", "MULTIPLY"],
["/", "DIVIDE"]
]
},
{
"type": "input_value",
"name": "RIGHT",
"check": "Number"
}
],
"inputsInline": true,
"output": "Number",
"colour": 160,
"tooltip": "Perform basic arithmetic operations.",
"helpUrl": ""
}]);
// Block for tracking the number of trades currently in play
Blockly.defineBlocksWithJsonArray([{
"type": "active_trades",
"message0": "Number of active trades",
"output": "Number",
"colour": 230,
"tooltip": "Get the number of active trades currently open.",
"helpUrl": ""
}]);
// Block for checking if a flag is set
Blockly.defineBlocksWithJsonArray([{
"type": "flag_is_set",
"message0": "flag %1 is set",
"args0": [
{
"type": "field_input",
"name": "FLAG_NAME",
"text": "flag_name"
}
],
"output": "Boolean",
"colour": 160,
"tooltip": "Check if the specified flag is set to True.",
"helpUrl": ""
}]);
// Block for setting a flag based on a condition
Blockly.defineBlocksWithJsonArray([{
"type": "set_flag",
"message0": "If %1 then set flag %2 to %3",
"args0": [
{
"type": "input_value", // This will accept a Boolean condition (comparison or logical)
"name": "CONDITION",
"check": "Boolean"
},
{
"type": "field_input",
"name": "FLAG_NAME",
"text": "flag_name"
},
{
"type": "field_dropdown",
"name": "FLAG_VALUE",
"options": [["True", "True"], ["False", "False"]]
}
],
"previousStatement": null,
"nextStatement": null,
"colour": 230,
"tooltip": "Set a flag to True or False if the condition is met.",
"helpUrl": ""
}]);
console.log('Custom blocks defined');
}

View File

@ -19,38 +19,49 @@ class Data {
/* Comms handles communication with the servers. Register /* Comms handles communication with the servers. Register
callbacks to handle various incoming messages.*/ callbacks to handle various incoming messages.*/
this.comms = new Comms(); this.comms = new Comms();
this.comms.registerCallback('candle_update', this.candle_update)
this.comms.registerCallback('candle_close', this.candle_close)
this.comms.registerCallback('indicator_update', this.indicator_update)
// Open the connection to our local server.
this.comms.setAppCon();
/* Open connection for streaming candle data wth the exchange.
Pass it the time period of candles to stream. */
this.comms.setExchangeCon(this.interval, this.trading_pair);
//Request historical price data from the server. // Initialize other properties
this.price_history = this.comms.getPriceHistory(this.user_name); this.price_history = null;
this.indicator_data = null;
// Last price from price history. this.last_price = null;
this.price_history.then((value) => {
if (value && value.length > 0) {
this.last_price = value[value.length - 1].close;
} else {
console.error('Received empty price history data');
this.last_price = null;
}
}).catch((error) => {
console.error('Error processing price history:', error);
this.last_price = null;
});
// Request from the server initialization data for the indicators.
this.indicator_data = this.comms.getIndicatorData(this.user_name);
// Call back for indicator updates.
this.i_updates = null; this.i_updates = null;
} }
/**
* Initializes the Data instance by setting up connections and fetching data.
* Should be called after creating a new instance of Data.
*/
async initialize() {
// Register callbacks
this.comms.registerCallback('candle_update', this.candle_update.bind(this));
this.comms.registerCallback('candle_close', this.candle_close.bind(this));
this.comms.registerCallback('indicator_update', this.indicator_update.bind(this));
// Open the connection to your local server
this.comms.setAppCon();
// Open connection for streaming candle data with the exchange
this.comms.setExchangeCon(this.interval, this.trading_pair);
// Request historical price data from the server
try {
this.price_history = await this.comms.getPriceHistory(this.user_name);
if (this.price_history && this.price_history.length > 0) {
this.last_price = this.price_history[this.price_history.length - 1].close;
} else {
console.error('Received empty price history data');
}
} catch (error) {
console.error('Error fetching price history:', error);
}
// Request indicator data from the server
try {
this.indicator_data = await this.comms.getIndicatorData(this.user_name);
} catch (error) {
console.error('Error fetching indicator data:', error);
}
}
candle_update(new_candle){ candle_update(new_candle){
// This is called everytime a candle update comes from the local server. // This is called everytime a candle update comes from the local server.
window.UI.charts.update_main_chart(new_candle); window.UI.charts.update_main_chart(new_candle);
@ -60,11 +71,17 @@ class Data {
registerCallback_i_updates(call_back){ registerCallback_i_updates(call_back){
this.i_updates = call_back; this.i_updates = call_back;
} }
indicator_update(data){
// This is called everytime an indicator update come in. // This is called everytime an indicator update come in.
window.UI.data.i_updates(data); indicator_update(data) {
if (typeof this.i_updates === 'function') {
this.i_updates(data);
} else {
console.warn('No indicator update callback registered.');
}
} }
candle_close(new_candle){ candle_close(new_candle){
// This is called everytime a candle closes. // This is called everytime a candle closes.
//console.log('Candle close:'); //console.log('Candle close:');

View File

@ -1,15 +1,16 @@
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'); this.strats = new Strategies();
this.exchanges = new Exchanges(); this.exchanges = new Exchanges();
this.data = new Data(); this.data = new Data();
this.controls = new Controls(); this.controls = new Controls();
this.signals = new Signals(this.data.indicators);
this.alerts = new Alerts("alert_list"); this.alerts = new Alerts("alert_list");
this.trade = new Trade(); this.trade = new Trade();
this.users = new Users(); this.users = new Users();
this.indicators = new Indicators(this.data.comms); this.indicators = new Indicators(this.data.comms);
this.signals = new Signals(this);
this.backtesting = new Backtesting(this);
// 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,10 +19,18 @@ class User_Interface {
this.initializeAll(); this.initializeAll();
} }
initializeAll() { async initializeAll() {
window.addEventListener('load', () => { window.addEventListener('load', async () => {
this.initializeChartsAndIndicators(); try {
this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats)); await this.data.initialize(); // Wait for initialization
this.initializeChartsAndIndicators();
// Initialize other UI components here
this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats), "draggable_header", "resize-strategy");
this.initializeResizablePopup("backtest_form", null, "backtest_draggable_header", "resize-backtest");
} catch (error) {
console.error('Initialization failed:', error);
}
}); });
} }
@ -46,26 +55,30 @@ class User_Interface {
this.controls.init_TP_selector(); this.controls.init_TP_selector();
this.trade.initialize(); this.trade.initialize();
this.exchanges.initialize(); this.exchanges.initialize();
this.strats.initialize(); this.strats.initialize('strats_display', 'new_strat_form', this.data);
this.backtesting.fetchSavedTests();
} }
/** /**
* Make a popup resizable, and optionally pass a resize callback (like for Blockly workspaces). * 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 {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). * @param {function|null} resizeCallback - Optional callback to run when resizing (for Blockly or other elements).
* @param {string|null} headerId - The ID of the header to use for dragging, or null to drag the entire element.
* @param {string} resizerId - The ID of the resizer handle element.
*/ */
initializeResizablePopup(popupId, resizeCallback = null) { initializeResizablePopup(popupId, resizeCallback = null, headerId = null, resizerId) {
const popupElement = document.getElementById(popupId); const popupElement = document.getElementById(popupId);
this.dragElement(popupElement); this.dragElement(popupElement, headerId);
this.makeResizable(popupElement, resizeCallback); this.makeResizable(popupElement, resizeCallback, resizerId);
} }
/** /**
* Make an element draggable by dragging its header. * Make an element draggable by dragging its header.
* @param {HTMLElement} elm - The element to make draggable. * @param {HTMLElement} elm - The element to make draggable.
* @param {string|null} headerId - The ID of the header to use for dragging, or null to drag the entire element.
*/ */
dragElement(elm) { dragElement(elm, headerId = null) {
const header = document.getElementById("draggable_header"); const header = headerId ? document.getElementById(headerId) : elm;
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (header) { if (header) {
@ -102,9 +115,10 @@ class User_Interface {
* Make an element resizable and optionally call a resize callback (like for Blockly workspace). * Make an element resizable and optionally call a resize callback (like for Blockly workspace).
* @param {HTMLElement} elm - The element to make resizable. * @param {HTMLElement} elm - The element to make resizable.
* @param {function|null} resizeCallback - Optional callback to resize specific content. * @param {function|null} resizeCallback - Optional callback to resize specific content.
* @param {string} resizerId - The ID of the resizer handle element.
*/ */
makeResizable(elm, resizeCallback = null) { makeResizable(elm, resizeCallback = null, resizerId) {
const resizer = document.getElementById("resize-br"); const resizer = document.getElementById(resizerId);
let originalWidth = 0; let originalWidth = 0;
let originalHeight = 0; let originalHeight = 0;
let originalMouseX = 0; let originalMouseX = 0;

View File

@ -0,0 +1,38 @@
// Define Blockly blocks dynamically based on indicators
export function defineIndicatorBlocks() {
const indicatorOutputs = window.UI.indicators.getIndicatorOutputs();
const toolboxCategory = document.querySelector('#toolbox category[name="Indicators"]');
for (let indicatorName in indicatorOutputs) {
const outputs = indicatorOutputs[indicatorName];
// Define the block for this indicator
Blockly.defineBlocksWithJsonArray([{
"type": indicatorName,
"message0": `${indicatorName} %1`,
"args0": [
{
"type": "field_dropdown",
"name": "OUTPUT",
"options": outputs.map(output => [output, output])
}
],
"output": "Number",
"colour": 230,
"tooltip": `Select the ${indicatorName} output`,
"helpUrl": ""
}]);
// Define how this block will generate Python code
Blockly.Python[indicatorName] = Blockly.Python.forBlock[indicatorName] = function(block) {
const selectedOutput = block.getFieldValue('OUTPUT');
const code = `get_${indicatorName.toLowerCase()}_value('${selectedOutput}')`;
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Append dynamically created blocks to the Indicators category in the toolbox
const blockElement = document.createElement('block');
blockElement.setAttribute('type', indicatorName);
toolboxCategory.appendChild(blockElement);
}
}

View File

@ -1,38 +1,55 @@
class Indicator_Output { class Indicator_Output {
constructor(name) { constructor(name) {
this.legend={}; this.legend = {};
} }
create_legend(name, chart, lineSeries){ create_legend(name, chart, lineSeries) {
// Create legend div and append it to the output element // Create legend div and append it to the output element
let target_div = document.getElementById('indicator_output'); let target_div = document.getElementById('indicator_output');
this.legend[name] = document.createElement('div'); this.legend[name] = document.createElement('div');
this.legend[name].className = 'legend'; this.legend[name].className = 'legend';
this.legend[name].style.opacity = 0.1; // Initially mostly transparent
this.legend[name].style.transition = 'opacity 1s ease-out'; // Smooth transition for fade-out
target_div.appendChild(this.legend[name]); target_div.appendChild(this.legend[name]);
this.legend[name].style.display = 'block'; this.legend[name].style.display = 'block';
this.legend[name].style.left = 3 + 'px'; this.legend[name].style.left = 3 + 'px';
this.legend[name].style.top = 3 + 'px'; this.legend[name].style.top = 3 + 'px';
// subscribe set legend text to crosshair moves // subscribe set legend text to crosshair moves
chart.subscribeCrosshairMove((param) => { chart.subscribeCrosshairMove((param) => {
this.set_legend_text(param.seriesPrices.get(lineSeries),name); this.set_legend_text(param.seriesPrices.get(lineSeries), name);
}); });
} }
set_legend_text(priceValue, name) {
set_legend_text(priceValue,name) {
// Callback assigned to fire on crosshair movements. // Callback assigned to fire on crosshair movements.
let val = 'n/a'; let val = 'n/a';
if (priceValue !== undefined) { if (priceValue !== undefined) {
val = (Math.round(priceValue * 100) / 100).toFixed(2); val = (Math.round(priceValue * 100) / 100).toFixed(2);
} }
this.legend[name].innerHTML = name + ' <span style="color:rgba(4, 111, 232, 1)">' + val + '</span>';
// Update legend text
this.legend[name].innerHTML = `${name} <span style="color:rgba(4, 111, 232, 1)">${val}</span>`;
// Make legend fully visible
this.legend[name].style.opacity = 1;
this.legend[name].style.display = 'block';
// Set a timeout to fade out the legend after 3 seconds
clearTimeout(this.legend[name].fadeTimeout); // Clear any previous timeout to prevent conflicts
this.legend[name].fadeTimeout = setTimeout(() => {
this.legend[name].style.opacity = 0.1; // Gradually fade out
// Set another timeout to hide the element after the fade-out transition
setTimeout(() => {
this.legend[name].style.display = 'none';
}, 1000); // Wait for the fade-out transition to complete (1s)
}, 1000);
} }
clear_legend(name) { clear_legend(name) {
// Remove the legend div from the DOM // Remove the legend div from the DOM
if (this.legend[name]) { for (const key in this.legend) {
this.legend[name].remove(); // Remove the legend from the DOM if (key.startsWith(name)) {
delete this.legend[name]; // Remove the reference from the object this.legend[key].remove(); // Remove the legend from the DOM
} else { delete this.legend[key]; // Remove the reference from the object
console.warn(`Legend for ${name} not found.`); }
} }
} }
} }
@ -84,27 +101,71 @@ class Indicator {
color: color, color: color,
lineWidth: lineWidth lineWidth: lineWidth
}); });
// Initialise the crosshair legend for the charts. // Initialise the crosshair legend for the charts with a unique name for each line.
iOutput.create_legend(this.name, chart, this.lines[name]); iOutput.create_legend(`${this.name}_${name}`, chart, this.lines[name]);
} }
setLine(name, data, value_name) { setLine(lineName, data, value_name) {
console.log('indicators[68]: setLine takes:(name,data,value_name)'); console.log('indicators[68]: setLine takes:(lineName, data, value_name)');
console.log(name, data, value_name); console.log(lineName, data, value_name);
// Initialize the data with the data object provided.
this.lines[name].setData(data); let priceValue;
// Isolate the last value provided and round to 2 decimals places.
let priceValue = data.at(-1).value; // Check if the data is a multi-value object
this.updateDisplay(name, priceValue, value_name); if (typeof data === 'object' && data !== null && value_name in data) {
// Update indicator output/crosshair legend. // Multi-value indicator: Extract the array for the specific key
iOutput.set_legend_text(data.at(-1).value, this.name); const processedData = data[value_name];
// Set the data for the line
this.lines[lineName].setData(processedData);
// Isolate the last value provided and round to 2 decimal places
priceValue = processedData.at(-1).value;
// Update the display and legend for multi-value indicators
this.updateDisplay(lineName, { [value_name]: priceValue }, 'value');
} else {
// Single-value indicator: Initialize the data directly
this.lines[lineName].setData(data);
// Isolate the last value provided and round to 2 decimal places
priceValue = data.at(-1).value;
// Update the display and legend for single-value indicators
this.updateDisplay(lineName, priceValue, value_name);
}
iOutput.set_legend_text(priceValue, `${this.name}_${lineName}`);
} }
updateDisplay(name, priceValue, value_name) { updateDisplay(name, priceValue, value_name) {
// Update the data in the edit and view indicators panel let element = document.getElementById(this.name + '_' + value_name);
let element = document.getElementById(this.name + '_' + value_name) if (element) {
if (element){ if (typeof priceValue === 'object' && priceValue !== null) {
element.value = (Math.round(priceValue * 100) / 100).toFixed(2); // Handle multiple values by joining them into a single string with labels
let currentValues = element.value ? element.value.split(', ').reduce((acc, pair) => {
let [key, val] = pair.split(': ');
if (!isNaN(parseFloat(val))) {
acc[key] = parseFloat(val);
}
return acc;
}, {}) : {};
// Update current values with the new key-value pairs
Object.assign(currentValues, priceValue);
// Set the updated values back to the element
element.value = Object.entries(currentValues)
.filter(([key, value]) => !isNaN(value)) // Skip NaN values
.map(([key, value]) => `${key}: ${(Math.round(value * 100) / 100).toFixed(2)}`)
.join(', '); // Use comma for formatting
} else {
// Handle simple values as before
element.value = (Math.round(priceValue * 100) / 100).toFixed(2);
}
// Adjust the element styling dynamically for wrapping and height
element.style.height = 'auto'; // Reset height
element.style.height = (element.scrollHeight) + 'px'; // Adjust height based on content
} else { } else {
console.warn(`Element with ID ${this.name}_${value_name} not found.`); console.warn(`Element with ID ${this.name}_${value_name} not found.`);
} }
@ -115,12 +176,34 @@ class Indicator {
} }
updateLine(name, data, value_name) { updateLine(name, data, value_name) {
// Update the line-set data in the chart console.log('indicators[68]: updateLine takes:(name, data, value_name)');
this.lines[name].update(data); console.log(name, data, value_name);
// Update indicator output/crosshair legend.
iOutput.set_legend_text(data.value, this.name); // Check if the data is a multi-value object
// Update the data in the edit and view indicators panel if (typeof data === 'object' && data !== null && value_name in data) {
this.updateDisplay(name, data.value, value_name); // Multi-value indicator: Extract the array for the specific key
const processedData = data[value_name];
// Update the line-set data in the chart
this.lines[name].update(processedData);
// Isolate the last value provided and round to 2 decimal places
const priceValue = processedData.at(-1).value;
// Update the display and legend for multi-value indicators
this.updateDisplay(name, { [value_name]: priceValue }, 'value');
iOutput.set_legend_text(priceValue, `${this.name}_${name}`);
} else {
// Single-value indicator: Initialize the data directly
this.lines[name].update(data);
// Isolate the last value provided and round to 2 decimal places
const priceValue = data.at(-1).value;
// Update the display and legend for single-value indicators
this.updateDisplay(name, priceValue, value_name);
iOutput.set_legend_text(priceValue, `${this.name}_${name}`);
}
} }
updateHist(name, data) { updateHist(name, data) {
@ -249,19 +332,25 @@ class MACD extends Indicator {
const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null); const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null);
if (filteredData.length > 0) { if (filteredData.length > 0) {
// Set the 'line_m' for the MACD line // Prepare the filtered data for the MACD line
this.setLine('line_m', filteredData.map(row => ({ const line_m = filteredData.map(row => ({
time: row.time, time: row.time,
value: row.macd value: row.macd
})), 'macd'); }));
// Set the 'line_s' for the signal line // Set the 'line_m' for the MACD line
this.setLine('line_s', filteredData.map(row => ({ this.setLine('line_m', { macd: line_m }, 'macd');
// Prepare the filtered data for the signal line
const line_s = filteredData.map(row => ({
time: row.time, time: row.time,
value: row.signal value: row.signal
})), 'signal'); }));
// Set the histogram // Set the 'line_s' for the signal line
this.setLine('line_s', { signal: line_s }, 'signal');
// Set the histogram data
this.setHist(this.name, filteredData.map(row => ({ this.setHist(this.name, filteredData.map(row => ({
time: row.time, time: row.time,
value: row.hist value: row.hist
@ -271,15 +360,35 @@ class MACD extends Indicator {
} }
} }
update(data) { update(data) {
// Update the 'macd' line // Filter out rows where macd, signal, or hist are null
this.updateLine('line_m', {time: data[0].time, value: data[0].macd }, 'macd'); const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null);
// Update the 'signal' line if (filteredData.length > 0) {
this.updateLine('line_s', { time: data[0].time, value: data[0].signal }, 'signal'); // Update the 'macd' line
const line_m = filteredData.map(row => ({
time: row.time,
value: row.macd
}));
this.updateLine('line_m', { macd: line_m }, 'macd');
// Update the 'hist' (histogram) bar // Update the 'signal' line
this.updateHist('hist', {time: data[0].time, value: data[0].hist }); const line_s = filteredData.map(row => ({
time: row.time,
value: row.signal
}));
this.updateLine('line_s', { signal: line_s }, 'signal');
// Update the 'hist' (histogram) bar
const hist_data = filteredData.map(row => ({
time: row.time,
value: row.hist
}));
this.updateHist('hist', hist_data);
} else {
console.error('No valid MACD data found for update.');
}
} }
} }
indicatorMap.set("MACD", MACD); indicatorMap.set("MACD", MACD);
@ -341,30 +450,64 @@ class Bolenger extends Indicator {
}; };
} }
init(data) { init(data) {
// Set the 'line_u' for the upper line // Filter out rows where upper, middle, or lower are null
this.setLine('line_u', data.map(row => ({ const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null);
time: row.time,
value: row.upper
})), 'value');
// Set the 'line_m' for the middle line if (filteredData.length > 0) {
this.setLine('line_m', data.map(row => ({ // Set the 'line_u' for the upper line
time: row.time, const line_u = filteredData.map(row => ({
value: row.middle time: row.time,
})), 'value2'); value: row.upper
}));
this.setLine('line_u', { upper: line_u }, 'upper');
// Set the 'line_l' for the lower line // Set the 'line_m' for the middle line
this.setLine('line_l', data.map(row => ({ const line_m = filteredData.map(row => ({
time: row.time, time: row.time,
value: row.lower value: row.middle
})), 'value3'); }));
} this.setLine('line_m', { middle: line_m }, 'middle');
// Set the 'line_l' for the lower line
const line_l = filteredData.map(row => ({
time: row.time,
value: row.lower
}));
this.setLine('line_l', { lower: line_l }, 'lower');
} else {
console.error('No valid data found for init.');
}
}
update(data) { update(data) {
this.updateLine('line_u', data[0][0], 'value'); // Filter out rows where upper, middle, or lower are null
this.updateLine('line_m', data[1][0], 'value2'); const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null);
this.updateLine('line_l', data[2][0], 'value3');
if (filteredData.length > 0) {
// Update the 'upper' line
const line_u = filteredData.map(row => ({
time: row.time,
value: row.upper
}));
this.updateLine('line_u', { upper: line_u }, 'upper');
// Update the 'middle' line
const line_m = filteredData.map(row => ({
time: row.time,
value: row.middle
}));
this.updateLine('line_m', { middle: line_m }, 'middle');
// Update the 'lower' line
const line_l = filteredData.map(row => ({
time: row.time,
value: row.lower
}));
this.updateLine('line_l', { lower: line_l }, 'lower');
} else {
console.error('No valid data found for update.');
}
} }
} }
indicatorMap.set("BOLBands", Bolenger); indicatorMap.set("BOLBands", Bolenger);
@ -422,8 +565,12 @@ class Indicators {
objects, then inserts the data into the charts. objects, then inserts the data into the charts.
*/ */
this.create_indicators(idata.indicators, charts); this.create_indicators(idata.indicators, charts);
// Initialize each indicators with the data. // Initialize each indicator with the data directly
idata.indicator_data.then( (data) => { this.init_indicators(data); } ); if (idata.indicator_data) {
this.init_indicators(idata.indicator_data);
} else {
console.error('Indicator data is not available.');
}
} }
init_indicators(data){ init_indicators(data){

View File

@ -0,0 +1,114 @@
// Define JSON generators for custom blocks
export function defineJsonGenerators() {
// Initialize JSON generator
if (!Blockly.JSON) {
Blockly.JSON = new Blockly.Generator('JSON');
}
// JSON Generator for 'trade_action' block
Blockly.JSON['trade_action'] = function(block) {
const condition = Blockly.JSON.valueToCode(block, 'CONDITION', Blockly.JSON.ORDER_ATOMIC);
const tradeType = block.getFieldValue('TRADE_TYPE');
const size = Blockly.JSON.valueToCode(block, 'SIZE', Blockly.JSON.ORDER_ATOMIC) || null;
const stopLoss = Blockly.JSON.valueToCode(block, 'STOP_LOSS', Blockly.JSON.ORDER_ATOMIC) || null;
const takeProfit = Blockly.JSON.valueToCode(block, 'TAKE_PROFIT', Blockly.JSON.ORDER_ATOMIC) || null;
const tradeOptions = Blockly.JSON.statementToCode(block, 'TRADE_OPTIONS').trim();
const json = {
type: 'trade_action',
condition: condition,
trade_type: tradeType,
size: size,
stop_loss: stopLoss,
take_profit: takeProfit,
trade_options: tradeOptions ? JSON.parse(tradeOptions) : []
};
return JSON.stringify(json);
};
// JSON generator for 'order_type' block
Blockly.JSON['order_type'] = function(block) {
const orderType = block.getFieldValue('ORDER_TYPE');
const limitPrice = Blockly.JSON.valueToCode(block, 'LIMIT_PRICE', Blockly.JSON.ORDER_ATOMIC) || null;
const json = {
order_type: orderType,
limit_price: limitPrice
};
return JSON.stringify(json);
};
// JSON generator for 'time_in_force' block
Blockly.JSON['time_in_force'] = function(block) {
const tif = block.getFieldValue('TIF');
const json = { tif: tif };
return JSON.stringify(json);
};
// JSON generator for 'comparison' block
Blockly.JSON['comparison'] = Blockly.JSON.forBlock['comparison'] = function(block) {
const left = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC);
const right = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC);
const operator = block.getFieldValue('OPERATOR');
const json = {
type: 'comparison',
operator: operator,
left: left,
right: right
};
return JSON.stringify(json);
};
// JSON generator for 'logical_and' block
Blockly.JSON['logical_and'] = Blockly.JSON.forBlock['logical_and'] = function(block) {
const left = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC);
const right = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC);
const json = {
type: 'logical_and',
left: left,
right: right
};
return JSON.stringify(json);
};
// JSON generator for 'logical_or' block
Blockly.JSON['logical_or'] = Blockly.JSON.forBlock['logical_or'] = function(block) {
const left = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC);
const right = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC);
const json = {
type: 'logical_or',
left: left,
right: right
};
return JSON.stringify(json);
};
// JSON generator for 'last_candle_value' block
Blockly.JSON['last_candle_value'] = Blockly.JSON.forBlock['last_candle_value'] = function(block) {
const candlePart = block.getFieldValue('CANDLE_PART');
const source = Blockly.JSON.valueToCode(block, 'SOURCE', Blockly.JSON.ORDER_ATOMIC) || null;
const json = {
type: 'last_candle_value',
candle_part: candlePart,
source: source
};
return JSON.stringify(json);
};
// JSON generator for 'source' block
Blockly.JSON['source'] = Blockly.JSON.forBlock['source'] = function(block) {
const timeframe = block.getFieldValue('TF');
const exchange = block.getFieldValue('EXC');
const symbol = block.getFieldValue('SYM');
const json = {
type: 'source',
timeframe: timeframe,
exchange: exchange,
symbol: symbol
};
return JSON.stringify(json);
};
console.log('JSON generators defined with forBlock assignments');
};

View File

@ -0,0 +1,178 @@
// Define Python generators for custom blocks
export function definePythonGenerators() {
// Python Generator for target_market
Blockly.Python['target_market'] = Blockly.Python.forBlock['target_market'] = function(block) {
var timeframe = block.getFieldValue('TF');
var exchange = block.getFieldValue('EXC');
var symbol = block.getFieldValue('SYM');
var code = `target_market(timeframe='${timeframe}', exchange='${exchange}', symbol='${symbol}')`;
return code;
};
// Generator for last_candle_value
Blockly.Python['last_candle_value'] = Blockly.Python.forBlock['last_candle_value'] = function(block) {
var candlePart = block.getFieldValue('CANDLE_PART');
var source = Blockly.Python.valueToCode(block, 'SOURCE', Blockly.Python.ORDER_ATOMIC) || 'None'; // Handle optional source
var code;
if (source !== 'None') {
// Use the provided source feed if available
code = `get_last_candle_value('${candlePart}', source=${source})`;
} else {
// Fallback to default source
code = `get_last_candle_value('${candlePart}')`;
}
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Comparison block to Python code
Blockly.Python['comparison'] = Blockly.Python.forBlock['comparison'] = function(block) {
const left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC);
const right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC);
const operator = block.getFieldValue('OPERATOR');
return [left + ' ' + operator + ' ' + right, Blockly.Python.ORDER_ATOMIC];
};
// Logical OR block to Python code
Blockly.Python['logical_or'] = Blockly.Python.forBlock['logical_or'] = function(block) {
var left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC);
var right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC);
var code = `${left} or ${right}`;
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Logical AND block to Python code
Blockly.Python['logical_and'] = Blockly.Python.forBlock['logical_and'] = function(block) {
var left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC);
var right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC);
var code = `${left} and ${right}`;
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Stop Loss block to Python code
Blockly.Python['stop_loss'] = Blockly.Python.forBlock['stop_loss'] = function(block) {
var stopLoss = Blockly.Python.valueToCode(block, 'STOP_LOSS', Blockly.Python.ORDER_ATOMIC);
return [stopLoss, Blockly.Python.ORDER_ATOMIC];
};
// Take Profit block to Python code
Blockly.Python['take_profit'] = Blockly.Python.forBlock['take_profit'] = function(block) {
var takeProfit = Blockly.Python.valueToCode(block, 'TAKE_PROFIT', Blockly.Python.ORDER_ATOMIC);
return [takeProfit, Blockly.Python.ORDER_ATOMIC];
};
// Python generator for is_false block
Blockly.Python['is_false'] =Blockly.Python.forBlock['is_false']= function(block) {
var condition = Blockly.Python.valueToCode(block, 'CONDITION', Blockly.Python.ORDER_ATOMIC);
var code = `not ${condition}`;
return [code, Blockly.Python.ORDER_ATOMIC];
};
Blockly.Python['trade_action'] = Blockly.Python.forBlock['trade_action'] = function(block) {
const condition = Blockly.Python.valueToCode(block, 'CONDITION', Blockly.Python.ORDER_ATOMIC);
const tradeType = block.getFieldValue('TRADE_TYPE');
const size = Blockly.Python.valueToCode(block, 'SIZE', Blockly.Python.ORDER_ATOMIC) || 'None';
const stopLoss = Blockly.Python.valueToCode(block, 'STOP_LOSS', Blockly.Python.ORDER_ATOMIC) || 'None';
const takeProfit = Blockly.Python.valueToCode(block, 'TAKE_PROFIT', Blockly.Python.ORDER_ATOMIC) || 'None';
// Process trade options
let tradeOptionsCode = Blockly.Python.statementToCode(block, 'TRADE_OPTIONS').trim();
tradeOptionsCode = tradeOptionsCode.split('\n').filter(line => line.trim() !== '');
// Collect all arguments into an array
const argsList = [`size=${size}`, `stop_loss=${stopLoss}`, `take_profit=${takeProfit}`];
if (tradeOptionsCode.length > 0) {
argsList.push(...tradeOptionsCode);
}
const args = argsList.join(', ');
const code = `if ${condition}:\n self.${tradeType}(${args})\n`;
return code;
};
// Order Type block to Python code
Blockly.Python['order_type'] = Blockly.Python.forBlock['order_type'] = function(block) {
const orderType = block.getFieldValue('ORDER_TYPE');
const limitPrice = Blockly.Python.valueToCode(block, 'LIMIT_PRICE', Blockly.Python.ORDER_ATOMIC) || 'None';
let code = `order_type='${orderType}'`;
if (orderType === 'limit') {
code += `, limit_price=${limitPrice}`;
}
return code;
};
// Value Input block to Python code
Blockly.Python['value_input'] = Blockly.Python.forBlock['value_input'] = function(block) {
var value = block.getFieldValue('VALUE');
return [value.toString(), Blockly.Python.ORDER_ATOMIC]; // Returning both value and precedence
};
// Time in Force block to Python code
Blockly.Python['time_in_force'] = Blockly.Python.forBlock['time_in_force'] = function(block) {
const tif = block.getFieldValue('TIF');
const code = `tif='${tif}'`;
return code;
};
Blockly.Python['source'] = Blockly.Python.forBlock['source'] = function(block) {
var timeframe = block.getFieldValue('TF');
var exchange = block.getFieldValue('EXC');
var symbol = block.getFieldValue('SYM');
// Return the source information as an object or string
var code = `{'timeframe': '${timeframe}', 'exchange': '${exchange}', 'symbol': '${symbol}'}`;
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Generator code for strategy_profit_loss block
Blockly.Python['strategy_profit_loss'] = Blockly.Python.forBlock['strategy_profit_loss'] = function(block) {
var direction = block.getFieldValue('DIRECTION');
var code = `strategy_profit_loss('${direction}')`;
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Generator code for current_balance block
Blockly.Python['current_balance'] = Blockly.Python.forBlock['current_balance'] = function() {
var code = 'get_current_balance()';
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Generator code for starting_balance block
Blockly.Python['starting_balance'] = Blockly.Python.forBlock['starting_balance'] = function() {
var code = 'get_starting_balance()';
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Generator code for arithmetic_operator block
Blockly.Python['arithmetic_operator'] = Blockly.Python.forBlock['arithmetic_operator'] = function(block) {
var operator = block.getFieldValue('OPERATOR');
var left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC);
var right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC);
var code = `${left} ${operator} ${right}`;
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Python generator for the active_trades block
Blockly.Python['active_trades'] = Blockly.Python.forBlock['active_trades'] = function(block) {
var code = `get_active_trades()`; // You would define this method in your Python backtesting engine
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Python generator for the flag_is_set block
Blockly.Python['flag_is_set'] = Blockly.Python.forBlock['flag_is_set'] = function(block) {
var flagName = block.getFieldValue('FLAG_NAME');
var code = `flag_is_set('${flagName}')`;
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Python generator for set_flag block
Blockly.Python['set_flag'] = Blockly.Python.forBlock['set_flag'] = function(block) {
var condition = Blockly.Python.valueToCode(block, 'CONDITION', Blockly.Python.ORDER_ATOMIC);
var flagName = block.getFieldValue('FLAG_NAME');
var flagValue = block.getFieldValue('FLAG_VALUE') === 'True' ? 'True' : 'False';
var code = `if ${condition}:\n set_flag('${flagName}', ${flagValue})\n`;
return code;
};
console.log('Python generators defined');
}

View File

@ -1,159 +1,146 @@
class Signals { class Signals {
constructor(indicators) { constructor(ui) {
this.indicators = indicators; this.ui = ui;
this.signals=[]; this.comms = ui.data.comms;
this.indicators = ui.indicators;
this.data = ui.data;
this.signals = [];
// Register handlers with Comms for specific message types
this.comms.on('signal_created', this.handleSignalCreated.bind(this));
this.comms.on('signal_updated', this.handleSignalUpdated.bind(this));
this.comms.on('signal_deleted', this.handleSignalDeleted.bind(this));
this.comms.on('updates', this.handleUpdates.bind(this));
} }
handleSignalCreated(data) {
console.log("New signal created:", data);
// Logic to update signals UI
const list_of_one = [data];
this.set_data(list_of_one);
}
handleSignalUpdated(data) {
console.log("Signal updated:", data);
// Logic to update signals UI
this.update_signal_states(data);
}
handleSignalDeleted(data) {
console.log("Signal deleted:", data);
// Logic to remove signal from UI
this.delete_signal(data.name);
}
handleUpdates(data) {
const { s_updates } = data;
if (s_updates) {
this.update_signal_states(s_updates);
}
}
// Call to display the 'Create new signal' dialog. // Call to display the 'Create new signal' dialog.
open_signal_Form() { document.getElementById("new_sig_form").style.display = "grid"; } open_signal_Form() { document.getElementById("new_sig_form").style.display = "grid"; }
// Call to hide the 'Create new signal' dialog. // Call to hide the 'Create new signal' dialog.
close_signal_Form() { document.getElementById("new_sig_form").style.display = "none"; } close_signal_Form() { document.getElementById("new_sig_form").style.display = "none"; }
request_signals(){ request_signals() {
// Requests a list of all the signals from the server. // Requests a list of all the signals from the server.
if (window.UI.data.comms) { if (this.comms) {
window.UI.data.comms.sendToApp('request', { request: 'signals', user_name: window.UI.data.user_name }); this.comms.sendToApp('request', { request: 'signals', user_name: this.data.user_name });
} else { } else {
console.error('Comms instance not available.'); console.error('Comms instance not available.');
} }
} }
delete_signal(signal_name){ delete_signal(signal_name) {
// Requests that the server remove a specific signal. // Requests that the server remove a specific signal.
window.UI.data.comms.sendToApp('delete_signal', signal_name); this.comms.sendToApp('delete_signal', { name: signal_name });
// Get the signal element from the UI // Get the signal element from the UI
let child = document.getElementById(signal_name + '_item'); let child = document.getElementById(signal_name + '_item');
// Ask the parent of the signal element to remove its child(signal) from the document. // Ask the parent of the signal element to remove its child(signal) from the document.
child.parentNode.removeChild(child); if (child && child.parentNode) {
child.parentNode.removeChild(child);
}
} }
i_update(updates){
// Check the indicator updates for updates that are use as signal sources.
// Update the signals about these changes.
// Update the html that displays that info.
// Loop through all the signals. i_update(updates) {
for (let signal in this.signals){ for (let signal in this.signals) {
// Get the name of the 1st source.
let s1 = this.signals[signal].source1; let s1 = this.signals[signal].source1;
// Check the updates for a source 1 update. if (s1 in updates) {
if (s1 in updates){
// Get the property of that source.
let p1 = this.signals[signal].prop1; let p1 = this.signals[signal].prop1;
// Get the value of that property.
let value1 = updates[s1].data[0][p1]; let value1 = updates[s1].data[0][p1];
// Update the signals record of the value.
this.signals[signal].value1 = value1.toFixed(2); this.signals[signal].value1 = value1.toFixed(2);
} } else {
else{
// If there is no update move onto the next signal.
console.log('!no update for: s1 maybe the indicator is disabled'); console.log('!no update for: s1 maybe the indicator is disabled');
break; break;
} }
// If the second source is an indicator and not just a value. if (this.signals[signal].source2 != 'value') {
if (this.signals[signal].source2 != 'value'){
// Get the name of the second source.
let s2 = this.signals[signal].source2; let s2 = this.signals[signal].source2;
// Check is source 2 is in the updates.
if (s2 in updates) { if (s2 in updates) {
// Get the property of that source.
let p2 = this.signals[signal].prop2; let p2 = this.signals[signal].prop2;
// Get the value of that property.
let value2 = updates[s2].data[0][p2]; let value2 = updates[s2].data[0][p2];
// Update the signals record of the value.
this.signals[signal].value2 = value2.toFixed(2); this.signals[signal].value2 = value2.toFixed(2);
} } else {
else{
// If there is no update move onto the next signal.
console.log('!no update for: s2 maybe the indicator is disabled'); console.log('!no update for: s2 maybe the indicator is disabled');
break; break;
} }
// loop to next signal.
} }
// Update the html element that displays this information.
document.getElementById(this.signals[signal].name + '_value1').innerHTML = this.signals[signal].value1; document.getElementById(this.signals[signal].name + '_value1').innerHTML = this.signals[signal].value1;
document.getElementById(this.signals[signal].name + '_value2').innerHTML = this.signals[signal].value2; document.getElementById(this.signals[signal].name + '_value2').innerHTML = this.signals[signal].value2;
} }
} }
update_signal_states(s_updates){
for (name in s_updates){ update_signal_states(s_updates) {
let id = name + '_state' for (let name in s_updates) {
let id = name + '_state';
let span = document.getElementById(id); let span = document.getElementById(id);
span.innerHTML = s_updates[name]; if (span) {
span.innerHTML = s_updates[name];
}
console.log('state change!'); console.log('state change!');
console.log(name); console.log(name);
} }
} }
set_data(signals){
// Create a list item for every signal and add it to a UL element. set_data(signals) {
var ul = document.getElementById("signal_list"); var ul = document.getElementById("signal_list");
for (let sig in signals) {
// loop through a provided list of signals and attributes. let obj = typeof(signals[sig]) == 'string' ? JSON.parse(signals[sig]) : signals[sig];
for (let sig in signals){
// Create a Json object from each signals.
// During initialization this receives the object in string form.
// when the object is created this function receives an object.
if (typeof(signals[sig]) == 'string'){
var obj = JSON.parse(signals[sig]);
}else {var obj=signals[sig];}
// Keep a local record of the signals.
this.signals.push(obj); this.signals.push(obj);
// Define the function that is called when deleting an individual signal. let click_func = `this.delete_signal('${obj.name}')`;
let click_func = "window.UI.signals.delete_signal('" + obj.name + "')"; let delete_btn = `<button onclick="${click_func}" style="color:red;">&#10008;</button>`;
// create a delete button for every individual signal.
let delete_btn = '<button onclick="' + click_func + '" style="color:red;">&#10008;</button>';
// Put all the attributes into html elements. let signal_name = ` <span>${obj.name}: </span>`;
let signal_name = " <span>" + obj.name + ": </span>"; let signal_state = `<span id='${obj.name}_state'>${obj.state}</span><br>`;
let signal_state = "<span id='" + obj.name + "_state'>" + obj.state + "</span><br>"; let signal_source1 = `<span>${obj.source1}(${obj.prop1}) </span>`;
let signal_source1 = "<span>" + obj.source1 + "(" + obj.prop1 + ") </span>"; let signal_val1 = `<span id='${obj.name}_value1'>${obj.value1}</span>`;
let signal_val1 = "<span id='" + obj.name + "_value1'>" + obj.value1 + "</span>"; let operator = ` ${obj.operator} `;
let operator = " " + obj.operator + " "; let signal_source2 = `<span>${obj.source2}(${obj.prop2}) </span>`;
let signal_source2 = "<span>" + obj.source2 + "(" + obj.prop2 + ") </span>"; let signal_val2 = `<span id='${obj.name}_value2'>${obj.value2}</span>`;
let signal_val2 = "<span id='" + obj.name + "_value2'>" + obj.value2 + "</span>";
// Stick all the html together. let html = delete_btn + signal_name + signal_state + signal_source1 + signal_val1 + operator + signal_source2 + signal_val2;
let html = delete_btn;
html += signal_name + signal_state;
html += signal_source1 + signal_val1;
html += operator;
html += signal_source2 + signal_val2;
// Create the list item.
let li = document.createElement("li"); let li = document.createElement("li");
// Give it an id.
li.id = obj.name + '_item'; li.id = obj.name + '_item';
// Inject the html. li.innerHTML = html;
li.innerHTML= html;
// And add it the the UL we created earlier.
ul.appendChild(li); ul.appendChild(li);
} }
} }
fill_prop(target_id, indctr){ fill_prop(target_id, indctr) {
// arg1: Id of of a selection element.
// arg2: Name of an indicator
// Replace the options of an HTML select elements
// with the properties an indicator object.
// Fetch the objects using name and id received.
var target = document.getElementById(target_id); var target = document.getElementById(target_id);
var properties = window.UI.data.indicators[indctr]; var properties = this.indicators[indctr];
// Remove any previous options in the select tag.
removeOptions(target); removeOptions(target);
// Loop through each property in the object. for(let prop in properties) {
// Create an option element for each one. if (prop == 'type' || prop == 'visible' || prop == 'period' || prop.substring(0, 5) == 'color') {
// Append it to the selection element. continue;
for(let prop in properties)
{
if (prop =='type'|| prop == 'visible' || prop == 'period'){continue;}
if (prop.substring(0,5) == 'color'){continue;}
var opt = document.createElement("option");
opt.innerHTML = prop;
target.appendChild(opt);
} }
return; var opt = document.createElement("option");
opt.innerHTML = prop;
target.appendChild(opt);
}
function removeOptions(selectElement) { function removeOptions(selectElement) {
var i, L = selectElement.options.length - 1; var i, L = selectElement.options.length - 1;
for(i = L; i >= 0; i--) { for(i = L; i >= 0; i--) {
@ -161,140 +148,86 @@ class Signals {
} }
} }
} }
switch_panel(p1,p2){
// Panel switcher for multi page forms switch_panel(p1, p2) {
// arg1 = target from id document.getElementById(p1).style.display = 'none';
// arg2 = next target id document.getElementById(p2).style.display = 'grid';
// This function is used in the New Signal dialog in signals
document.getElementById(p1).style.display='none';
document.getElementById(p2).style.display='grid';
} }
hideIfTrue(firstValue, scndValue, id){ hideIfTrue(firstValue, scndValue, id) {
// Compare first two args and hides an element if they are equal. if (firstValue == scndValue) {
// This function is used in the New Signal dialog in signals document.getElementById(id).style.display = 'none';
if( firstValue == scndValue){ } else {
document.getElementById(id).style.display='none'; document.getElementById(id).style.display = 'block';
}else{
document.getElementById(id).style.display='block'
} }
} }
ns_next(n){
// This function is used in the New Signal dialog in signals ns_next(n) {
if (n==1){ if (n == 1) {
// Check input fields.
let sigName = document.getElementById('signal_name').value; let sigName = document.getElementById('signal_name').value;
let sigSource = document.getElementById('sig_source').value; let sigSource = document.getElementById('sig_source').value;
let sigProp = document.getElementById('sig_prop').value; let sigProp = document.getElementById('sig_prop').value;
if (sigName == '' ) { alert('Please give the signal a name.'); return; } if (sigName == '') { alert('Please give the signal a name.'); return; }
// Populate sig_display document.getElementById('sig_display').innerHTML = `${sigName}: {${sigSource}:${sigProp}}`;
document.getElementById('sig_display').innerHTML = (sigName + ': {' + sigSource + ':' + sigProp +'}'); let indctrVal = document.getElementById(sigSource + '_' + sigProp).value;
// Popilate Value input
let indctrVal = document.getElementById(sigSource + '_' + sigProp).value;
document.getElementById('value').value = indctrVal; document.getElementById('value').value = indctrVal;
this.switch_panel('panel_1', 'panel_2');
this.switch_panel('panel_1','panel_2');
} }
if (n==2){ if (n == 2) {
let sigName = document.getElementById('signal_name').value;
let sigSource = document.getElementById('sig_source').value;
let sigProp = document.getElementById('sig_prop').value;
let sig2Source = document.getElementById('sig2_source').value;
let sig2Prop = document.getElementById('sig2_prop').value;
let operator = document.querySelector('input[name="Operator"]:checked').value;
let range = document.getElementById('rangeVal').value;
let sigType = document.getElementById('select_s_type').value;
let value = document.getElementById('value').value;
// Collect all the input fields. let sig1 = `${sigSource} : ${sigProp}`;
let sig2 = sigType == 'Comparison' ? `${sig2Source} : ${sig2Prop}` : value;
let sigName = document.getElementById('signal_name').value; // The name of the New Signal. let operatorStr = operator == '+/-' ? `${operator} ${range}` : operator;
let sigSource = document.getElementById('sig_source').value; // The source(indicator) of the signal.
let sigProp = document.getElementById('sig_prop').value; // The property to evaluate.
let sig2Source = document.getElementById('sig2_source').value; // The second source if selected.
let sig2Prop = document.getElementById('sig2_prop').value; // The second property to evaluate.
let operator = document.querySelector('input[name="Operator"]:checked').value; // The operator this evaluation will use.
let range = document.getElementById('rangeVal').value; // The value of any range being evaluated.
let sigType = document.getElementById('select_s_type').value; // The type of signal value or indicator comparison.
let value = document.getElementById('value').value; // The input value if it is a value comparison.
// Create a string to define the signal.
// Include the first indicator source.
var sig1 = `${sigSource} : ${sigProp}`;
// If it is a comparison signal include the second indicator source.
if (sigType == 'Comparison') {
var sig2 = `${sig2Source} : ${sig2Prop}`;
}
// If it is a value signal include the value.
if (sigType == 'Value') {var sig2 = value;}
// If the operator is set to range, include the range value in the string.
if (operator == '+/-') {
var operatorStr = `${operator} ${range}`;
} else{
var operatorStr = operator;
}
let sigDisplayStr = `(${sigName}) (${sig1}) (${operatorStr}) (${sig2})`; let sigDisplayStr = `(${sigName}) (${sig1}) (${operatorStr}) (${sig2})`;
// Get the current realtime values of the sources.
let sig1_realtime = document.getElementById(sigSource + '_' + sigProp).value; let sig1_realtime = document.getElementById(sigSource + '_' + sigProp).value;
let sig2_realtime = sigType == 'Comparison' ? document.getElementById(sig2Source + '_' + sig2Prop).value : sig2;
if (sigType == 'Comparison') {
// If its a comparison get the second value from the second source.
var sig2_realtime = document.getElementById(sig2Source + '_' + sig2Prop).value;
}else {
// If not the second realtime value is literally the value.
var sig2_realtime = sig2;
}
// Populate the signal display field with the string.
document.getElementById('sig_display2').innerHTML = sigDisplayStr; document.getElementById('sig_display2').innerHTML = sigDisplayStr;
document.getElementById('sig_realtime').innerHTML = `(${sigProp} : ${sig1_realtime}) (${operatorStr}) (${sig2_realtime})`;
// Populate the realtime values display. let evalStr;
let realtime_Str = `(${sigProp} : ${sig1_realtime}) (${operatorStr}) (${sig2_realtime})`; if (operator == '=') evalStr = (sig1_realtime == sig2_realtime);
document.getElementById('sig_realtime').innerHTML = realtime_Str; if (operator == '>') evalStr = (sig1_realtime > sig2_realtime);
// Evaluate the signal if (operator == '<') evalStr = (sig1_realtime < sig2_realtime);
var evalStr; if (operator == '+/-') evalStr = (Math.abs(sig1_realtime - sig2_realtime) <= range);
if (operator == '=') {evalStr = (sig1_realtime == sig2_realtime);console.log([sig1_realtime, sig2_realtime, operator,evalStr]);}
if (operator == '>') {evalStr = (sig1_realtime > sig2_realtime);}
if (operator == '<') {evalStr = (sig1_realtime < sig2_realtime);}
if (operator == '+/-') {
evalStr = (Math.abs(sig1_realtime - sig2_realtime) <= range);
}
// Populate the signal eval field with the string.
document.getElementById('sig_eval').innerHTML = evalStr; document.getElementById('sig_eval').innerHTML = evalStr;
this.switch_panel('panel_2', 'panel_3');
// Show the panel
this.switch_panel('panel_2','panel_3');
} }
} }
submitNewSignal(){ submitNewSignal() {
let name = document.getElementById('signal_name').value;
let source1 = document.getElementById('sig_source').value;
let prop1 = document.getElementById('sig_prop').value;
let source2 = document.getElementById('sig2_source').value;
let prop2 = document.getElementById('sig2_prop').value;
let operator = document.querySelector('input[name="Operator"]:checked').value;
let range = document.getElementById('rangeVal').value;
let sigType = document.getElementById('select_s_type').value;
let value = document.getElementById('value').value;
// Collect all the input fields. if (sigType != 'Comparison') {
source2 = 'value';
var name = document.getElementById('signal_name').value; // The name of the New Signal. prop2 = value;
var source1 = document.getElementById('sig_source').value; // The source(indicator) of the signal.
var prop1 = document.getElementById('sig_prop').value; // The property to evaluate.
var source2 = document.getElementById('sig2_source').value; // The second source if selected.
var prop2 = document.getElementById('sig2_prop').value; // The second property to evaluate.
var operator = document.querySelector('input[name="Operator"]:checked').value; // The operator this evaluation will use.
var range = document.getElementById('rangeVal').value; // The value of any range being evaluated.
var sigType = document.getElementById('select_s_type').value; // The type of signal value or indicator comparison.
var value = document.getElementById('value').value; // The input value if it is a value comparison.
var state = false;
if (sigType == 'Comparison'){
var source2 = source2;
var prop2 = prop2;
}else{
var source2 = 'value';
var prop2 = value;
} }
var value1 = null;
var value2 = null;
if (operator == "+/-" ){
var range = {range : range};
var data = {name, source1, prop1, operator, source2, prop2, range, state, value1, value2};
}else{
var data = {name, source1, prop1, operator, source2, prop2, state, value1, value2};
}
/* It may be more maintainable to configure the connection inside the different classes
than passing functions, references and callbacks around. */
window.UI.data.comms.sendToApp( "new_signal", data); let state = false;
this.close_signal_Form(); let value1 = null;
let value2 = null;
let data = operator == "+/-" ? {name, source1, prop1, operator, source2, prop2, range, state, value1, value2} : {name, source1, prop1, operator, source2, prop2, state, value1, value2};
this.comms.sendToApp("new_signal", data);
this.close_signal_Form();
} }
} }

View File

@ -47,12 +47,15 @@ class Trade {
// Store this object pointer for referencing inside callbacks and event handlers. // Store this object pointer for referencing inside callbacks and event handlers.
var that = this; var that = this;
// Assign the quote value of the asset to the current price display element. // Assign the quote value of the asset to the current price display element.
window.UI.data.price_history.then((ph) => { if (window.UI.data.price_history && window.UI.data.price_history.length > 0) {
let ph = window.UI.data.price_history;
// Assign the last closing price in the price history to the price input element. // Assign the last closing price in the price history to the price input element.
that.priceInput_el.value = ph[ph.length-1].close; that.priceInput_el.value = ph[ph.length - 1].close;
// Set the current price display to the same value. // Set the current price display to the same value.
that.currentPrice_el.value = that.priceInput_el.value; that.currentPrice_el.value = that.priceInput_el.value;
}); } else {
console.error('Price history data is not available or empty.');
}
// Set the trade value to zero. This will update when price and quantity inputs are received. // Set the trade value to zero. This will update when price and quantity inputs are received.
this.tradeValue_el.value = 0; this.tradeValue_el.value = 0;
// Toggle current price or input-field for value updates depending on orderType. // Toggle current price or input-field for value updates depending on orderType.

View File

@ -0,0 +1,105 @@
<!-- Backtest Form Popup -->
<div class="form-popup" id="backtest_form" style="display: none; overflow: hidden; position: absolute; width: 400px; height: 600px; border-radius: 10px;">
<!-- Draggable Header Section -->
<div id="backtest_draggable_header" style="cursor: move; padding: 10px; background-color: #3E3AF2; color: white; border-bottom: 1px solid #ccc;">
<h1 id="backtest-form-header-create">Create New Backtest</h1>
<h1 id="backtest-form-header-edit" style="display: none;">Edit Backtest</h1>
</div>
<!-- Main Content (Scrollable) -->
<form class="form-container" style="overflow-y: auto;">
<!-- Select Strategy -->
<div>
<label for="strategy_select" style="display: inline-block; width: 30%;">Strategy:</label>
<select id="strategy_select" name="strategy_select" style="width: 65%;"></select>
</div>
<!-- Start Date -->
<div>
<label for="start_date" style="display: inline-block; width: 30%;">Start Date/Time:</label>
<input type="datetime-local" id="start_date" name="start_date" style="width: 65%;" required>
</div>
<!-- Initial Capital -->
<div>
<label for="initial_capital" style="display: inline-block; width: 30%;">Initial Capital:</label>
<input type="number" id="initial_capital" name="initial_capital" style="width: 65%;" required value="10000">
</div>
<!-- Commission -->
<div>
<label for="commission" style="display: inline-block; width: 30%;">Commission:</label>
<input type="number" step="0.0001" id="commission" name="commission" style="width: 65%;" required value="0.001">
</div>
<!-- Buttons -->
<div style="text-align: center;">
<button type="button" class="btn cancel" onclick="UI.backtesting.closeForm()">Close</button>
<button id="backtest-submit-create" type="button" class="btn next" onclick="UI.backtesting.submitTest('new')">Run Test</button>
<button id="backtest-submit-edit" type="button" class="btn next" onclick="UI.backtesting.submitTest('edit')" style="display:none;">Edit Test</button>
</div>
<!-- Progress bar (Initially Hidden) -->
<div id="backtest-progress-container" style="display: none; margin-top: 10px;">
<div id="progress_bar" style="width: 0%; height: 20px; background-color: green; text-align: center; color: white;">
0%
</div>
</div>
<!-- Results section (Initially Hidden) -->
<div id="backtest-results" style="display: none; margin-top: 10px;">
<h4>Test Results</h4>
<pre id="results_display"></pre>
</div>
</form>
<!-- Resizer -->
<div id="resize-backtest" class="resize-handle"></div>
</div>
<style>
/* Styling for backtest popup and container */
.backtests-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.backtest-item {
position: relative;
width: 150px;
height: 120px;
text-align: center;
transition: transform 0.3s ease;
}
.backtest-item:hover {
transform: scale(1.05);
}
.backtest-name {
position: absolute;
top: 80px;
left: 50%;
transform: translateX(-50%);
background-color: white;
padding: 5px;
border-radius: 10px;
color: black;
text-align: center;
width: 100px;
}
.delete-button {
position: absolute;
top: 5px;
left: 5px;
background-color: red;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
}
</style>

View File

@ -1,3 +1,7 @@
<div class="content"> <div class="content" id="backtesting_hud">
<h3>Back Testing</h3> <button class="btn" id="new_backtest_btn" onclick="UI.backtesting.openForm('new')">New Backtest</button>
</div> <hr>
<h3>Back Tests</h3>
<div class="backtests-container" id="backtest_display"></div>
</div>

View File

@ -29,11 +29,13 @@
<script src="{{ url_for('static', filename='controls.js') }}"></script> <script src="{{ url_for('static', filename='controls.js') }}"></script>
<script src="{{ url_for('static', filename='signals.js') }}"></script> <script src="{{ url_for('static', filename='signals.js') }}"></script>
<script src="{{ url_for('static', filename='trade.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='general.js') }}"></script> <script src="{{ url_for('static', filename='general.js') }}"></script>
</head> </head>
<body> <body>
<!-- Hidden Div elements containing markup for popup context and forms.--> <!-- Hidden Div elements containing markup for popup context and forms.-->
{% include "backtest_popup.html" %}
{% include "new_trade_popup.html" %} {% include "new_trade_popup.html" %}
{% include "new_strategy_popup.html" %} {% include "new_strategy_popup.html" %}
{% include "new_signal_popup.html" %} {% include "new_signal_popup.html" %}

View File

@ -42,11 +42,8 @@
<!-- Fixed property: Value --> <!-- Fixed property: Value -->
<div style="text-align: center;"> <div style="text-align: center;">
{% if 'value' in indicator_list[indicator] %} <!-- Create a generic value container for JavaScript to populate -->
<input class="ie_value" type="number" id="{{indicator}}_value" value="{{indicator_list[indicator]['value']}}" name="value" readonly> <textarea class="ie_value" id="{{indicator}}_value" name="value" readonly style="resize: none; overflow: hidden; height: auto; width: 100%; border: none; background: transparent; text-align: center;">N/A</textarea>
{% else %}
<span>-</span>
{% endif %}
</div> </div>
<!-- Fixed property: Type --> <!-- Fixed property: Type -->
@ -103,9 +100,11 @@
<!-- Color Picker --> <!-- Color Picker -->
<div style="text-align: center;"> <div style="text-align: center;">
{% if 'color' in indicator_list[indicator] %} {% if 'color' in indicator_list[indicator] %}
<input class="ietextbox" type="color" id="{{indicator}}_color" value="{{indicator_list[indicator]['color']}}" name="color"> <input class="ietextbox" type="color" id="{{indicator}}_color"
value="{{ indicator_list[indicator]['color'] if indicator_list[indicator]['color'] else '#000000' }}"
name="color">
{% else %} {% else %}
<span>-</span> <input class="ietextbox" type="color" id="{{indicator}}_color" value="#000000" name="color">
{% endif %} {% endif %}
</div> </div>

View File

@ -41,7 +41,7 @@
</form> </form>
<!-- Add the missing resize handle here --> <!-- Add the missing resize handle here -->
<div class="resize-handle" id="resize-br"></div> <div id="resize-strategy" class="resize-handle"></div>
</div> </div>
<!-- Add CSS to hide scrollbars but allow scrolling and fix the resize handle --> <!-- Add CSS to hide scrollbars but allow scrolling and fix the resize handle -->
@ -109,24 +109,36 @@
<!-- Indicator blocks go here --> <!-- Indicator blocks go here -->
</category> </category>
<category name="Values" colour="230"> <category name="Values" colour="230">
<block type="last_candle_value"></block> <block type="last_candle_value"></block>
<block type="value_input"></block> <block type="value_input"></block>
</category> <block type="source"></block>
<block type="current_balance"></block>
<block type="starting_balance"></block>
<block type="strategy_profit_loss"></block>
<block type="active_trades"></block>
</category>
<category name="Logic" colour="210"> <category name="Logic" colour="210">
<block type="comparison"></block> <block type="comparison"></block>
<block type="logical_and"></block> <block type="logical_and"></block>
<block type="logical_or"></block> <block type="logical_or"></block>
<block type="is_true"></block> <block type="arithmetic_operator"></block>
<block type="is_false"></block>
<block type="flag_is_set"></block>
<block type="set_flag"></block>
</category> </category>
<!-- New category for Trading Actions --> <!-- New category for Trading Actions -->
<category name="Trading" colour="230"> <category name="Trading" colour="230">
<block type="trade_action"></block> <block type="trade_action"></block>
<block type="order_type"></block> <block type="order_type"></block>
<block type="time_in_force"></block> <block type="time_in_force"></block>
<block type="stop_loss"></block> <block type="stop_loss"></block>
<block type="take_profit"></block> <block type="take_profit"></block>
</category> <block type="target_market"></block>
</category>
</xml> </xml>

View File

@ -1,10 +1,12 @@
<!-- Container for the javascript chart --> <!-- Container for the javascript chart -->
<div id="chart"> <div id="chart">
<div class="a1" > <div class="a1" >
<!-- Toggle On/Off indicators-->
<button id="enable_indicators" type="button" onclick="UI.controls.showAtPos(event,'indicators')">Indicators</button>
<!-- Container exchange_name for any indicator output -->
<div id="indicator_output" ></div>
<!-- Chart specific controls --> <!-- Chart specific controls -->
<div id="chart_controls"> <div id="chart_controls">
<!-- Container exchange_name for any indicator output -->
<div id="indicator_output" ></div>
<!-- Trading pair selector --> <!-- Trading pair selector -->
<form id="tp_selector" action="/settings" method="post"> <form id="tp_selector" action="/settings" method="post">
<input type="hidden" name="setting" value="trading_pair" /> <input type="hidden" name="setting" value="trading_pair" />
@ -34,8 +36,6 @@
{% endfor %} {% endfor %}
</select> </select>
</form> </form>
<!-- Toggle On/Off indicators-->
<button id="enable_indicators" type="button" onclick="UI.controls.showAtPos(event,'indicators')">Indicators</button>
</div> </div>
</div> </div>
</div> </div>