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
flask==2.3.2
flask_cors==3.0.10
flask_sock==0.7.0
numpy<2.0.0
flask==3.0.3
config~=0.5.1
PyYAML~=6.0
PyYAML==6.0.2
requests==2.30.0
pandas==2.0.1
pandas==2.2.3
passlib~=1.7.4
SQLAlchemy==2.0.13
ccxt==4.3.65
email-validator~=2.2.0
TA-Lib~=0.4.32
bcrypt~=4.2.0
ccxt==4.4.8
pytz==2024.2
backtrader==1.9.78.123

View File

@ -48,7 +48,8 @@ class BrighterTrades:
self.strategies = Strategies(self.data, self.trades)
# 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:
"""
@ -219,6 +220,7 @@ class BrighterTrades:
chart_view = self.users.get_chart_view(user_name=user_name)
indicator_types = self.indicators.get_available_indicator_types()
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:
chart_view = {'timeframe': '', 'exchange_name': '', 'market': ''}
@ -234,7 +236,9 @@ class BrighterTrades:
'exchange_name': chart_view.get('exchange_name'),
'trading_pair': chart_view.get('market'),
'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
@ -569,6 +573,12 @@ class BrighterTrades:
""" Return a JSON object of all the trades in the trades instance."""
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):
"""
Adjusts the specified setting for a user.
@ -638,12 +648,14 @@ class BrighterTrades:
# self.candles.set_cache(user_name=user_name)
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.
:param msg_type: The type of 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
no data is found to ensure the WebSocket channel isn't burdened with unnecessary
communication.
@ -706,6 +718,17 @@ class BrighterTrades:
r_data = self.connect_or_config_exchange(user_name=user, exchange_name=exchange, api_keys=keys)
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 the message is a reply log the response to the terminal.
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 numpy as np
import json
from indicators import Indicator, indicators_registry
from shared_utilities import unix_time_millis

View File

@ -562,7 +562,8 @@ class UserIndicatorManagement(UserExchangeManagement):
else:
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):

View File

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

View File

@ -1,53 +1,312 @@
from dataclasses import dataclass, field, asdict
from itertools import count
import ast
import json
import re
@dataclass
class Order:
symbol: str
clientOrderId: hex
transactTime: float
price: float
origQty: float
executedQty: float
cummulativeQuoteQty: float
status: str
timeInForce: str
type: str
side: str
orderId: int = field(default_factory=count().__next__)
import backtrader as bt
import datetime as dt
from DataCache_v3 import DataCache
from Strategies import Strategies
import threading
import numpy as np
class Backtester:
def __init__(self):
self.orders = []
def __init__(self, data_cache: DataCache, strategies: Strategies):
""" 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):
order = Order(symbol=symbol, clientOrderId=newClientOrderId, transactTime=1507725176595, price=price,
origQty=quantity, executedQty=quantity, cummulativeQuoteQty=(quantity * price), status='FILLED',
timeInForce=timeInForce, type=type, side=side)
self.orders.append(order)
return asdict(order)
def get_default_chart_view(self, user_name):
"""Fetch default chart view if no specific source is provided."""
return self.data_cache.get_datacache_item(
item_name='chart_view', cache_name='users', filter_vals=('user_name', user_name))
def create_order(self, symbol, side, type, timeInForce, quantity, price=None, newClientOrderId=None):
order = Order(symbol=symbol, clientOrderId=newClientOrderId, transactTime=1507725176595, price=price,
origQty=quantity, executedQty=quantity, cummulativeQuoteQty=(quantity * price), status='FILLED',
timeInForce=timeInForce, type=type, side=side)
self.orders.append(order)
return asdict(order)
def cache_backtest(self, user_name, backtest_name, backtest_data):
""" Cache the backtest data for a user """
columns = ('user_name', 'strategy_name', 'start_time', 'capital', 'commission', 'results')
values = (
backtest_data.get('user_name'),
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):
for order in self.orders:
if order.symbol == symbol:
if order.orderId == orderId:
return asdict(order)
return None
def map_user_strategy(self, user_strategy):
"""Maps user strategy details into a Backtrader-compatible strategy class."""
def get_precision(self, symbol=None):
return 3
class MappedStrategy(bt.Strategy):
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):
return 10
def __init__(self):
# Extract unique sources (exchange, symbol, timeframe) from blocks
self.sources = self.extract_sources(user_strategy)
def get_min_qty(self, symbol=None):
return 0.001
# Map of source to data feed (used later in next())
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('slow_p', 26)
self.properties.setdefault('signal_p', 9)
self.properties.setdefault('color_1', generate_random_color()) # Upper band
self.properties.setdefault('color_2', generate_random_color()) # Middle 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 {
constructor() {
this.height = height;
}
constructor(ui) {
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;
}
#indicator_output{
overflow-y: scroll;
width: 300px;
height:50px;
padding: 3px;
border-style: solid;
color: blueviolet;
position: absolute;
height: fit-content;
width: 150px;
margin-top: 30px;
border-radius: 8px;
background-image: linear-gradient(93deg, #ffffffde, #fffefe29);
}
#chart_controls{
border-style:none;
width: 775px;
width: 600px;
padding: 15px;
display: grid;
grid-template-columns:350px 2fr 1fr 1fr;
grid-template-columns: 4fr 2fr 2fr 2fr;
margin-left: 250;
}
#indicators{
display: none;

View File

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

View File

@ -2,14 +2,12 @@ class Comms {
constructor() {
this.connectionOpen = false;
this.appCon = null; // WebSocket connection for app communication
this.eventHandlers = {}; // Event handlers for message types
// Callback collections that will receive various updates.
this.candleUpdateCallbacks = [];
this.candleCloseCallbacks = [];
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 */
candleUpdate(newCandle) {
@ -40,7 +62,7 @@ class Comms {
}
}
candleClose(newCandle) {
candleClose(newCandle) {
this.sendToApp('candle_data', newCandle);
for (const callback of this.candleCloseCallbacks) {
@ -149,7 +171,7 @@ class Comms {
}
}
/**
/**
* Sends a request to update an indicator's properties.
* @param {Object} indicatorData - An object containing the updated properties of the indicator.
* @returns {Promise<Object>} - The response from the server.
@ -206,7 +228,6 @@ class Comms {
}
}
setAppCon() {
this.appCon = new WebSocket('ws://localhost:5000/ws');
@ -228,71 +249,8 @@ class Comms {
}
if (message && message.reply !== undefined) {
// Handle different reply types from the server
if (message.reply === 'updates') {
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);
}
// Emit the event to registered handlers
this.emit(message.reply, message.data);
}
}
});
@ -309,8 +267,6 @@ class Comms {
};
}
/**
* Sets up a WebSocket connection to the exchange for receiving 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
callbacks to handle various incoming messages.*/
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.
this.price_history = this.comms.getPriceHistory(this.user_name);
// Last price from price history.
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.
// Initialize other properties
this.price_history = null;
this.indicator_data = null;
this.last_price = 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){
// This is called everytime a candle update comes from the local server.
window.UI.charts.update_main_chart(new_candle);
@ -60,11 +71,17 @@ class Data {
registerCallback_i_updates(call_back){
this.i_updates = call_back;
}
indicator_update(data){
// This is called everytime an indicator update come in.
window.UI.data.i_updates(data);
// This is called everytime an indicator update come in.
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){
// This is called everytime a candle closes.
//console.log('Candle close:');

View File

@ -1,15 +1,16 @@
class User_Interface {
constructor() {
// Initialize all components needed by the user interface
this.strats = new Strategies('strats_display');
this.strats = new Strategies();
this.exchanges = new Exchanges();
this.data = new Data();
this.controls = new Controls();
this.signals = new Signals(this.data.indicators);
this.alerts = new Alerts("alert_list");
this.trade = new Trade();
this.users = new Users();
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
this.data.registerCallback_i_updates(this.indicators.update);
@ -18,10 +19,18 @@ class User_Interface {
this.initializeAll();
}
initializeAll() {
window.addEventListener('load', () => {
this.initializeChartsAndIndicators();
this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats));
async initializeAll() {
window.addEventListener('load', async () => {
try {
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.trade.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).
* @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 {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);
this.dragElement(popupElement);
this.makeResizable(popupElement, resizeCallback);
this.dragElement(popupElement, headerId);
this.makeResizable(popupElement, resizeCallback, resizerId);
}
/**
* Make an element draggable by dragging its header.
* @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) {
const header = document.getElementById("draggable_header");
dragElement(elm, headerId = null) {
const header = headerId ? document.getElementById(headerId) : elm;
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (header) {
@ -102,9 +115,10 @@ class User_Interface {
* Make an element resizable and optionally call a resize callback (like for Blockly workspace).
* @param {HTMLElement} elm - The element to make resizable.
* @param {function|null} resizeCallback - Optional callback to resize specific content.
* @param {string} resizerId - The ID of the resizer handle element.
*/
makeResizable(elm, resizeCallback = null) {
const resizer = document.getElementById("resize-br");
makeResizable(elm, resizeCallback = null, resizerId) {
const resizer = document.getElementById(resizerId);
let originalWidth = 0;
let originalHeight = 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 {
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
let target_div = document.getElementById('indicator_output');
this.legend[name] = document.createElement('div');
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]);
this.legend[name].style.display = 'block';
this.legend[name].style.left = 3 + 'px';
this.legend[name].style.top = 3 + 'px';
// subscribe set legend text to crosshair moves
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.
let val = 'n/a';
if (priceValue !== undefined) {
val = (Math.round(priceValue * 100) / 100).toFixed(2);
}
this.legend[name].innerHTML = name + ' <span style="color:rgba(4, 111, 232, 1)">' + val + '</span>';
if (priceValue !== undefined) {
val = (Math.round(priceValue * 100) / 100).toFixed(2);
}
// 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) {
// Remove the legend div from the DOM
if (this.legend[name]) {
this.legend[name].remove(); // Remove the legend from the DOM
delete this.legend[name]; // Remove the reference from the object
} else {
console.warn(`Legend for ${name} not found.`);
for (const key in this.legend) {
if (key.startsWith(name)) {
this.legend[key].remove(); // Remove the legend from the DOM
delete this.legend[key]; // Remove the reference from the object
}
}
}
}
@ -84,27 +101,71 @@ class Indicator {
color: color,
lineWidth: lineWidth
});
// Initialise the crosshair legend for the charts.
iOutput.create_legend(this.name, chart, this.lines[name]);
// Initialise the crosshair legend for the charts with a unique name for each line.
iOutput.create_legend(`${this.name}_${name}`, chart, this.lines[name]);
}
setLine(name, data, value_name) {
console.log('indicators[68]: setLine takes:(name,data,value_name)');
console.log(name, data, value_name);
// Initialize the data with the data object provided.
this.lines[name].setData(data);
// Isolate the last value provided and round to 2 decimals places.
let priceValue = data.at(-1).value;
this.updateDisplay(name, priceValue, value_name);
// Update indicator output/crosshair legend.
iOutput.set_legend_text(data.at(-1).value, this.name);
setLine(lineName, data, value_name) {
console.log('indicators[68]: setLine takes:(lineName, data, value_name)');
console.log(lineName, data, value_name);
let priceValue;
// Check if the data is a multi-value object
if (typeof data === 'object' && data !== null && value_name in data) {
// Multi-value indicator: Extract the array for the specific key
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) {
// Update the data in the edit and view indicators panel
let element = document.getElementById(this.name + '_' + value_name)
if (element){
element.value = (Math.round(priceValue * 100) / 100).toFixed(2);
let element = document.getElementById(this.name + '_' + value_name);
if (element) {
if (typeof priceValue === 'object' && priceValue !== null) {
// 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 {
console.warn(`Element with ID ${this.name}_${value_name} not found.`);
}
@ -115,12 +176,34 @@ class Indicator {
}
updateLine(name, data, value_name) {
// Update the line-set data in the chart
this.lines[name].update(data);
// Update indicator output/crosshair legend.
iOutput.set_legend_text(data.value, this.name);
// Update the data in the edit and view indicators panel
this.updateDisplay(name, data.value, value_name);
console.log('indicators[68]: updateLine takes:(name, data, value_name)');
console.log(name, data, value_name);
// Check if the data is a multi-value object
if (typeof data === 'object' && data !== null && value_name in data) {
// 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) {
@ -249,19 +332,25 @@ class MACD extends Indicator {
const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null);
if (filteredData.length > 0) {
// Set the 'line_m' for the MACD line
this.setLine('line_m', filteredData.map(row => ({
// Prepare the filtered data for the MACD line
const line_m = filteredData.map(row => ({
time: row.time,
value: row.macd
})), 'macd');
}));
// Set the 'line_s' for the signal line
this.setLine('line_s', filteredData.map(row => ({
// Set the 'line_m' for the MACD line
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,
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 => ({
time: row.time,
value: row.hist
@ -271,15 +360,35 @@ class MACD extends Indicator {
}
}
update(data) {
// Update the 'macd' line
this.updateLine('line_m', {time: data[0].time, value: data[0].macd }, 'macd');
// Filter out rows where macd, signal, or hist are null
const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null);
// Update the 'signal' line
this.updateLine('line_s', { time: data[0].time, value: data[0].signal }, 'signal');
if (filteredData.length > 0) {
// 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
this.updateHist('hist', {time: data[0].time, value: data[0].hist });
// Update the 'signal' line
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);
@ -341,30 +450,64 @@ class Bolenger extends Indicator {
};
}
init(data) {
// Set the 'line_u' for the upper line
this.setLine('line_u', data.map(row => ({
time: row.time,
value: row.upper
})), 'value');
init(data) {
// Filter out rows where upper, middle, or lower are null
const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null);
// Set the 'line_m' for the middle line
this.setLine('line_m', data.map(row => ({
time: row.time,
value: row.middle
})), 'value2');
if (filteredData.length > 0) {
// Set the 'line_u' for the upper line
const line_u = filteredData.map(row => ({
time: row.time,
value: row.upper
}));
this.setLine('line_u', { upper: line_u }, 'upper');
// Set the 'line_l' for the lower line
this.setLine('line_l', data.map(row => ({
time: row.time,
value: row.lower
})), 'value3');
}
// Set the 'line_m' for the middle line
const line_m = filteredData.map(row => ({
time: row.time,
value: row.middle
}));
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) {
this.updateLine('line_u', data[0][0], 'value');
this.updateLine('line_m', data[1][0], 'value2');
this.updateLine('line_l', data[2][0], 'value3');
// Filter out rows where upper, middle, or lower are null
const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null);
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);
@ -422,8 +565,12 @@ class Indicators {
objects, then inserts the data into the charts.
*/
this.create_indicators(idata.indicators, charts);
// Initialize each indicators with the data.
idata.indicator_data.then( (data) => { this.init_indicators(data); } );
// Initialize each indicator with the data directly
if (idata.indicator_data) {
this.init_indicators(idata.indicator_data);
} else {
console.error('Indicator data is not available.');
}
}
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 {
constructor(indicators) {
this.indicators = indicators;
this.signals=[];
constructor(ui) {
this.ui = ui;
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.
open_signal_Form() { document.getElementById("new_sig_form").style.display = "grid"; }
// Call to hide the 'Create new signal' dialog.
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.
if (window.UI.data.comms) {
window.UI.data.comms.sendToApp('request', { request: 'signals', user_name: window.UI.data.user_name });
if (this.comms) {
this.comms.sendToApp('request', { request: 'signals', user_name: this.data.user_name });
} else {
console.error('Comms instance not available.');
}
}
delete_signal(signal_name){
delete_signal(signal_name) {
// 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
let child = document.getElementById(signal_name + '_item');
// 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.
for (let signal in this.signals){
// Get the name of the 1st source.
i_update(updates) {
for (let signal in this.signals) {
let s1 = this.signals[signal].source1;
// Check the updates for a source 1 update.
if (s1 in updates){
// Get the property of that source.
if (s1 in updates) {
let p1 = this.signals[signal].prop1;
// Get the value of that property.
let value1 = updates[s1].data[0][p1];
// Update the signals record of the value.
this.signals[signal].value1 = value1.toFixed(2);
}
else{
// If there is no update move onto the next signal.
} else {
console.log('!no update for: s1 maybe the indicator is disabled');
break;
}
// If the second source is an indicator and not just a value.
if (this.signals[signal].source2 != 'value'){
// Get the name of the second source.
if (this.signals[signal].source2 != 'value') {
let s2 = this.signals[signal].source2;
// Check is source 2 is in the updates.
if (s2 in updates) {
// Get the property of that source.
let p2 = this.signals[signal].prop2;
// Get the value of that property.
let value2 = updates[s2].data[0][p2];
// Update the signals record of the value.
this.signals[signal].value2 = value2.toFixed(2);
}
else{
// If there is no update move onto the next signal.
} else {
console.log('!no update for: s2 maybe the indicator is disabled');
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 + '_value2').innerHTML = this.signals[signal].value2;
}
}
update_signal_states(s_updates){
for (name in s_updates){
let id = name + '_state'
update_signal_states(s_updates) {
for (let name in s_updates) {
let id = name + '_state';
let span = document.getElementById(id);
span.innerHTML = s_updates[name];
if (span) {
span.innerHTML = s_updates[name];
}
console.log('state change!');
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");
// loop through a provided list of signals and attributes.
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.
for (let sig in signals) {
let obj = typeof(signals[sig]) == 'string' ? JSON.parse(signals[sig]) : signals[sig];
this.signals.push(obj);
// Define the function that is called when deleting an individual signal.
let click_func = "window.UI.signals.delete_signal('" + obj.name + "')";
// create a delete button for every individual signal.
let delete_btn = '<button onclick="' + click_func + '" style="color:red;">&#10008;</button>';
let click_func = `this.delete_signal('${obj.name}')`;
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_state = "<span id='" + obj.name + "_state'>" + obj.state + "</span><br>";
let signal_source1 = "<span>" + obj.source1 + "(" + obj.prop1 + ") </span>";
let signal_val1 = "<span id='" + obj.name + "_value1'>" + obj.value1 + "</span>";
let operator = " " + obj.operator + " ";
let signal_source2 = "<span>" + obj.source2 + "(" + obj.prop2 + ") </span>";
let signal_val2 = "<span id='" + obj.name + "_value2'>" + obj.value2 + "</span>";
let signal_name = ` <span>${obj.name}: </span>`;
let signal_state = `<span id='${obj.name}_state'>${obj.state}</span><br>`;
let signal_source1 = `<span>${obj.source1}(${obj.prop1}) </span>`;
let signal_val1 = `<span id='${obj.name}_value1'>${obj.value1}</span>`;
let operator = ` ${obj.operator} `;
let signal_source2 = `<span>${obj.source2}(${obj.prop2}) </span>`;
let signal_val2 = `<span id='${obj.name}_value2'>${obj.value2}</span>`;
// Stick all the html together.
let html = delete_btn;
html += signal_name + signal_state;
html += signal_source1 + signal_val1;
html += operator;
html += signal_source2 + signal_val2;
let html = delete_btn + signal_name + signal_state + signal_source1 + signal_val1 + operator + signal_source2 + signal_val2;
// Create the list item.
let li = document.createElement("li");
// Give it an id.
li.id = obj.name + '_item';
// Inject the html.
li.innerHTML= html;
// And add it the the UL we created earlier.
li.innerHTML = html;
ul.appendChild(li);
}
}
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.
fill_prop(target_id, indctr) {
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);
// Loop through each property in the object.
// Create an option element for each one.
// Append it to the selection element.
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);
for(let prop in properties) {
if (prop == 'type' || prop == 'visible' || prop == 'period' || prop.substring(0, 5) == 'color') {
continue;
}
return;
var opt = document.createElement("option");
opt.innerHTML = prop;
target.appendChild(opt);
}
function removeOptions(selectElement) {
var i, L = selectElement.options.length - 1;
for(i = L; i >= 0; i--) {
@ -161,140 +148,86 @@ class Signals {
}
}
}
switch_panel(p1,p2){
// Panel switcher for multi page forms
// arg1 = target from id
// arg2 = next target id
// This function is used in the New Signal dialog in signals
document.getElementById(p1).style.display='none';
document.getElementById(p2).style.display='grid';
switch_panel(p1, p2) {
document.getElementById(p1).style.display = 'none';
document.getElementById(p2).style.display = 'grid';
}
hideIfTrue(firstValue, scndValue, id){
// Compare first two args and hides an element if they are equal.
// This function is used in the New Signal dialog in signals
if( firstValue == scndValue){
document.getElementById(id).style.display='none';
}else{
document.getElementById(id).style.display='block'
hideIfTrue(firstValue, scndValue, id) {
if (firstValue == scndValue) {
document.getElementById(id).style.display = 'none';
} else {
document.getElementById(id).style.display = 'block';
}
}
ns_next(n){
// This function is used in the New Signal dialog in signals
if (n==1){
// Check input fields.
ns_next(n) {
if (n == 1) {
let sigName = document.getElementById('signal_name').value;
let sigSource = document.getElementById('sig_source').value;
let sigProp = document.getElementById('sig_prop').value;
if (sigName == '' ) { alert('Please give the signal a name.'); return; }
// Populate sig_display
document.getElementById('sig_display').innerHTML = (sigName + ': {' + sigSource + ':' + sigProp +'}');
// Popilate Value input
let indctrVal = document.getElementById(sigSource + '_' + sigProp).value;
if (sigName == '') { alert('Please give the signal a name.'); return; }
document.getElementById('sig_display').innerHTML = `${sigName}: {${sigSource}:${sigProp}}`;
let indctrVal = document.getElementById(sigSource + '_' + sigProp).value;
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 sigName = document.getElementById('signal_name').value; // The name of the New Signal.
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 sig1 = `${sigSource} : ${sigProp}`;
let sig2 = sigType == 'Comparison' ? `${sig2Source} : ${sig2Prop}` : value;
let operatorStr = operator == '+/-' ? `${operator} ${range}` : operator;
let sigDisplayStr = `(${sigName}) (${sig1}) (${operatorStr}) (${sig2})`;
// Get the current realtime values of the sources.
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_realtime').innerHTML = `(${sigProp} : ${sig1_realtime}) (${operatorStr}) (${sig2_realtime})`;
// Populate the realtime values display.
let realtime_Str = `(${sigProp} : ${sig1_realtime}) (${operatorStr}) (${sig2_realtime})`;
document.getElementById('sig_realtime').innerHTML = realtime_Str;
// Evaluate the signal
var evalStr;
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);
}
let evalStr;
if (operator == '=') evalStr = (sig1_realtime == sig2_realtime);
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;
// Show the panel
this.switch_panel('panel_2','panel_3');
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.
var name = document.getElementById('signal_name').value; // The name of the New Signal.
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;
if (sigType != 'Comparison') {
source2 = 'value';
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);
this.close_signal_Form();
let state = false;
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.
var that = this;
// 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.
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.
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.
this.tradeValue_el.value = 0;
// 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">
<h3>Back Testing</h3>
</div>
<div class="content" id="backtesting_hud">
<button class="btn" id="new_backtest_btn" onclick="UI.backtesting.openForm('new')">New Backtest</button>
<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='signals.js') }}"></script>
<script src="{{ url_for('static', filename='trade.js') }}"></script>
<script src="{{ url_for('static', filename='backtesting.js') }}"></script>
<script src="{{ url_for('static', filename='general.js') }}"></script>
</head>
<body>
<!-- Hidden Div elements containing markup for popup context and forms.-->
{% include "backtest_popup.html" %}
{% include "new_trade_popup.html" %}
{% include "new_strategy_popup.html" %}
{% include "new_signal_popup.html" %}

View File

@ -42,11 +42,8 @@
<!-- Fixed property: Value -->
<div style="text-align: center;">
{% if 'value' in indicator_list[indicator] %}
<input class="ie_value" type="number" id="{{indicator}}_value" value="{{indicator_list[indicator]['value']}}" name="value" readonly>
{% else %}
<span>-</span>
{% endif %}
<!-- Create a generic value container for JavaScript to populate -->
<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>
</div>
<!-- Fixed property: Type -->
@ -103,9 +100,11 @@
<!-- Color Picker -->
<div style="text-align: center;">
{% 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 %}
<span>-</span>
<input class="ietextbox" type="color" id="{{indicator}}_color" value="#000000" name="color">
{% endif %}
</div>

View File

@ -41,7 +41,7 @@
</form>
<!-- Add the missing resize handle here -->
<div class="resize-handle" id="resize-br"></div>
<div id="resize-strategy" class="resize-handle"></div>
</div>
<!-- Add CSS to hide scrollbars but allow scrolling and fix the resize handle -->
@ -109,24 +109,36 @@
<!-- Indicator blocks go here -->
</category>
<category name="Values" colour="230">
<block type="last_candle_value"></block>
<block type="value_input"></block>
</category>
<category name="Values" colour="230">
<block type="last_candle_value"></block>
<block type="value_input"></block>
<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">
<block type="comparison"></block>
<block type="logical_and"></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>
<!-- New category for Trading Actions -->
<category name="Trading" colour="230">
<block type="trade_action"></block>
<block type="order_type"></block>
<block type="time_in_force"></block>
<block type="stop_loss"></block>
<block type="take_profit"></block>
</category>
<category name="Trading" colour="230">
<block type="trade_action"></block>
<block type="order_type"></block>
<block type="time_in_force"></block>
<block type="stop_loss"></block>
<block type="take_profit"></block>
<block type="target_market"></block>
</category>
</xml>

View File

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