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:
parent
86843e8cb4
commit
89e0f8b849
|
|
@ -1,14 +1,11 @@
|
||||||
numpy==1.24.3
|
numpy<2.0.0
|
||||||
flask==2.3.2
|
flask==3.0.3
|
||||||
flask_cors==3.0.10
|
|
||||||
flask_sock==0.7.0
|
|
||||||
config~=0.5.1
|
config~=0.5.1
|
||||||
PyYAML~=6.0
|
PyYAML==6.0.2
|
||||||
requests==2.30.0
|
requests==2.30.0
|
||||||
pandas==2.0.1
|
pandas==2.2.3
|
||||||
passlib~=1.7.4
|
passlib~=1.7.4
|
||||||
SQLAlchemy==2.0.13
|
ccxt==4.4.8
|
||||||
ccxt==4.3.65
|
|
||||||
email-validator~=2.2.0
|
pytz==2024.2
|
||||||
TA-Lib~=0.4.32
|
backtrader==1.9.78.123
|
||||||
bcrypt~=4.2.0
|
|
||||||
|
|
@ -48,7 +48,8 @@ class BrighterTrades:
|
||||||
self.strategies = Strategies(self.data, self.trades)
|
self.strategies = Strategies(self.data, self.trades)
|
||||||
|
|
||||||
# Object responsible for testing trade and strategies data.
|
# Object responsible for testing trade and strategies data.
|
||||||
self.backtester = Backtester()
|
self.backtester = Backtester(data_cache=self.data, strategies=self.strategies)
|
||||||
|
self.backtests = {} # In-memory storage for backtests (replace with DB access in production)
|
||||||
|
|
||||||
def create_new_user(self, email: str, username: str, password: str) -> bool:
|
def create_new_user(self, email: str, username: str, password: str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -219,6 +220,7 @@ class BrighterTrades:
|
||||||
chart_view = self.users.get_chart_view(user_name=user_name)
|
chart_view = self.users.get_chart_view(user_name=user_name)
|
||||||
indicator_types = self.indicators.get_available_indicator_types()
|
indicator_types = self.indicators.get_available_indicator_types()
|
||||||
available_indicators = self.indicators.get_indicator_list(user_name)
|
available_indicators = self.indicators.get_indicator_list(user_name)
|
||||||
|
exchange = self.exchanges.get_exchange(ename=chart_view.get('exchange'), uname=user_name)
|
||||||
|
|
||||||
if not chart_view:
|
if not chart_view:
|
||||||
chart_view = {'timeframe': '', 'exchange_name': '', 'market': ''}
|
chart_view = {'timeframe': '', 'exchange_name': '', 'market': ''}
|
||||||
|
|
@ -234,7 +236,9 @@ class BrighterTrades:
|
||||||
'exchange_name': chart_view.get('exchange_name'),
|
'exchange_name': chart_view.get('exchange_name'),
|
||||||
'trading_pair': chart_view.get('market'),
|
'trading_pair': chart_view.get('market'),
|
||||||
'user_name': user_name,
|
'user_name': user_name,
|
||||||
'public_exchanges': self.exchanges.get_public_exchanges()
|
'public_exchanges': self.exchanges.get_public_exchanges(),
|
||||||
|
'intervals': exchange.intervals if exchange else [],
|
||||||
|
'symbols': exchange.get_symbols() if exchange else {}
|
||||||
|
|
||||||
}
|
}
|
||||||
return js_data
|
return js_data
|
||||||
|
|
@ -569,6 +573,12 @@ class BrighterTrades:
|
||||||
""" Return a JSON object of all the trades in the trades instance."""
|
""" Return a JSON object of all the trades in the trades instance."""
|
||||||
return self.trades.get_trades('dict')
|
return self.trades.get_trades('dict')
|
||||||
|
|
||||||
|
def delete_backtest(self, msg_data):
|
||||||
|
""" Delete an existing backtest. """
|
||||||
|
backtest_name = msg_data.get('name')
|
||||||
|
if backtest_name in self.backtests:
|
||||||
|
del self.backtests[backtest_name]
|
||||||
|
|
||||||
def adjust_setting(self, user_name: str, setting: str, params: Any):
|
def adjust_setting(self, user_name: str, setting: str, params: Any):
|
||||||
"""
|
"""
|
||||||
Adjusts the specified setting for a user.
|
Adjusts the specified setting for a user.
|
||||||
|
|
@ -638,12 +648,14 @@ class BrighterTrades:
|
||||||
# self.candles.set_cache(user_name=user_name)
|
# self.candles.set_cache(user_name=user_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
def process_incoming_message(self, msg_type: str, msg_data: dict | str) -> dict | None:
|
def process_incoming_message(self, msg_type: str, msg_data: dict | str, socket_conn) -> dict | None:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Processes an incoming message and performs the corresponding actions based on the message type and data.
|
Processes an incoming message and performs the corresponding actions based on the message type and data.
|
||||||
|
|
||||||
:param msg_type: The type of the incoming message.
|
:param msg_type: The type of the incoming message.
|
||||||
:param msg_data: The data associated with the incoming message.
|
:param msg_data: The data associated with the incoming message.
|
||||||
|
:param socket_conn: The WebSocket connection to send updates back to the client.
|
||||||
:return: dict|None - A dictionary containing the response message and data, or None if no response is needed or
|
:return: dict|None - A dictionary containing the response message and data, or None if no response is needed or
|
||||||
no data is found to ensure the WebSocket channel isn't burdened with unnecessary
|
no data is found to ensure the WebSocket channel isn't burdened with unnecessary
|
||||||
communication.
|
communication.
|
||||||
|
|
@ -706,6 +718,17 @@ class BrighterTrades:
|
||||||
r_data = self.connect_or_config_exchange(user_name=user, exchange_name=exchange, api_keys=keys)
|
r_data = self.connect_or_config_exchange(user_name=user, exchange_name=exchange, api_keys=keys)
|
||||||
return standard_reply("Exchange_connection_result", r_data)
|
return standard_reply("Exchange_connection_result", r_data)
|
||||||
|
|
||||||
|
# Handle backtest operations
|
||||||
|
if msg_type == 'submit_backtest':
|
||||||
|
user_id = self.get_user_info(user_name=msg_data['user_name'], info='User_id')
|
||||||
|
# Pass socket_conn to the backtest handler
|
||||||
|
result = self.backtester.handle_backtest_message(user_id, msg_data, socket_conn)
|
||||||
|
return standard_reply("backtest_submitted", result)
|
||||||
|
|
||||||
|
if msg_type == 'delete_backtest':
|
||||||
|
self.delete_backtest(msg_data)
|
||||||
|
return standard_reply("backtest_deleted", {})
|
||||||
|
|
||||||
if msg_type == 'reply':
|
if msg_type == 'reply':
|
||||||
# If the message is a reply log the response to the terminal.
|
# If the message is a reply log the response to the terminal.
|
||||||
print(f"\napp.py:Received reply: {msg_data}")
|
print(f"\napp.py:Received reply: {msg_data}")
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from typing import Any, Tuple, List, Optional
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import json
|
|
||||||
|
|
||||||
from indicators import Indicator, indicators_registry
|
from indicators import Indicator, indicators_registry
|
||||||
from shared_utilities import unix_time_millis
|
from shared_utilities import unix_time_millis
|
||||||
|
|
|
||||||
|
|
@ -562,7 +562,8 @@ class UserIndicatorManagement(UserExchangeManagement):
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'{specific_property} is not a specific property of chart_views')
|
raise ValueError(f'{specific_property} is not a specific property of chart_views')
|
||||||
|
|
||||||
self.modify_user_data(username=user_name, field_name='chart_views', new_data=chart_view)
|
chart_view_str = json.dumps(chart_view)
|
||||||
|
self.modify_user_data(username=user_name, field_name='chart_views', new_data=chart_view_str)
|
||||||
|
|
||||||
|
|
||||||
class Users(UserIndicatorManagement):
|
class Users(UserIndicatorManagement):
|
||||||
|
|
|
||||||
|
|
@ -129,8 +129,8 @@ def ws(socket_conn):
|
||||||
socket_conn.send(json.dumps({"success": False, "message": "User not logged in"}))
|
socket_conn.send(json.dumps({"success": False, "message": "User not logged in"}))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Process the incoming message based on the type
|
# Process the incoming message based on the type, passing socket_conn
|
||||||
resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data)
|
resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data, socket_conn=socket_conn)
|
||||||
|
|
||||||
# Send the response back to the client
|
# Send the response back to the client
|
||||||
if resp:
|
if resp:
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,312 @@
|
||||||
from dataclasses import dataclass, field, asdict
|
import ast
|
||||||
from itertools import count
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
import backtrader as bt
|
||||||
@dataclass
|
import datetime as dt
|
||||||
class Order:
|
from DataCache_v3 import DataCache
|
||||||
symbol: str
|
from Strategies import Strategies
|
||||||
clientOrderId: hex
|
import threading
|
||||||
transactTime: float
|
import numpy as np
|
||||||
price: float
|
|
||||||
origQty: float
|
|
||||||
executedQty: float
|
|
||||||
cummulativeQuoteQty: float
|
|
||||||
status: str
|
|
||||||
timeInForce: str
|
|
||||||
type: str
|
|
||||||
side: str
|
|
||||||
orderId: int = field(default_factory=count().__next__)
|
|
||||||
|
|
||||||
|
|
||||||
class Backtester:
|
class Backtester:
|
||||||
def __init__(self):
|
def __init__(self, data_cache: DataCache, strategies: Strategies):
|
||||||
self.orders = []
|
""" Initialize the Backtesting class with a cache for back-tests """
|
||||||
|
self.data_cache = data_cache
|
||||||
|
self.strategies = strategies
|
||||||
|
# Create a cache for storing back-tests
|
||||||
|
self.data_cache.create_cache('tests', cache_type='row', size_limit=100,
|
||||||
|
default_expiration=dt.timedelta(days=1),
|
||||||
|
eviction_policy='evict')
|
||||||
|
|
||||||
def create_test_order(self, symbol, side, type, timeInForce, quantity, price=None, newClientOrderId=None):
|
def get_default_chart_view(self, user_name):
|
||||||
order = Order(symbol=symbol, clientOrderId=newClientOrderId, transactTime=1507725176595, price=price,
|
"""Fetch default chart view if no specific source is provided."""
|
||||||
origQty=quantity, executedQty=quantity, cummulativeQuoteQty=(quantity * price), status='FILLED',
|
return self.data_cache.get_datacache_item(
|
||||||
timeInForce=timeInForce, type=type, side=side)
|
item_name='chart_view', cache_name='users', filter_vals=('user_name', user_name))
|
||||||
self.orders.append(order)
|
|
||||||
return asdict(order)
|
|
||||||
|
|
||||||
def create_order(self, symbol, side, type, timeInForce, quantity, price=None, newClientOrderId=None):
|
def cache_backtest(self, user_name, backtest_name, backtest_data):
|
||||||
order = Order(symbol=symbol, clientOrderId=newClientOrderId, transactTime=1507725176595, price=price,
|
""" Cache the backtest data for a user """
|
||||||
origQty=quantity, executedQty=quantity, cummulativeQuoteQty=(quantity * price), status='FILLED',
|
columns = ('user_name', 'strategy_name', 'start_time', 'capital', 'commission', 'results')
|
||||||
timeInForce=timeInForce, type=type, side=side)
|
values = (
|
||||||
self.orders.append(order)
|
backtest_data.get('user_name'),
|
||||||
return asdict(order)
|
backtest_data.get('strategy'),
|
||||||
|
backtest_data.get('start_date'),
|
||||||
|
backtest_data.get('capital', 10000), # Default capital if not provided
|
||||||
|
backtest_data.get('commission', 0.001), # Default commission
|
||||||
|
None # No results yet; will be filled in after backtest completion
|
||||||
|
)
|
||||||
|
cache_key = f"backtest:{user_name}:{backtest_name}"
|
||||||
|
self.data_cache.insert_row_into_cache('tests', columns, values, key=cache_key)
|
||||||
|
|
||||||
def get_order(self, symbol, orderId):
|
def map_user_strategy(self, user_strategy):
|
||||||
for order in self.orders:
|
"""Maps user strategy details into a Backtrader-compatible strategy class."""
|
||||||
if order.symbol == symbol:
|
|
||||||
if order.orderId == orderId:
|
|
||||||
return asdict(order)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_precision(self, symbol=None):
|
class MappedStrategy(bt.Strategy):
|
||||||
return 3
|
params = (
|
||||||
|
('initial_cash', user_strategy['params'].get('initial_cash', 10000)),
|
||||||
|
('commission', user_strategy['params'].get('commission', 0.001)),
|
||||||
|
)
|
||||||
|
|
||||||
def get_min_notional_qty(self, symbol=None):
|
def __init__(self):
|
||||||
return 10
|
# Extract unique sources (exchange, symbol, timeframe) from blocks
|
||||||
|
self.sources = self.extract_sources(user_strategy)
|
||||||
|
|
||||||
def get_min_qty(self, symbol=None):
|
# Map of source to data feed (used later in next())
|
||||||
return 0.001
|
self.source_data_feed_map = {}
|
||||||
|
|
||||||
|
def extract_sources(self, user_strategy):
|
||||||
|
"""Extracts unique sources from the strategy."""
|
||||||
|
sources = []
|
||||||
|
for block in user_strategy.get('blocks', []):
|
||||||
|
if block.get('type') in ['last_candle_value', 'trade_action']:
|
||||||
|
source = self.extract_source_from_block(block)
|
||||||
|
if source and source not in sources:
|
||||||
|
sources.append(source)
|
||||||
|
elif block.get('type') == 'target_market':
|
||||||
|
target_source = self.extract_target_market(block)
|
||||||
|
if target_source and target_source not in sources:
|
||||||
|
sources.append(target_source)
|
||||||
|
return sources
|
||||||
|
|
||||||
|
def extract_source_from_block(self, block):
|
||||||
|
"""Extract source (exchange, symbol, timeframe) from a strategy block."""
|
||||||
|
source = {}
|
||||||
|
if block.get('type') == 'last_candle_value':
|
||||||
|
source = block.get('SOURCE', None)
|
||||||
|
# If SOURCE is missing, use the trade target or default
|
||||||
|
if not source:
|
||||||
|
source = self.get_default_chart_view(self.user_name) # Fallback to default
|
||||||
|
return source
|
||||||
|
|
||||||
|
def extract_target_market(self, block):
|
||||||
|
"""Extracts target market data (timeframe, exchange, symbol) from the trade_action block."""
|
||||||
|
target_market = block.get('target_market', {})
|
||||||
|
return {
|
||||||
|
'timeframe': target_market.get('TF', '5m'),
|
||||||
|
'exchange': target_market.get('EXC', 'Binance'),
|
||||||
|
'symbol': target_market.get('SYM', 'BTCUSD')
|
||||||
|
}
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
"""Execute trading logic using the compiled strategy."""
|
||||||
|
try:
|
||||||
|
exec(self.compiled_logic, {'self': self, 'data_feeds': self.source_data_feed_map})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error executing trading logic: {e}")
|
||||||
|
|
||||||
|
return MappedStrategy
|
||||||
|
|
||||||
|
def prepare_data_feed(self, start_date: str, sources: list, user_name: str):
|
||||||
|
"""
|
||||||
|
Prepare multiple data feeds based on the start date and list of sources.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Convert the start date to a datetime object
|
||||||
|
start_dt = dt.datetime.strptime(start_date, '%Y-%m-%dT%H:%M')
|
||||||
|
|
||||||
|
# Dictionary to map each source to its corresponding data feed
|
||||||
|
data_feeds = {}
|
||||||
|
|
||||||
|
for source in sources:
|
||||||
|
# Ensure exchange details contain required keys (fallback if missing)
|
||||||
|
asset = source.get('asset', 'BTCUSD')
|
||||||
|
timeframe = source.get('timeframe', '5m')
|
||||||
|
exchange = source.get('exchange', 'Binance')
|
||||||
|
|
||||||
|
# Fetch OHLC data from DataCache based on the source
|
||||||
|
ex_details = [asset, timeframe, exchange, user_name]
|
||||||
|
data = self.data_cache.get_records_since(start_dt, ex_details)
|
||||||
|
|
||||||
|
# Return the data as a Pandas DataFrame compatible with Backtrader
|
||||||
|
data_feeds[tuple(ex_details)] = data
|
||||||
|
|
||||||
|
return data_feeds
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error preparing data feed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run_backtest(self, strategy, data_feed_map, msg_data, user_name, callback, socket_conn):
|
||||||
|
"""
|
||||||
|
Runs a backtest using Backtrader on a separate thread and calls the callback with the results when finished.
|
||||||
|
Also sends progress updates to the client via WebSocket.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute_backtest():
|
||||||
|
cerebro = bt.Cerebro()
|
||||||
|
|
||||||
|
# Add the mapped strategy to the backtest
|
||||||
|
cerebro.addstrategy(strategy)
|
||||||
|
|
||||||
|
# Add all the data feeds to Cerebro
|
||||||
|
total_bars = 0 # Total number of data points (bars) across all feeds
|
||||||
|
for source, data_feed in data_feed_map.items():
|
||||||
|
bt_feed = bt.feeds.PandasData(dataname=data_feed)
|
||||||
|
cerebro.adddata(bt_feed)
|
||||||
|
strategy.source_data_feed_map[source] = bt_feed
|
||||||
|
total_bars = max(total_bars, len(data_feed)) # Get the total bars from the largest feed
|
||||||
|
|
||||||
|
# Capture initial capital
|
||||||
|
initial_capital = cerebro.broker.getvalue()
|
||||||
|
|
||||||
|
# Progress tracking variables
|
||||||
|
current_bar = 0
|
||||||
|
last_progress = 0
|
||||||
|
|
||||||
|
# Custom next function to track progress (if you have a large dataset)
|
||||||
|
def track_progress():
|
||||||
|
nonlocal current_bar, last_progress
|
||||||
|
current_bar += 1
|
||||||
|
progress = (current_bar / total_bars) * 100
|
||||||
|
|
||||||
|
# Send progress update every 10% increment
|
||||||
|
if progress >= last_progress + 10:
|
||||||
|
last_progress += 10
|
||||||
|
socket_conn.send(json.dumps({"progress": int(last_progress)}))
|
||||||
|
|
||||||
|
# Attach the custom next method to the strategy
|
||||||
|
strategy.next = track_progress
|
||||||
|
|
||||||
|
# Run the backtest
|
||||||
|
print("Running backtest...")
|
||||||
|
start_time = dt.datetime.now()
|
||||||
|
cerebro.run()
|
||||||
|
end_time = dt.datetime.now()
|
||||||
|
|
||||||
|
# Extract performance metrics
|
||||||
|
final_value = cerebro.broker.getvalue()
|
||||||
|
run_duration = (end_time - start_time).total_seconds()
|
||||||
|
|
||||||
|
# Send 100% completion
|
||||||
|
socket_conn.send(json.dumps({"progress": 100}))
|
||||||
|
|
||||||
|
# Prepare the results to pass into the callback
|
||||||
|
callback({
|
||||||
|
"initial_capital": initial_capital,
|
||||||
|
"final_portfolio_value": final_value,
|
||||||
|
"run_duration": run_duration
|
||||||
|
})
|
||||||
|
|
||||||
|
# Map the user strategy and prepare the data feeds
|
||||||
|
sources = strategy.extract_sources()
|
||||||
|
data_feed_map = self.prepare_data_feed(msg_data['start_date'], sources, user_name)
|
||||||
|
|
||||||
|
# Run the backtest in a separate thread
|
||||||
|
thread = threading.Thread(target=execute_backtest)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def handle_backtest_message(self, user_id, msg_data, socket_conn):
|
||||||
|
user_name = msg_data.get('user_name')
|
||||||
|
backtest_name = f"{msg_data['strategy']}_backtest"
|
||||||
|
|
||||||
|
# Cache the backtest data
|
||||||
|
self.cache_backtest(user_name, backtest_name, msg_data)
|
||||||
|
|
||||||
|
# Fetch the strategy using user_id and strategy_name
|
||||||
|
strategy_name = msg_data.get('strategy')
|
||||||
|
user_strategy = self.strategies.get_strategy_by_name(user_id=user_id, name=strategy_name)
|
||||||
|
|
||||||
|
if not user_strategy:
|
||||||
|
return {"error": f"Strategy {strategy_name} not found for user {user_name}"}
|
||||||
|
|
||||||
|
# Extract sources from the strategy JSON
|
||||||
|
sources = self.extract_sources_from_strategy_json(user_strategy.get('strategy_json'))
|
||||||
|
|
||||||
|
if not sources:
|
||||||
|
return {"error": "No valid sources found in the strategy."}
|
||||||
|
|
||||||
|
# Prepare the data feed map based on extracted sources
|
||||||
|
data_feed_map = self.prepare_data_feed(msg_data['start_date'], sources, user_name)
|
||||||
|
|
||||||
|
if data_feed_map is None:
|
||||||
|
return {"error": "Data feed could not be prepared. Please check the data source."}
|
||||||
|
|
||||||
|
# Map the user strategy to a Backtrader strategy class
|
||||||
|
mapped_strategy = self.map_user_strategy(user_strategy)
|
||||||
|
|
||||||
|
# Define the callback function to handle backtest completion
|
||||||
|
def backtest_callback(results):
|
||||||
|
self.store_backtest_results(user_name, backtest_name, results)
|
||||||
|
self.update_strategy_stats(user_id, strategy_name, results)
|
||||||
|
|
||||||
|
# Run the backtest and pass the callback function, msg_data, and user_name
|
||||||
|
self.run_backtest(mapped_strategy, data_feed_map, msg_data, user_name, backtest_callback, socket_conn)
|
||||||
|
|
||||||
|
return {"reply": "backtest_started"}
|
||||||
|
|
||||||
|
def extract_sources_from_strategy_json(self, strategy_json):
|
||||||
|
sources = []
|
||||||
|
|
||||||
|
# Parse the JSON strategy to extract sources
|
||||||
|
def traverse_blocks(blocks):
|
||||||
|
for block in blocks:
|
||||||
|
if block['type'] == 'source':
|
||||||
|
source = {
|
||||||
|
'timeframe': block['fields'].get('TF'),
|
||||||
|
'exchange': block['fields'].get('EXC'),
|
||||||
|
'symbol': block['fields'].get('SYM')
|
||||||
|
}
|
||||||
|
sources.append(source)
|
||||||
|
# Recursively traverse inputs and statements
|
||||||
|
if 'inputs' in block:
|
||||||
|
traverse_blocks(block['inputs'].values())
|
||||||
|
if 'statements' in block:
|
||||||
|
traverse_blocks(block['statements'].values())
|
||||||
|
if 'next' in block:
|
||||||
|
traverse_blocks([block['next']])
|
||||||
|
|
||||||
|
traverse_blocks(strategy_json)
|
||||||
|
return sources
|
||||||
|
|
||||||
|
def update_strategy_stats(self, user_id, strategy_name, results):
|
||||||
|
""" Update the strategy stats with the backtest results """
|
||||||
|
strategy = self.strategies.get_strategy_by_name(user_id=user_id, name=strategy_name)
|
||||||
|
|
||||||
|
if strategy:
|
||||||
|
initial_capital = results['initial_capital']
|
||||||
|
final_value = results['final_portfolio_value']
|
||||||
|
returns = np.array(results['returns'])
|
||||||
|
equity_curve = np.array(results['equity_curve'])
|
||||||
|
trades = results['trades']
|
||||||
|
|
||||||
|
total_return = (final_value - initial_capital) / initial_capital * 100
|
||||||
|
|
||||||
|
risk_free_rate = 0.0
|
||||||
|
mean_return = np.mean(returns)
|
||||||
|
std_return = np.std(returns)
|
||||||
|
sharpe_ratio = (mean_return - risk_free_rate) / std_return if std_return != 0 else 0
|
||||||
|
|
||||||
|
running_max = np.maximum.accumulate(equity_curve)
|
||||||
|
drawdowns = (equity_curve - running_max) / running_max
|
||||||
|
max_drawdown = np.min(drawdowns) * 100
|
||||||
|
|
||||||
|
num_trades = len(trades)
|
||||||
|
wins = sum(1 for trade in trades if trade['profit'] > 0)
|
||||||
|
losses = num_trades - wins
|
||||||
|
win_loss_ratio = wins / losses if losses != 0 else wins
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'total_return': total_return,
|
||||||
|
'sharpe_ratio': sharpe_ratio,
|
||||||
|
'max_drawdown': max_drawdown,
|
||||||
|
'number_of_trades': num_trades,
|
||||||
|
'win_loss_ratio': win_loss_ratio,
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy.update_stats(stats)
|
||||||
|
else:
|
||||||
|
print(f"Strategy {strategy_name} not found for user {user_id}.")
|
||||||
|
|
||||||
|
def store_backtest_results(self, user_name, backtest_name, results):
|
||||||
|
""" Store the backtest results in the cache """
|
||||||
|
cache_key = f"backtest:{user_name}:{backtest_name}"
|
||||||
|
|
||||||
|
filter_vals = [('tbl_key', cache_key)]
|
||||||
|
backtest_data = self.data_cache.get_rows_from_cache('tests', filter_vals)
|
||||||
|
|
||||||
|
if not backtest_data.empty:
|
||||||
|
backtest_data['results'] = results
|
||||||
|
self.data_cache.insert_row_into_cache('tests', backtest_data.keys(), backtest_data.values(), key=cache_key)
|
||||||
|
else:
|
||||||
|
print(f"Backtest {backtest_name} not found in cache.")
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,7 @@ class MACD(Indicator):
|
||||||
self.properties.setdefault('fast_p', 12)
|
self.properties.setdefault('fast_p', 12)
|
||||||
self.properties.setdefault('slow_p', 26)
|
self.properties.setdefault('slow_p', 26)
|
||||||
self.properties.setdefault('signal_p', 9)
|
self.properties.setdefault('signal_p', 9)
|
||||||
|
|
||||||
self.properties.setdefault('color_1', generate_random_color()) # Upper band
|
self.properties.setdefault('color_1', generate_random_color()) # Upper band
|
||||||
self.properties.setdefault('color_2', generate_random_color()) # Middle band
|
self.properties.setdefault('color_2', generate_random_color()) # Middle band
|
||||||
self.properties.setdefault('color_3', generate_random_color()) # Lower band
|
self.properties.setdefault('color_3', generate_random_color()) # Lower band
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,190 @@
|
||||||
|
|
||||||
class Backtesting {
|
class Backtesting {
|
||||||
constructor() {
|
constructor(ui) {
|
||||||
this.height = height;
|
this.ui = ui;
|
||||||
}
|
this.comms = ui.data.comms;
|
||||||
|
this.tests = []; // Stores the list of saved backtests
|
||||||
|
this.target_id = 'backtest_display'; // The container to display backtests
|
||||||
|
|
||||||
|
// Register handlers for backtesting messages
|
||||||
|
this.comms.on('backtest_results', this.handleBacktestResults.bind(this));
|
||||||
|
this.comms.on('progress', this.handleProgress.bind(this));
|
||||||
|
this.comms.on('backtests_list', this.handleBacktestsList.bind(this));
|
||||||
|
this.comms.on('backtest_deleted', this.handleBacktestDeleted.bind(this));
|
||||||
|
this.comms.on('updates', this.handleUpdates.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBacktestResults(data) {
|
||||||
|
console.log("Backtest results received:", data.results);
|
||||||
|
// Logic to stop running animation and display results
|
||||||
|
this.stopRunningAnimation(data.results);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleProgress(data) {
|
||||||
|
console.log("Backtest progress:", data.progress);
|
||||||
|
// Logic to update progress bar
|
||||||
|
this.updateProgressBar(data.progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBacktestsList(data) {
|
||||||
|
console.log("Backtests list received:", data.tests);
|
||||||
|
// Logic to update backtesting UI
|
||||||
|
this.set_data(data.tests);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBacktestDeleted(data) {
|
||||||
|
console.log(`Backtest "${data.name}" was successfully deleted.`);
|
||||||
|
// Logic to refresh list of backtests
|
||||||
|
this.fetchSavedTests();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpdates(data) {
|
||||||
|
const { trade_updts } = data;
|
||||||
|
if (trade_updts) {
|
||||||
|
this.ui.trade.update_received(trade_updts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgressBar(progress) {
|
||||||
|
const progressBar = document.getElementById('progress_bar');
|
||||||
|
if (progressBar) {
|
||||||
|
progressBar.style.width = `${progress}%`;
|
||||||
|
progressBar.textContent = `${progress}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showRunningAnimation() {
|
||||||
|
const resultsContainer = document.getElementById('backtest-results');
|
||||||
|
const resultsDisplay = document.getElementById('results_display');
|
||||||
|
const progressContainer = document.getElementById('backtest-progress-container');
|
||||||
|
const progressBar = document.getElementById('progress_bar');
|
||||||
|
|
||||||
|
resultsContainer.style.display = 'none';
|
||||||
|
progressContainer.style.display = 'block';
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
progressBar.textContent = '0%';
|
||||||
|
resultsDisplay.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
displayTestResults(results) {
|
||||||
|
const resultsContainer = document.getElementById('backtest-results');
|
||||||
|
const resultsDisplay = document.getElementById('results_display');
|
||||||
|
|
||||||
|
resultsContainer.style.display = 'block';
|
||||||
|
resultsDisplay.innerHTML = `<pre>${JSON.stringify(results, null, 2)}</pre>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRunningAnimation(results) {
|
||||||
|
const progressContainer = document.getElementById('backtest-progress-container');
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
this.displayTestResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSavedTests() {
|
||||||
|
this.comms.sendToApp('get_backtests', { user_name: this.ui.data.user_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHTML() {
|
||||||
|
let html = '';
|
||||||
|
for (const test of this.tests) {
|
||||||
|
html += `
|
||||||
|
<div class="backtest-item">
|
||||||
|
<button class="delete-button" onclick="this.ui.backtesting.deleteTest('${test.name}')">✘</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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -317,20 +317,22 @@ height: 500px;
|
||||||
border-style:none;
|
border-style:none;
|
||||||
}
|
}
|
||||||
#indicator_output{
|
#indicator_output{
|
||||||
overflow-y: scroll;
|
color: blueviolet;
|
||||||
width: 300px;
|
position: absolute;
|
||||||
height:50px;
|
height: fit-content;
|
||||||
padding: 3px;
|
width: 150px;
|
||||||
border-style: solid;
|
margin-top: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-image: linear-gradient(93deg, #ffffffde, #fffefe29);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chart_controls{
|
#chart_controls{
|
||||||
border-style:none;
|
border-style:none;
|
||||||
width: 775px;
|
width: 600px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:350px 2fr 1fr 1fr;
|
grid-template-columns: 4fr 2fr 2fr 2fr;
|
||||||
|
margin-left: 250;
|
||||||
}
|
}
|
||||||
#indicators{
|
#indicators{
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,13 @@ class Charts {
|
||||||
// - Create the candle stick series for our chart
|
// - Create the candle stick series for our chart
|
||||||
this.candleSeries = this.chart_1.addCandlestickSeries();
|
this.candleSeries = this.chart_1.addCandlestickSeries();
|
||||||
|
|
||||||
//Initialise the candlestick series
|
// Initialize the candlestick series if price_history is available
|
||||||
this.price_history.then((ph) => {
|
if (this.price_history && this.price_history.length > 0) {
|
||||||
//Initialise the candle data
|
this.candleSeries.setData(this.price_history);
|
||||||
this.candleSeries.setData(ph);
|
console.log('Candle series init:', this.price_history);
|
||||||
console.log('Candle series init:', ph)
|
} else {
|
||||||
})
|
console.error('Price history is not available or is empty.');
|
||||||
|
}
|
||||||
this.bind_charts(this.chart_1);
|
this.bind_charts(this.chart_1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,12 @@ class Comms {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.connectionOpen = false;
|
this.connectionOpen = false;
|
||||||
this.appCon = null; // WebSocket connection for app communication
|
this.appCon = null; // WebSocket connection for app communication
|
||||||
|
this.eventHandlers = {}; // Event handlers for message types
|
||||||
|
|
||||||
// Callback collections that will receive various updates.
|
// Callback collections that will receive various updates.
|
||||||
this.candleUpdateCallbacks = [];
|
this.candleUpdateCallbacks = [];
|
||||||
this.candleCloseCallbacks = [];
|
this.candleCloseCallbacks = [];
|
||||||
this.indicatorUpdateCallbacks = [];
|
this.indicatorUpdateCallbacks = [];
|
||||||
|
|
||||||
// Flags
|
|
||||||
this.connectionOpen = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,6 +30,30 @@ class Comms {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an event handler for a specific message type.
|
||||||
|
* @param {string} messageType - The type of the message to handle.
|
||||||
|
* @param {function} handler - The handler function to register.
|
||||||
|
*/
|
||||||
|
on(messageType, handler) {
|
||||||
|
if (!this.eventHandlers[messageType]) {
|
||||||
|
this.eventHandlers[messageType] = [];
|
||||||
|
}
|
||||||
|
this.eventHandlers[messageType].push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit an event to all registered handlers.
|
||||||
|
* @param {string} messageType - The type of the message.
|
||||||
|
* @param {Object} data - The data to pass to the handlers.
|
||||||
|
*/
|
||||||
|
emit(messageType, data) {
|
||||||
|
const handlers = this.eventHandlers[messageType];
|
||||||
|
if (handlers) {
|
||||||
|
handlers.forEach(handler => handler(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Callback declarations */
|
/* Callback declarations */
|
||||||
|
|
||||||
candleUpdate(newCandle) {
|
candleUpdate(newCandle) {
|
||||||
|
|
@ -40,7 +62,7 @@ class Comms {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
candleClose(newCandle) {
|
candleClose(newCandle) {
|
||||||
this.sendToApp('candle_data', newCandle);
|
this.sendToApp('candle_data', newCandle);
|
||||||
|
|
||||||
for (const callback of this.candleCloseCallbacks) {
|
for (const callback of this.candleCloseCallbacks) {
|
||||||
|
|
@ -149,7 +171,7 @@ class Comms {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a request to update an indicator's properties.
|
* Sends a request to update an indicator's properties.
|
||||||
* @param {Object} indicatorData - An object containing the updated properties of the indicator.
|
* @param {Object} indicatorData - An object containing the updated properties of the indicator.
|
||||||
* @returns {Promise<Object>} - The response from the server.
|
* @returns {Promise<Object>} - The response from the server.
|
||||||
|
|
@ -206,7 +228,6 @@ class Comms {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setAppCon() {
|
setAppCon() {
|
||||||
this.appCon = new WebSocket('ws://localhost:5000/ws');
|
this.appCon = new WebSocket('ws://localhost:5000/ws');
|
||||||
|
|
||||||
|
|
@ -228,71 +249,8 @@ class Comms {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message && message.reply !== undefined) {
|
if (message && message.reply !== undefined) {
|
||||||
// Handle different reply types from the server
|
// Emit the event to registered handlers
|
||||||
if (message.reply === 'updates') {
|
this.emit(message.reply, message.data);
|
||||||
const { i_updates, s_updates, stg_updts, trade_updts } = message.data;
|
|
||||||
|
|
||||||
// Handle indicator updates
|
|
||||||
if (i_updates) {
|
|
||||||
this.indicatorUpdate(i_updates);
|
|
||||||
window.UI.signals.i_update(i_updates);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle signal updates
|
|
||||||
if (s_updates) {
|
|
||||||
window.UI.signals.update_signal_states(s_updates);
|
|
||||||
window.UI.alerts.publish_alerts('signal_changes', s_updates);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle strategy updates
|
|
||||||
if (stg_updts) {
|
|
||||||
window.UI.strats.update_received(stg_updts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle trade updates
|
|
||||||
if (trade_updts) {
|
|
||||||
window.UI.trade.update_received(trade_updts);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (message.reply === 'signals') {
|
|
||||||
window.UI.signals.set_data(message.data);
|
|
||||||
|
|
||||||
} else if (message.reply === 'strategies') {
|
|
||||||
window.UI.strats.set_data(message.data);
|
|
||||||
|
|
||||||
} else if (message.reply === 'trades') {
|
|
||||||
window.UI.trade.set_data(message.data);
|
|
||||||
|
|
||||||
} else if (message.reply === 'signal_created') {
|
|
||||||
const list_of_one = [message.data];
|
|
||||||
window.UI.signals.set_data(list_of_one);
|
|
||||||
|
|
||||||
} else if (message.reply === 'trade_created') {
|
|
||||||
const list_of_one = [message.data];
|
|
||||||
window.UI.trade.set_data(list_of_one);
|
|
||||||
|
|
||||||
} else if (message.reply === 'Exchange_connection_result') {
|
|
||||||
window.UI.exchanges.postConnection(message.data);
|
|
||||||
|
|
||||||
} else if (message.reply === 'strategy_created') {
|
|
||||||
// Handle the strategy creation response
|
|
||||||
if (message.data.success) {
|
|
||||||
// Success - Notify the user and update the UI
|
|
||||||
alert(message.data.message); // Display a success message
|
|
||||||
console.log("New strategy data:", message.data); // Log or handle the new strategy data
|
|
||||||
|
|
||||||
// Optionally, refresh the list of strategies
|
|
||||||
window.UI.strats.fetchSavedStrategies();
|
|
||||||
} else {
|
|
||||||
// Failure - Notify the user of the error
|
|
||||||
alert(`Error: ${message.data.message}`);
|
|
||||||
console.error("Strategy creation error:", message.data.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.log(message.reply);
|
|
||||||
console.log(message.data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -309,8 +267,6 @@ class Comms {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up a WebSocket connection to the exchange for receiving candlestick data.
|
* Sets up a WebSocket connection to the exchange for receiving candlestick data.
|
||||||
* @param {string} interval - The interval of the candlestick data.
|
* @param {string} interval - The interval of the candlestick data.
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
@ -19,38 +19,49 @@ class Data {
|
||||||
/* Comms handles communication with the servers. Register
|
/* Comms handles communication with the servers. Register
|
||||||
callbacks to handle various incoming messages.*/
|
callbacks to handle various incoming messages.*/
|
||||||
this.comms = new Comms();
|
this.comms = new Comms();
|
||||||
this.comms.registerCallback('candle_update', this.candle_update)
|
|
||||||
this.comms.registerCallback('candle_close', this.candle_close)
|
|
||||||
this.comms.registerCallback('indicator_update', this.indicator_update)
|
|
||||||
// Open the connection to our local server.
|
|
||||||
this.comms.setAppCon();
|
|
||||||
/* Open connection for streaming candle data wth the exchange.
|
|
||||||
Pass it the time period of candles to stream. */
|
|
||||||
this.comms.setExchangeCon(this.interval, this.trading_pair);
|
|
||||||
|
|
||||||
//Request historical price data from the server.
|
// Initialize other properties
|
||||||
this.price_history = this.comms.getPriceHistory(this.user_name);
|
this.price_history = null;
|
||||||
|
this.indicator_data = null;
|
||||||
// Last price from price history.
|
this.last_price = null;
|
||||||
this.price_history.then((value) => {
|
|
||||||
if (value && value.length > 0) {
|
|
||||||
this.last_price = value[value.length - 1].close;
|
|
||||||
} else {
|
|
||||||
console.error('Received empty price history data');
|
|
||||||
this.last_price = null;
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error('Error processing price history:', error);
|
|
||||||
this.last_price = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Request from the server initialization data for the indicators.
|
|
||||||
this.indicator_data = this.comms.getIndicatorData(this.user_name);
|
|
||||||
|
|
||||||
// Call back for indicator updates.
|
|
||||||
this.i_updates = null;
|
this.i_updates = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the Data instance by setting up connections and fetching data.
|
||||||
|
* Should be called after creating a new instance of Data.
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
// Register callbacks
|
||||||
|
this.comms.registerCallback('candle_update', this.candle_update.bind(this));
|
||||||
|
this.comms.registerCallback('candle_close', this.candle_close.bind(this));
|
||||||
|
this.comms.registerCallback('indicator_update', this.indicator_update.bind(this));
|
||||||
|
|
||||||
|
// Open the connection to your local server
|
||||||
|
this.comms.setAppCon();
|
||||||
|
|
||||||
|
// Open connection for streaming candle data with the exchange
|
||||||
|
this.comms.setExchangeCon(this.interval, this.trading_pair);
|
||||||
|
|
||||||
|
// Request historical price data from the server
|
||||||
|
try {
|
||||||
|
this.price_history = await this.comms.getPriceHistory(this.user_name);
|
||||||
|
if (this.price_history && this.price_history.length > 0) {
|
||||||
|
this.last_price = this.price_history[this.price_history.length - 1].close;
|
||||||
|
} else {
|
||||||
|
console.error('Received empty price history data');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching price history:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request indicator data from the server
|
||||||
|
try {
|
||||||
|
this.indicator_data = await this.comms.getIndicatorData(this.user_name);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching indicator data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
candle_update(new_candle){
|
candle_update(new_candle){
|
||||||
// This is called everytime a candle update comes from the local server.
|
// This is called everytime a candle update comes from the local server.
|
||||||
window.UI.charts.update_main_chart(new_candle);
|
window.UI.charts.update_main_chart(new_candle);
|
||||||
|
|
@ -60,11 +71,17 @@ class Data {
|
||||||
registerCallback_i_updates(call_back){
|
registerCallback_i_updates(call_back){
|
||||||
this.i_updates = call_back;
|
this.i_updates = call_back;
|
||||||
}
|
}
|
||||||
indicator_update(data){
|
|
||||||
// This is called everytime an indicator update come in.
|
// This is called everytime an indicator update come in.
|
||||||
window.UI.data.i_updates(data);
|
indicator_update(data) {
|
||||||
|
if (typeof this.i_updates === 'function') {
|
||||||
|
this.i_updates(data);
|
||||||
|
} else {
|
||||||
|
console.warn('No indicator update callback registered.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
candle_close(new_candle){
|
candle_close(new_candle){
|
||||||
// This is called everytime a candle closes.
|
// This is called everytime a candle closes.
|
||||||
//console.log('Candle close:');
|
//console.log('Candle close:');
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
class User_Interface {
|
class User_Interface {
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialize all components needed by the user interface
|
// Initialize all components needed by the user interface
|
||||||
this.strats = new Strategies('strats_display');
|
this.strats = new Strategies();
|
||||||
this.exchanges = new Exchanges();
|
this.exchanges = new Exchanges();
|
||||||
this.data = new Data();
|
this.data = new Data();
|
||||||
this.controls = new Controls();
|
this.controls = new Controls();
|
||||||
this.signals = new Signals(this.data.indicators);
|
|
||||||
this.alerts = new Alerts("alert_list");
|
this.alerts = new Alerts("alert_list");
|
||||||
this.trade = new Trade();
|
this.trade = new Trade();
|
||||||
this.users = new Users();
|
this.users = new Users();
|
||||||
this.indicators = new Indicators(this.data.comms);
|
this.indicators = new Indicators(this.data.comms);
|
||||||
|
this.signals = new Signals(this);
|
||||||
|
this.backtesting = new Backtesting(this);
|
||||||
|
|
||||||
// Register a callback function for when indicator updates are received from the data object
|
// Register a callback function for when indicator updates are received from the data object
|
||||||
this.data.registerCallback_i_updates(this.indicators.update);
|
this.data.registerCallback_i_updates(this.indicators.update);
|
||||||
|
|
@ -18,10 +19,18 @@ class User_Interface {
|
||||||
this.initializeAll();
|
this.initializeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeAll() {
|
async initializeAll() {
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', async () => {
|
||||||
this.initializeChartsAndIndicators();
|
try {
|
||||||
this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats));
|
await this.data.initialize(); // Wait for initialization
|
||||||
|
this.initializeChartsAndIndicators();
|
||||||
|
|
||||||
|
// Initialize other UI components here
|
||||||
|
this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats), "draggable_header", "resize-strategy");
|
||||||
|
this.initializeResizablePopup("backtest_form", null, "backtest_draggable_header", "resize-backtest");
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Initialization failed:', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,26 +55,30 @@ class User_Interface {
|
||||||
this.controls.init_TP_selector();
|
this.controls.init_TP_selector();
|
||||||
this.trade.initialize();
|
this.trade.initialize();
|
||||||
this.exchanges.initialize();
|
this.exchanges.initialize();
|
||||||
this.strats.initialize();
|
this.strats.initialize('strats_display', 'new_strat_form', this.data);
|
||||||
|
this.backtesting.fetchSavedTests();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a popup resizable, and optionally pass a resize callback (like for Blockly workspaces).
|
* Make a popup resizable, and optionally pass a resize callback (like for Blockly workspaces).
|
||||||
* @param {string} popupId - The ID of the popup to make resizable.
|
* @param {string} popupId - The ID of the popup to make resizable.
|
||||||
* @param {function|null} resizeCallback - Optional callback to run when resizing (for Blockly or other elements).
|
* @param {function|null} resizeCallback - Optional callback to run when resizing (for Blockly or other elements).
|
||||||
|
* @param {string|null} headerId - The ID of the header to use for dragging, or null to drag the entire element.
|
||||||
|
* @param {string} resizerId - The ID of the resizer handle element.
|
||||||
*/
|
*/
|
||||||
initializeResizablePopup(popupId, resizeCallback = null) {
|
initializeResizablePopup(popupId, resizeCallback = null, headerId = null, resizerId) {
|
||||||
const popupElement = document.getElementById(popupId);
|
const popupElement = document.getElementById(popupId);
|
||||||
this.dragElement(popupElement);
|
this.dragElement(popupElement, headerId);
|
||||||
this.makeResizable(popupElement, resizeCallback);
|
this.makeResizable(popupElement, resizeCallback, resizerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an element draggable by dragging its header.
|
* Make an element draggable by dragging its header.
|
||||||
* @param {HTMLElement} elm - The element to make draggable.
|
* @param {HTMLElement} elm - The element to make draggable.
|
||||||
|
* @param {string|null} headerId - The ID of the header to use for dragging, or null to drag the entire element.
|
||||||
*/
|
*/
|
||||||
dragElement(elm) {
|
dragElement(elm, headerId = null) {
|
||||||
const header = document.getElementById("draggable_header");
|
const header = headerId ? document.getElementById(headerId) : elm;
|
||||||
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
|
|
@ -102,9 +115,10 @@ class User_Interface {
|
||||||
* Make an element resizable and optionally call a resize callback (like for Blockly workspace).
|
* Make an element resizable and optionally call a resize callback (like for Blockly workspace).
|
||||||
* @param {HTMLElement} elm - The element to make resizable.
|
* @param {HTMLElement} elm - The element to make resizable.
|
||||||
* @param {function|null} resizeCallback - Optional callback to resize specific content.
|
* @param {function|null} resizeCallback - Optional callback to resize specific content.
|
||||||
|
* @param {string} resizerId - The ID of the resizer handle element.
|
||||||
*/
|
*/
|
||||||
makeResizable(elm, resizeCallback = null) {
|
makeResizable(elm, resizeCallback = null, resizerId) {
|
||||||
const resizer = document.getElementById("resize-br");
|
const resizer = document.getElementById(resizerId);
|
||||||
let originalWidth = 0;
|
let originalWidth = 0;
|
||||||
let originalHeight = 0;
|
let originalHeight = 0;
|
||||||
let originalMouseX = 0;
|
let originalMouseX = 0;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,38 +1,55 @@
|
||||||
class Indicator_Output {
|
class Indicator_Output {
|
||||||
constructor(name) {
|
constructor(name) {
|
||||||
this.legend={};
|
this.legend = {};
|
||||||
}
|
}
|
||||||
create_legend(name, chart, lineSeries){
|
create_legend(name, chart, lineSeries) {
|
||||||
// Create legend div and append it to the output element
|
// Create legend div and append it to the output element
|
||||||
let target_div = document.getElementById('indicator_output');
|
let target_div = document.getElementById('indicator_output');
|
||||||
this.legend[name] = document.createElement('div');
|
this.legend[name] = document.createElement('div');
|
||||||
this.legend[name].className = 'legend';
|
this.legend[name].className = 'legend';
|
||||||
|
this.legend[name].style.opacity = 0.1; // Initially mostly transparent
|
||||||
|
this.legend[name].style.transition = 'opacity 1s ease-out'; // Smooth transition for fade-out
|
||||||
target_div.appendChild(this.legend[name]);
|
target_div.appendChild(this.legend[name]);
|
||||||
this.legend[name].style.display = 'block';
|
this.legend[name].style.display = 'block';
|
||||||
this.legend[name].style.left = 3 + 'px';
|
this.legend[name].style.left = 3 + 'px';
|
||||||
this.legend[name].style.top = 3 + 'px';
|
this.legend[name].style.top = 3 + 'px';
|
||||||
// subscribe set legend text to crosshair moves
|
// subscribe set legend text to crosshair moves
|
||||||
chart.subscribeCrosshairMove((param) => {
|
chart.subscribeCrosshairMove((param) => {
|
||||||
this.set_legend_text(param.seriesPrices.get(lineSeries),name);
|
this.set_legend_text(param.seriesPrices.get(lineSeries), name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_legend_text(priceValue, name) {
|
||||||
set_legend_text(priceValue,name) {
|
|
||||||
// Callback assigned to fire on crosshair movements.
|
// Callback assigned to fire on crosshair movements.
|
||||||
let val = 'n/a';
|
let val = 'n/a';
|
||||||
if (priceValue !== undefined) {
|
if (priceValue !== undefined) {
|
||||||
val = (Math.round(priceValue * 100) / 100).toFixed(2);
|
val = (Math.round(priceValue * 100) / 100).toFixed(2);
|
||||||
}
|
}
|
||||||
this.legend[name].innerHTML = name + ' <span style="color:rgba(4, 111, 232, 1)">' + val + '</span>';
|
|
||||||
|
// Update legend text
|
||||||
|
this.legend[name].innerHTML = `${name} <span style="color:rgba(4, 111, 232, 1)">${val}</span>`;
|
||||||
|
|
||||||
|
// Make legend fully visible
|
||||||
|
this.legend[name].style.opacity = 1;
|
||||||
|
this.legend[name].style.display = 'block';
|
||||||
|
|
||||||
|
// Set a timeout to fade out the legend after 3 seconds
|
||||||
|
clearTimeout(this.legend[name].fadeTimeout); // Clear any previous timeout to prevent conflicts
|
||||||
|
this.legend[name].fadeTimeout = setTimeout(() => {
|
||||||
|
this.legend[name].style.opacity = 0.1; // Gradually fade out
|
||||||
|
// Set another timeout to hide the element after the fade-out transition
|
||||||
|
setTimeout(() => {
|
||||||
|
this.legend[name].style.display = 'none';
|
||||||
|
}, 1000); // Wait for the fade-out transition to complete (1s)
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
clear_legend(name) {
|
clear_legend(name) {
|
||||||
// Remove the legend div from the DOM
|
// Remove the legend div from the DOM
|
||||||
if (this.legend[name]) {
|
for (const key in this.legend) {
|
||||||
this.legend[name].remove(); // Remove the legend from the DOM
|
if (key.startsWith(name)) {
|
||||||
delete this.legend[name]; // Remove the reference from the object
|
this.legend[key].remove(); // Remove the legend from the DOM
|
||||||
} else {
|
delete this.legend[key]; // Remove the reference from the object
|
||||||
console.warn(`Legend for ${name} not found.`);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,27 +101,71 @@ class Indicator {
|
||||||
color: color,
|
color: color,
|
||||||
lineWidth: lineWidth
|
lineWidth: lineWidth
|
||||||
});
|
});
|
||||||
// Initialise the crosshair legend for the charts.
|
// Initialise the crosshair legend for the charts with a unique name for each line.
|
||||||
iOutput.create_legend(this.name, chart, this.lines[name]);
|
iOutput.create_legend(`${this.name}_${name}`, chart, this.lines[name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLine(name, data, value_name) {
|
setLine(lineName, data, value_name) {
|
||||||
console.log('indicators[68]: setLine takes:(name,data,value_name)');
|
console.log('indicators[68]: setLine takes:(lineName, data, value_name)');
|
||||||
console.log(name, data, value_name);
|
console.log(lineName, data, value_name);
|
||||||
// Initialize the data with the data object provided.
|
|
||||||
this.lines[name].setData(data);
|
let priceValue;
|
||||||
// Isolate the last value provided and round to 2 decimals places.
|
|
||||||
let priceValue = data.at(-1).value;
|
// Check if the data is a multi-value object
|
||||||
this.updateDisplay(name, priceValue, value_name);
|
if (typeof data === 'object' && data !== null && value_name in data) {
|
||||||
// Update indicator output/crosshair legend.
|
// Multi-value indicator: Extract the array for the specific key
|
||||||
iOutput.set_legend_text(data.at(-1).value, this.name);
|
const processedData = data[value_name];
|
||||||
|
|
||||||
|
// Set the data for the line
|
||||||
|
this.lines[lineName].setData(processedData);
|
||||||
|
|
||||||
|
// Isolate the last value provided and round to 2 decimal places
|
||||||
|
priceValue = processedData.at(-1).value;
|
||||||
|
|
||||||
|
// Update the display and legend for multi-value indicators
|
||||||
|
this.updateDisplay(lineName, { [value_name]: priceValue }, 'value');
|
||||||
|
} else {
|
||||||
|
// Single-value indicator: Initialize the data directly
|
||||||
|
this.lines[lineName].setData(data);
|
||||||
|
|
||||||
|
// Isolate the last value provided and round to 2 decimal places
|
||||||
|
priceValue = data.at(-1).value;
|
||||||
|
|
||||||
|
// Update the display and legend for single-value indicators
|
||||||
|
this.updateDisplay(lineName, priceValue, value_name);
|
||||||
|
}
|
||||||
|
iOutput.set_legend_text(priceValue, `${this.name}_${lineName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDisplay(name, priceValue, value_name) {
|
updateDisplay(name, priceValue, value_name) {
|
||||||
// Update the data in the edit and view indicators panel
|
let element = document.getElementById(this.name + '_' + value_name);
|
||||||
let element = document.getElementById(this.name + '_' + value_name)
|
if (element) {
|
||||||
if (element){
|
if (typeof priceValue === 'object' && priceValue !== null) {
|
||||||
element.value = (Math.round(priceValue * 100) / 100).toFixed(2);
|
// Handle multiple values by joining them into a single string with labels
|
||||||
|
let currentValues = element.value ? element.value.split(', ').reduce((acc, pair) => {
|
||||||
|
let [key, val] = pair.split(': ');
|
||||||
|
if (!isNaN(parseFloat(val))) {
|
||||||
|
acc[key] = parseFloat(val);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {}) : {};
|
||||||
|
|
||||||
|
// Update current values with the new key-value pairs
|
||||||
|
Object.assign(currentValues, priceValue);
|
||||||
|
|
||||||
|
// Set the updated values back to the element
|
||||||
|
element.value = Object.entries(currentValues)
|
||||||
|
.filter(([key, value]) => !isNaN(value)) // Skip NaN values
|
||||||
|
.map(([key, value]) => `${key}: ${(Math.round(value * 100) / 100).toFixed(2)}`)
|
||||||
|
.join(', '); // Use comma for formatting
|
||||||
|
} else {
|
||||||
|
// Handle simple values as before
|
||||||
|
element.value = (Math.round(priceValue * 100) / 100).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust the element styling dynamically for wrapping and height
|
||||||
|
element.style.height = 'auto'; // Reset height
|
||||||
|
element.style.height = (element.scrollHeight) + 'px'; // Adjust height based on content
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Element with ID ${this.name}_${value_name} not found.`);
|
console.warn(`Element with ID ${this.name}_${value_name} not found.`);
|
||||||
}
|
}
|
||||||
|
|
@ -115,12 +176,34 @@ class Indicator {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLine(name, data, value_name) {
|
updateLine(name, data, value_name) {
|
||||||
// Update the line-set data in the chart
|
console.log('indicators[68]: updateLine takes:(name, data, value_name)');
|
||||||
this.lines[name].update(data);
|
console.log(name, data, value_name);
|
||||||
// Update indicator output/crosshair legend.
|
|
||||||
iOutput.set_legend_text(data.value, this.name);
|
// Check if the data is a multi-value object
|
||||||
// Update the data in the edit and view indicators panel
|
if (typeof data === 'object' && data !== null && value_name in data) {
|
||||||
this.updateDisplay(name, data.value, value_name);
|
// Multi-value indicator: Extract the array for the specific key
|
||||||
|
const processedData = data[value_name];
|
||||||
|
|
||||||
|
// Update the line-set data in the chart
|
||||||
|
this.lines[name].update(processedData);
|
||||||
|
|
||||||
|
// Isolate the last value provided and round to 2 decimal places
|
||||||
|
const priceValue = processedData.at(-1).value;
|
||||||
|
|
||||||
|
// Update the display and legend for multi-value indicators
|
||||||
|
this.updateDisplay(name, { [value_name]: priceValue }, 'value');
|
||||||
|
iOutput.set_legend_text(priceValue, `${this.name}_${name}`);
|
||||||
|
} else {
|
||||||
|
// Single-value indicator: Initialize the data directly
|
||||||
|
this.lines[name].update(data);
|
||||||
|
|
||||||
|
// Isolate the last value provided and round to 2 decimal places
|
||||||
|
const priceValue = data.at(-1).value;
|
||||||
|
|
||||||
|
// Update the display and legend for single-value indicators
|
||||||
|
this.updateDisplay(name, priceValue, value_name);
|
||||||
|
iOutput.set_legend_text(priceValue, `${this.name}_${name}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHist(name, data) {
|
updateHist(name, data) {
|
||||||
|
|
@ -249,19 +332,25 @@ class MACD extends Indicator {
|
||||||
const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null);
|
const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null);
|
||||||
|
|
||||||
if (filteredData.length > 0) {
|
if (filteredData.length > 0) {
|
||||||
// Set the 'line_m' for the MACD line
|
// Prepare the filtered data for the MACD line
|
||||||
this.setLine('line_m', filteredData.map(row => ({
|
const line_m = filteredData.map(row => ({
|
||||||
time: row.time,
|
time: row.time,
|
||||||
value: row.macd
|
value: row.macd
|
||||||
})), 'macd');
|
}));
|
||||||
|
|
||||||
// Set the 'line_s' for the signal line
|
// Set the 'line_m' for the MACD line
|
||||||
this.setLine('line_s', filteredData.map(row => ({
|
this.setLine('line_m', { macd: line_m }, 'macd');
|
||||||
|
|
||||||
|
// Prepare the filtered data for the signal line
|
||||||
|
const line_s = filteredData.map(row => ({
|
||||||
time: row.time,
|
time: row.time,
|
||||||
value: row.signal
|
value: row.signal
|
||||||
})), 'signal');
|
}));
|
||||||
|
|
||||||
// Set the histogram
|
// Set the 'line_s' for the signal line
|
||||||
|
this.setLine('line_s', { signal: line_s }, 'signal');
|
||||||
|
|
||||||
|
// Set the histogram data
|
||||||
this.setHist(this.name, filteredData.map(row => ({
|
this.setHist(this.name, filteredData.map(row => ({
|
||||||
time: row.time,
|
time: row.time,
|
||||||
value: row.hist
|
value: row.hist
|
||||||
|
|
@ -271,15 +360,35 @@ class MACD extends Indicator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
update(data) {
|
update(data) {
|
||||||
// Update the 'macd' line
|
// Filter out rows where macd, signal, or hist are null
|
||||||
this.updateLine('line_m', {time: data[0].time, value: data[0].macd }, 'macd');
|
const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null);
|
||||||
|
|
||||||
// Update the 'signal' line
|
if (filteredData.length > 0) {
|
||||||
this.updateLine('line_s', { time: data[0].time, value: data[0].signal }, 'signal');
|
// Update the 'macd' line
|
||||||
|
const line_m = filteredData.map(row => ({
|
||||||
|
time: row.time,
|
||||||
|
value: row.macd
|
||||||
|
}));
|
||||||
|
this.updateLine('line_m', { macd: line_m }, 'macd');
|
||||||
|
|
||||||
// Update the 'hist' (histogram) bar
|
// Update the 'signal' line
|
||||||
this.updateHist('hist', {time: data[0].time, value: data[0].hist });
|
const line_s = filteredData.map(row => ({
|
||||||
|
time: row.time,
|
||||||
|
value: row.signal
|
||||||
|
}));
|
||||||
|
this.updateLine('line_s', { signal: line_s }, 'signal');
|
||||||
|
|
||||||
|
// Update the 'hist' (histogram) bar
|
||||||
|
const hist_data = filteredData.map(row => ({
|
||||||
|
time: row.time,
|
||||||
|
value: row.hist
|
||||||
|
}));
|
||||||
|
this.updateHist('hist', hist_data);
|
||||||
|
} else {
|
||||||
|
console.error('No valid MACD data found for update.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
indicatorMap.set("MACD", MACD);
|
indicatorMap.set("MACD", MACD);
|
||||||
|
|
@ -341,30 +450,64 @@ class Bolenger extends Indicator {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
init(data) {
|
init(data) {
|
||||||
// Set the 'line_u' for the upper line
|
// Filter out rows where upper, middle, or lower are null
|
||||||
this.setLine('line_u', data.map(row => ({
|
const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null);
|
||||||
time: row.time,
|
|
||||||
value: row.upper
|
|
||||||
})), 'value');
|
|
||||||
|
|
||||||
// Set the 'line_m' for the middle line
|
if (filteredData.length > 0) {
|
||||||
this.setLine('line_m', data.map(row => ({
|
// Set the 'line_u' for the upper line
|
||||||
time: row.time,
|
const line_u = filteredData.map(row => ({
|
||||||
value: row.middle
|
time: row.time,
|
||||||
})), 'value2');
|
value: row.upper
|
||||||
|
}));
|
||||||
|
this.setLine('line_u', { upper: line_u }, 'upper');
|
||||||
|
|
||||||
// Set the 'line_l' for the lower line
|
// Set the 'line_m' for the middle line
|
||||||
this.setLine('line_l', data.map(row => ({
|
const line_m = filteredData.map(row => ({
|
||||||
time: row.time,
|
time: row.time,
|
||||||
value: row.lower
|
value: row.middle
|
||||||
})), 'value3');
|
}));
|
||||||
}
|
this.setLine('line_m', { middle: line_m }, 'middle');
|
||||||
|
|
||||||
|
// Set the 'line_l' for the lower line
|
||||||
|
const line_l = filteredData.map(row => ({
|
||||||
|
time: row.time,
|
||||||
|
value: row.lower
|
||||||
|
}));
|
||||||
|
this.setLine('line_l', { lower: line_l }, 'lower');
|
||||||
|
} else {
|
||||||
|
console.error('No valid data found for init.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
update(data) {
|
update(data) {
|
||||||
this.updateLine('line_u', data[0][0], 'value');
|
// Filter out rows where upper, middle, or lower are null
|
||||||
this.updateLine('line_m', data[1][0], 'value2');
|
const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null);
|
||||||
this.updateLine('line_l', data[2][0], 'value3');
|
|
||||||
|
if (filteredData.length > 0) {
|
||||||
|
// Update the 'upper' line
|
||||||
|
const line_u = filteredData.map(row => ({
|
||||||
|
time: row.time,
|
||||||
|
value: row.upper
|
||||||
|
}));
|
||||||
|
this.updateLine('line_u', { upper: line_u }, 'upper');
|
||||||
|
|
||||||
|
// Update the 'middle' line
|
||||||
|
const line_m = filteredData.map(row => ({
|
||||||
|
time: row.time,
|
||||||
|
value: row.middle
|
||||||
|
}));
|
||||||
|
this.updateLine('line_m', { middle: line_m }, 'middle');
|
||||||
|
|
||||||
|
// Update the 'lower' line
|
||||||
|
const line_l = filteredData.map(row => ({
|
||||||
|
time: row.time,
|
||||||
|
value: row.lower
|
||||||
|
}));
|
||||||
|
this.updateLine('line_l', { lower: line_l }, 'lower');
|
||||||
|
} else {
|
||||||
|
console.error('No valid data found for update.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
indicatorMap.set("BOLBands", Bolenger);
|
indicatorMap.set("BOLBands", Bolenger);
|
||||||
|
|
@ -422,8 +565,12 @@ class Indicators {
|
||||||
objects, then inserts the data into the charts.
|
objects, then inserts the data into the charts.
|
||||||
*/
|
*/
|
||||||
this.create_indicators(idata.indicators, charts);
|
this.create_indicators(idata.indicators, charts);
|
||||||
// Initialize each indicators with the data.
|
// Initialize each indicator with the data directly
|
||||||
idata.indicator_data.then( (data) => { this.init_indicators(data); } );
|
if (idata.indicator_data) {
|
||||||
|
this.init_indicators(idata.indicator_data);
|
||||||
|
} else {
|
||||||
|
console.error('Indicator data is not available.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init_indicators(data){
|
init_indicators(data){
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
};
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
@ -1,159 +1,146 @@
|
||||||
class Signals {
|
class Signals {
|
||||||
constructor(indicators) {
|
constructor(ui) {
|
||||||
this.indicators = indicators;
|
this.ui = ui;
|
||||||
this.signals=[];
|
this.comms = ui.data.comms;
|
||||||
|
this.indicators = ui.indicators;
|
||||||
|
this.data = ui.data;
|
||||||
|
this.signals = [];
|
||||||
|
|
||||||
|
// Register handlers with Comms for specific message types
|
||||||
|
this.comms.on('signal_created', this.handleSignalCreated.bind(this));
|
||||||
|
this.comms.on('signal_updated', this.handleSignalUpdated.bind(this));
|
||||||
|
this.comms.on('signal_deleted', this.handleSignalDeleted.bind(this));
|
||||||
|
this.comms.on('updates', this.handleUpdates.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSignalCreated(data) {
|
||||||
|
console.log("New signal created:", data);
|
||||||
|
// Logic to update signals UI
|
||||||
|
const list_of_one = [data];
|
||||||
|
this.set_data(list_of_one);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSignalUpdated(data) {
|
||||||
|
console.log("Signal updated:", data);
|
||||||
|
// Logic to update signals UI
|
||||||
|
this.update_signal_states(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSignalDeleted(data) {
|
||||||
|
console.log("Signal deleted:", data);
|
||||||
|
// Logic to remove signal from UI
|
||||||
|
this.delete_signal(data.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpdates(data) {
|
||||||
|
const { s_updates } = data;
|
||||||
|
if (s_updates) {
|
||||||
|
this.update_signal_states(s_updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Call to display the 'Create new signal' dialog.
|
// Call to display the 'Create new signal' dialog.
|
||||||
open_signal_Form() { document.getElementById("new_sig_form").style.display = "grid"; }
|
open_signal_Form() { document.getElementById("new_sig_form").style.display = "grid"; }
|
||||||
// Call to hide the 'Create new signal' dialog.
|
// Call to hide the 'Create new signal' dialog.
|
||||||
close_signal_Form() { document.getElementById("new_sig_form").style.display = "none"; }
|
close_signal_Form() { document.getElementById("new_sig_form").style.display = "none"; }
|
||||||
|
|
||||||
request_signals(){
|
request_signals() {
|
||||||
// Requests a list of all the signals from the server.
|
// Requests a list of all the signals from the server.
|
||||||
if (window.UI.data.comms) {
|
if (this.comms) {
|
||||||
window.UI.data.comms.sendToApp('request', { request: 'signals', user_name: window.UI.data.user_name });
|
this.comms.sendToApp('request', { request: 'signals', user_name: this.data.user_name });
|
||||||
} else {
|
} else {
|
||||||
console.error('Comms instance not available.');
|
console.error('Comms instance not available.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete_signal(signal_name){
|
delete_signal(signal_name) {
|
||||||
// Requests that the server remove a specific signal.
|
// Requests that the server remove a specific signal.
|
||||||
window.UI.data.comms.sendToApp('delete_signal', signal_name);
|
this.comms.sendToApp('delete_signal', { name: signal_name });
|
||||||
// Get the signal element from the UI
|
// Get the signal element from the UI
|
||||||
let child = document.getElementById(signal_name + '_item');
|
let child = document.getElementById(signal_name + '_item');
|
||||||
// Ask the parent of the signal element to remove its child(signal) from the document.
|
// Ask the parent of the signal element to remove its child(signal) from the document.
|
||||||
child.parentNode.removeChild(child);
|
if (child && child.parentNode) {
|
||||||
|
child.parentNode.removeChild(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
i_update(updates){
|
|
||||||
// Check the indicator updates for updates that are use as signal sources.
|
|
||||||
// Update the signals about these changes.
|
|
||||||
// Update the html that displays that info.
|
|
||||||
|
|
||||||
// Loop through all the signals.
|
i_update(updates) {
|
||||||
for (let signal in this.signals){
|
for (let signal in this.signals) {
|
||||||
// Get the name of the 1st source.
|
|
||||||
let s1 = this.signals[signal].source1;
|
let s1 = this.signals[signal].source1;
|
||||||
// Check the updates for a source 1 update.
|
if (s1 in updates) {
|
||||||
if (s1 in updates){
|
|
||||||
// Get the property of that source.
|
|
||||||
let p1 = this.signals[signal].prop1;
|
let p1 = this.signals[signal].prop1;
|
||||||
// Get the value of that property.
|
|
||||||
let value1 = updates[s1].data[0][p1];
|
let value1 = updates[s1].data[0][p1];
|
||||||
// Update the signals record of the value.
|
|
||||||
this.signals[signal].value1 = value1.toFixed(2);
|
this.signals[signal].value1 = value1.toFixed(2);
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
// If there is no update move onto the next signal.
|
|
||||||
console.log('!no update for: s1 maybe the indicator is disabled');
|
console.log('!no update for: s1 maybe the indicator is disabled');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// If the second source is an indicator and not just a value.
|
if (this.signals[signal].source2 != 'value') {
|
||||||
if (this.signals[signal].source2 != 'value'){
|
|
||||||
// Get the name of the second source.
|
|
||||||
let s2 = this.signals[signal].source2;
|
let s2 = this.signals[signal].source2;
|
||||||
// Check is source 2 is in the updates.
|
|
||||||
if (s2 in updates) {
|
if (s2 in updates) {
|
||||||
// Get the property of that source.
|
|
||||||
let p2 = this.signals[signal].prop2;
|
let p2 = this.signals[signal].prop2;
|
||||||
// Get the value of that property.
|
|
||||||
let value2 = updates[s2].data[0][p2];
|
let value2 = updates[s2].data[0][p2];
|
||||||
// Update the signals record of the value.
|
|
||||||
this.signals[signal].value2 = value2.toFixed(2);
|
this.signals[signal].value2 = value2.toFixed(2);
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
// If there is no update move onto the next signal.
|
|
||||||
console.log('!no update for: s2 maybe the indicator is disabled');
|
console.log('!no update for: s2 maybe the indicator is disabled');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// loop to next signal.
|
|
||||||
}
|
}
|
||||||
// Update the html element that displays this information.
|
|
||||||
document.getElementById(this.signals[signal].name + '_value1').innerHTML = this.signals[signal].value1;
|
document.getElementById(this.signals[signal].name + '_value1').innerHTML = this.signals[signal].value1;
|
||||||
document.getElementById(this.signals[signal].name + '_value2').innerHTML = this.signals[signal].value2;
|
document.getElementById(this.signals[signal].name + '_value2').innerHTML = this.signals[signal].value2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update_signal_states(s_updates){
|
|
||||||
for (name in s_updates){
|
update_signal_states(s_updates) {
|
||||||
let id = name + '_state'
|
for (let name in s_updates) {
|
||||||
|
let id = name + '_state';
|
||||||
let span = document.getElementById(id);
|
let span = document.getElementById(id);
|
||||||
span.innerHTML = s_updates[name];
|
if (span) {
|
||||||
|
span.innerHTML = s_updates[name];
|
||||||
|
}
|
||||||
console.log('state change!');
|
console.log('state change!');
|
||||||
console.log(name);
|
console.log(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
set_data(signals){
|
|
||||||
// Create a list item for every signal and add it to a UL element.
|
set_data(signals) {
|
||||||
var ul = document.getElementById("signal_list");
|
var ul = document.getElementById("signal_list");
|
||||||
|
for (let sig in signals) {
|
||||||
// loop through a provided list of signals and attributes.
|
let obj = typeof(signals[sig]) == 'string' ? JSON.parse(signals[sig]) : signals[sig];
|
||||||
for (let sig in signals){
|
|
||||||
|
|
||||||
// Create a Json object from each signals.
|
|
||||||
// During initialization this receives the object in string form.
|
|
||||||
// when the object is created this function receives an object.
|
|
||||||
if (typeof(signals[sig]) == 'string'){
|
|
||||||
var obj = JSON.parse(signals[sig]);
|
|
||||||
}else {var obj=signals[sig];}
|
|
||||||
// Keep a local record of the signals.
|
|
||||||
this.signals.push(obj);
|
this.signals.push(obj);
|
||||||
// Define the function that is called when deleting an individual signal.
|
let click_func = `this.delete_signal('${obj.name}')`;
|
||||||
let click_func = "window.UI.signals.delete_signal('" + obj.name + "')";
|
let delete_btn = `<button onclick="${click_func}" style="color:red;">✘</button>`;
|
||||||
// create a delete button for every individual signal.
|
|
||||||
let delete_btn = '<button onclick="' + click_func + '" style="color:red;">✘</button>';
|
|
||||||
|
|
||||||
// Put all the attributes into html elements.
|
let signal_name = ` <span>${obj.name}: </span>`;
|
||||||
let signal_name = " <span>" + obj.name + ": </span>";
|
let signal_state = `<span id='${obj.name}_state'>${obj.state}</span><br>`;
|
||||||
let signal_state = "<span id='" + obj.name + "_state'>" + obj.state + "</span><br>";
|
let signal_source1 = `<span>${obj.source1}(${obj.prop1}) </span>`;
|
||||||
let signal_source1 = "<span>" + obj.source1 + "(" + obj.prop1 + ") </span>";
|
let signal_val1 = `<span id='${obj.name}_value1'>${obj.value1}</span>`;
|
||||||
let signal_val1 = "<span id='" + obj.name + "_value1'>" + obj.value1 + "</span>";
|
let operator = ` ${obj.operator} `;
|
||||||
let operator = " " + obj.operator + " ";
|
let signal_source2 = `<span>${obj.source2}(${obj.prop2}) </span>`;
|
||||||
let signal_source2 = "<span>" + obj.source2 + "(" + obj.prop2 + ") </span>";
|
let signal_val2 = `<span id='${obj.name}_value2'>${obj.value2}</span>`;
|
||||||
let signal_val2 = "<span id='" + obj.name + "_value2'>" + obj.value2 + "</span>";
|
|
||||||
|
|
||||||
// Stick all the html together.
|
let html = delete_btn + signal_name + signal_state + signal_source1 + signal_val1 + operator + signal_source2 + signal_val2;
|
||||||
let html = delete_btn;
|
|
||||||
html += signal_name + signal_state;
|
|
||||||
html += signal_source1 + signal_val1;
|
|
||||||
html += operator;
|
|
||||||
html += signal_source2 + signal_val2;
|
|
||||||
|
|
||||||
// Create the list item.
|
|
||||||
let li = document.createElement("li");
|
let li = document.createElement("li");
|
||||||
// Give it an id.
|
|
||||||
li.id = obj.name + '_item';
|
li.id = obj.name + '_item';
|
||||||
// Inject the html.
|
li.innerHTML = html;
|
||||||
li.innerHTML= html;
|
|
||||||
// And add it the the UL we created earlier.
|
|
||||||
ul.appendChild(li);
|
ul.appendChild(li);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fill_prop(target_id, indctr){
|
fill_prop(target_id, indctr) {
|
||||||
// arg1: Id of of a selection element.
|
|
||||||
// arg2: Name of an indicator
|
|
||||||
// Replace the options of an HTML select elements
|
|
||||||
// with the properties an indicator object.
|
|
||||||
|
|
||||||
// Fetch the objects using name and id received.
|
|
||||||
var target = document.getElementById(target_id);
|
var target = document.getElementById(target_id);
|
||||||
var properties = window.UI.data.indicators[indctr];
|
var properties = this.indicators[indctr];
|
||||||
|
|
||||||
// Remove any previous options in the select tag.
|
|
||||||
removeOptions(target);
|
removeOptions(target);
|
||||||
// Loop through each property in the object.
|
for(let prop in properties) {
|
||||||
// Create an option element for each one.
|
if (prop == 'type' || prop == 'visible' || prop == 'period' || prop.substring(0, 5) == 'color') {
|
||||||
// Append it to the selection element.
|
continue;
|
||||||
for(let prop in properties)
|
|
||||||
{
|
|
||||||
if (prop =='type'|| prop == 'visible' || prop == 'period'){continue;}
|
|
||||||
if (prop.substring(0,5) == 'color'){continue;}
|
|
||||||
var opt = document.createElement("option");
|
|
||||||
opt.innerHTML = prop;
|
|
||||||
target.appendChild(opt);
|
|
||||||
}
|
}
|
||||||
return;
|
var opt = document.createElement("option");
|
||||||
|
opt.innerHTML = prop;
|
||||||
|
target.appendChild(opt);
|
||||||
|
}
|
||||||
function removeOptions(selectElement) {
|
function removeOptions(selectElement) {
|
||||||
var i, L = selectElement.options.length - 1;
|
var i, L = selectElement.options.length - 1;
|
||||||
for(i = L; i >= 0; i--) {
|
for(i = L; i >= 0; i--) {
|
||||||
|
|
@ -161,140 +148,86 @@ class Signals {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch_panel(p1,p2){
|
|
||||||
// Panel switcher for multi page forms
|
switch_panel(p1, p2) {
|
||||||
// arg1 = target from id
|
document.getElementById(p1).style.display = 'none';
|
||||||
// arg2 = next target id
|
document.getElementById(p2).style.display = 'grid';
|
||||||
// This function is used in the New Signal dialog in signals
|
|
||||||
document.getElementById(p1).style.display='none';
|
|
||||||
document.getElementById(p2).style.display='grid';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hideIfTrue(firstValue, scndValue, id){
|
hideIfTrue(firstValue, scndValue, id) {
|
||||||
// Compare first two args and hides an element if they are equal.
|
if (firstValue == scndValue) {
|
||||||
// This function is used in the New Signal dialog in signals
|
document.getElementById(id).style.display = 'none';
|
||||||
if( firstValue == scndValue){
|
} else {
|
||||||
document.getElementById(id).style.display='none';
|
document.getElementById(id).style.display = 'block';
|
||||||
}else{
|
|
||||||
document.getElementById(id).style.display='block'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ns_next(n){
|
|
||||||
// This function is used in the New Signal dialog in signals
|
ns_next(n) {
|
||||||
if (n==1){
|
if (n == 1) {
|
||||||
// Check input fields.
|
|
||||||
let sigName = document.getElementById('signal_name').value;
|
let sigName = document.getElementById('signal_name').value;
|
||||||
let sigSource = document.getElementById('sig_source').value;
|
let sigSource = document.getElementById('sig_source').value;
|
||||||
let sigProp = document.getElementById('sig_prop').value;
|
let sigProp = document.getElementById('sig_prop').value;
|
||||||
if (sigName == '' ) { alert('Please give the signal a name.'); return; }
|
if (sigName == '') { alert('Please give the signal a name.'); return; }
|
||||||
// Populate sig_display
|
document.getElementById('sig_display').innerHTML = `${sigName}: {${sigSource}:${sigProp}}`;
|
||||||
document.getElementById('sig_display').innerHTML = (sigName + ': {' + sigSource + ':' + sigProp +'}');
|
let indctrVal = document.getElementById(sigSource + '_' + sigProp).value;
|
||||||
// Popilate Value input
|
|
||||||
let indctrVal = document.getElementById(sigSource + '_' + sigProp).value;
|
|
||||||
document.getElementById('value').value = indctrVal;
|
document.getElementById('value').value = indctrVal;
|
||||||
|
this.switch_panel('panel_1', 'panel_2');
|
||||||
this.switch_panel('panel_1','panel_2');
|
|
||||||
}
|
}
|
||||||
if (n==2){
|
if (n == 2) {
|
||||||
|
let sigName = document.getElementById('signal_name').value;
|
||||||
|
let sigSource = document.getElementById('sig_source').value;
|
||||||
|
let sigProp = document.getElementById('sig_prop').value;
|
||||||
|
let sig2Source = document.getElementById('sig2_source').value;
|
||||||
|
let sig2Prop = document.getElementById('sig2_prop').value;
|
||||||
|
let operator = document.querySelector('input[name="Operator"]:checked').value;
|
||||||
|
let range = document.getElementById('rangeVal').value;
|
||||||
|
let sigType = document.getElementById('select_s_type').value;
|
||||||
|
let value = document.getElementById('value').value;
|
||||||
|
|
||||||
// Collect all the input fields.
|
let sig1 = `${sigSource} : ${sigProp}`;
|
||||||
|
let sig2 = sigType == 'Comparison' ? `${sig2Source} : ${sig2Prop}` : value;
|
||||||
let sigName = document.getElementById('signal_name').value; // The name of the New Signal.
|
let operatorStr = operator == '+/-' ? `${operator} ${range}` : operator;
|
||||||
let sigSource = document.getElementById('sig_source').value; // The source(indicator) of the signal.
|
|
||||||
let sigProp = document.getElementById('sig_prop').value; // The property to evaluate.
|
|
||||||
let sig2Source = document.getElementById('sig2_source').value; // The second source if selected.
|
|
||||||
let sig2Prop = document.getElementById('sig2_prop').value; // The second property to evaluate.
|
|
||||||
let operator = document.querySelector('input[name="Operator"]:checked').value; // The operator this evaluation will use.
|
|
||||||
let range = document.getElementById('rangeVal').value; // The value of any range being evaluated.
|
|
||||||
let sigType = document.getElementById('select_s_type').value; // The type of signal value or indicator comparison.
|
|
||||||
let value = document.getElementById('value').value; // The input value if it is a value comparison.
|
|
||||||
|
|
||||||
// Create a string to define the signal.
|
|
||||||
|
|
||||||
// Include the first indicator source.
|
|
||||||
var sig1 = `${sigSource} : ${sigProp}`;
|
|
||||||
// If it is a comparison signal include the second indicator source.
|
|
||||||
if (sigType == 'Comparison') {
|
|
||||||
var sig2 = `${sig2Source} : ${sig2Prop}`;
|
|
||||||
}
|
|
||||||
// If it is a value signal include the value.
|
|
||||||
if (sigType == 'Value') {var sig2 = value;}
|
|
||||||
|
|
||||||
// If the operator is set to range, include the range value in the string.
|
|
||||||
if (operator == '+/-') {
|
|
||||||
var operatorStr = `${operator} ${range}`;
|
|
||||||
} else{
|
|
||||||
var operatorStr = operator;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sigDisplayStr = `(${sigName}) (${sig1}) (${operatorStr}) (${sig2})`;
|
let sigDisplayStr = `(${sigName}) (${sig1}) (${operatorStr}) (${sig2})`;
|
||||||
|
|
||||||
// Get the current realtime values of the sources.
|
|
||||||
let sig1_realtime = document.getElementById(sigSource + '_' + sigProp).value;
|
let sig1_realtime = document.getElementById(sigSource + '_' + sigProp).value;
|
||||||
|
let sig2_realtime = sigType == 'Comparison' ? document.getElementById(sig2Source + '_' + sig2Prop).value : sig2;
|
||||||
|
|
||||||
if (sigType == 'Comparison') {
|
|
||||||
// If its a comparison get the second value from the second source.
|
|
||||||
var sig2_realtime = document.getElementById(sig2Source + '_' + sig2Prop).value;
|
|
||||||
}else {
|
|
||||||
// If not the second realtime value is literally the value.
|
|
||||||
var sig2_realtime = sig2;
|
|
||||||
}
|
|
||||||
// Populate the signal display field with the string.
|
|
||||||
document.getElementById('sig_display2').innerHTML = sigDisplayStr;
|
document.getElementById('sig_display2').innerHTML = sigDisplayStr;
|
||||||
|
document.getElementById('sig_realtime').innerHTML = `(${sigProp} : ${sig1_realtime}) (${operatorStr}) (${sig2_realtime})`;
|
||||||
|
|
||||||
// Populate the realtime values display.
|
let evalStr;
|
||||||
let realtime_Str = `(${sigProp} : ${sig1_realtime}) (${operatorStr}) (${sig2_realtime})`;
|
if (operator == '=') evalStr = (sig1_realtime == sig2_realtime);
|
||||||
document.getElementById('sig_realtime').innerHTML = realtime_Str;
|
if (operator == '>') evalStr = (sig1_realtime > sig2_realtime);
|
||||||
// Evaluate the signal
|
if (operator == '<') evalStr = (sig1_realtime < sig2_realtime);
|
||||||
var evalStr;
|
if (operator == '+/-') evalStr = (Math.abs(sig1_realtime - sig2_realtime) <= range);
|
||||||
if (operator == '=') {evalStr = (sig1_realtime == sig2_realtime);console.log([sig1_realtime, sig2_realtime, operator,evalStr]);}
|
|
||||||
if (operator == '>') {evalStr = (sig1_realtime > sig2_realtime);}
|
|
||||||
if (operator == '<') {evalStr = (sig1_realtime < sig2_realtime);}
|
|
||||||
if (operator == '+/-') {
|
|
||||||
evalStr = (Math.abs(sig1_realtime - sig2_realtime) <= range);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the signal eval field with the string.
|
|
||||||
document.getElementById('sig_eval').innerHTML = evalStr;
|
document.getElementById('sig_eval').innerHTML = evalStr;
|
||||||
|
this.switch_panel('panel_2', 'panel_3');
|
||||||
// Show the panel
|
|
||||||
this.switch_panel('panel_2','panel_3');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submitNewSignal(){
|
submitNewSignal() {
|
||||||
|
let name = document.getElementById('signal_name').value;
|
||||||
|
let source1 = document.getElementById('sig_source').value;
|
||||||
|
let prop1 = document.getElementById('sig_prop').value;
|
||||||
|
let source2 = document.getElementById('sig2_source').value;
|
||||||
|
let prop2 = document.getElementById('sig2_prop').value;
|
||||||
|
let operator = document.querySelector('input[name="Operator"]:checked').value;
|
||||||
|
let range = document.getElementById('rangeVal').value;
|
||||||
|
let sigType = document.getElementById('select_s_type').value;
|
||||||
|
let value = document.getElementById('value').value;
|
||||||
|
|
||||||
// Collect all the input fields.
|
if (sigType != 'Comparison') {
|
||||||
|
source2 = 'value';
|
||||||
var name = document.getElementById('signal_name').value; // The name of the New Signal.
|
prop2 = value;
|
||||||
var source1 = document.getElementById('sig_source').value; // The source(indicator) of the signal.
|
|
||||||
var prop1 = document.getElementById('sig_prop').value; // The property to evaluate.
|
|
||||||
var source2 = document.getElementById('sig2_source').value; // The second source if selected.
|
|
||||||
var prop2 = document.getElementById('sig2_prop').value; // The second property to evaluate.
|
|
||||||
var operator = document.querySelector('input[name="Operator"]:checked').value; // The operator this evaluation will use.
|
|
||||||
var range = document.getElementById('rangeVal').value; // The value of any range being evaluated.
|
|
||||||
var sigType = document.getElementById('select_s_type').value; // The type of signal value or indicator comparison.
|
|
||||||
var value = document.getElementById('value').value; // The input value if it is a value comparison.
|
|
||||||
var state = false;
|
|
||||||
if (sigType == 'Comparison'){
|
|
||||||
var source2 = source2;
|
|
||||||
var prop2 = prop2;
|
|
||||||
}else{
|
|
||||||
var source2 = 'value';
|
|
||||||
var prop2 = value;
|
|
||||||
}
|
}
|
||||||
var value1 = null;
|
|
||||||
var value2 = null;
|
|
||||||
if (operator == "+/-" ){
|
|
||||||
var range = {range : range};
|
|
||||||
var data = {name, source1, prop1, operator, source2, prop2, range, state, value1, value2};
|
|
||||||
}else{
|
|
||||||
var data = {name, source1, prop1, operator, source2, prop2, state, value1, value2};
|
|
||||||
}
|
|
||||||
/* It may be more maintainable to configure the connection inside the different classes
|
|
||||||
than passing functions, references and callbacks around. */
|
|
||||||
|
|
||||||
window.UI.data.comms.sendToApp( "new_signal", data);
|
let state = false;
|
||||||
this.close_signal_Form();
|
let value1 = null;
|
||||||
|
let value2 = null;
|
||||||
|
let data = operator == "+/-" ? {name, source1, prop1, operator, source2, prop2, range, state, value1, value2} : {name, source1, prop1, operator, source2, prop2, state, value1, value2};
|
||||||
|
|
||||||
|
this.comms.sendToApp("new_signal", data);
|
||||||
|
this.close_signal_Form();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,15 @@ class Trade {
|
||||||
// Store this object pointer for referencing inside callbacks and event handlers.
|
// Store this object pointer for referencing inside callbacks and event handlers.
|
||||||
var that = this;
|
var that = this;
|
||||||
// Assign the quote value of the asset to the current price display element.
|
// Assign the quote value of the asset to the current price display element.
|
||||||
window.UI.data.price_history.then((ph) => {
|
if (window.UI.data.price_history && window.UI.data.price_history.length > 0) {
|
||||||
|
let ph = window.UI.data.price_history;
|
||||||
// Assign the last closing price in the price history to the price input element.
|
// Assign the last closing price in the price history to the price input element.
|
||||||
that.priceInput_el.value = ph[ph.length-1].close;
|
that.priceInput_el.value = ph[ph.length - 1].close;
|
||||||
// Set the current price display to the same value.
|
// Set the current price display to the same value.
|
||||||
that.currentPrice_el.value = that.priceInput_el.value;
|
that.currentPrice_el.value = that.priceInput_el.value;
|
||||||
});
|
} else {
|
||||||
|
console.error('Price history data is not available or empty.');
|
||||||
|
}
|
||||||
// Set the trade value to zero. This will update when price and quantity inputs are received.
|
// Set the trade value to zero. This will update when price and quantity inputs are received.
|
||||||
this.tradeValue_el.value = 0;
|
this.tradeValue_el.value = 0;
|
||||||
// Toggle current price or input-field for value updates depending on orderType.
|
// Toggle current price or input-field for value updates depending on orderType.
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
<div class="content">
|
<div class="content" id="backtesting_hud">
|
||||||
<h3>Back Testing</h3>
|
<button class="btn" id="new_backtest_btn" onclick="UI.backtesting.openForm('new')">New Backtest</button>
|
||||||
</div>
|
<hr>
|
||||||
|
<h3>Back Tests</h3>
|
||||||
|
<div class="backtests-container" id="backtest_display"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,13 @@
|
||||||
<script src="{{ url_for('static', filename='controls.js') }}"></script>
|
<script src="{{ url_for('static', filename='controls.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='signals.js') }}"></script>
|
<script src="{{ url_for('static', filename='signals.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='trade.js') }}"></script>
|
<script src="{{ url_for('static', filename='trade.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='backtesting.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='general.js') }}"></script>
|
<script src="{{ url_for('static', filename='general.js') }}"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Hidden Div elements containing markup for popup context and forms.-->
|
<!-- Hidden Div elements containing markup for popup context and forms.-->
|
||||||
|
{% include "backtest_popup.html" %}
|
||||||
{% include "new_trade_popup.html" %}
|
{% include "new_trade_popup.html" %}
|
||||||
{% include "new_strategy_popup.html" %}
|
{% include "new_strategy_popup.html" %}
|
||||||
{% include "new_signal_popup.html" %}
|
{% include "new_signal_popup.html" %}
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,8 @@
|
||||||
|
|
||||||
<!-- Fixed property: Value -->
|
<!-- Fixed property: Value -->
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
{% if 'value' in indicator_list[indicator] %}
|
<!-- Create a generic value container for JavaScript to populate -->
|
||||||
<input class="ie_value" type="number" id="{{indicator}}_value" value="{{indicator_list[indicator]['value']}}" name="value" readonly>
|
<textarea class="ie_value" id="{{indicator}}_value" name="value" readonly style="resize: none; overflow: hidden; height: auto; width: 100%; border: none; background: transparent; text-align: center;">N/A</textarea>
|
||||||
{% else %}
|
|
||||||
<span>-</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fixed property: Type -->
|
<!-- Fixed property: Type -->
|
||||||
|
|
@ -103,9 +100,11 @@
|
||||||
<!-- Color Picker -->
|
<!-- Color Picker -->
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
{% if 'color' in indicator_list[indicator] %}
|
{% if 'color' in indicator_list[indicator] %}
|
||||||
<input class="ietextbox" type="color" id="{{indicator}}_color" value="{{indicator_list[indicator]['color']}}" name="color">
|
<input class="ietextbox" type="color" id="{{indicator}}_color"
|
||||||
|
value="{{ indicator_list[indicator]['color'] if indicator_list[indicator]['color'] else '#000000' }}"
|
||||||
|
name="color">
|
||||||
{% else %}
|
{% else %}
|
||||||
<span>-</span>
|
<input class="ietextbox" type="color" id="{{indicator}}_color" value="#000000" name="color">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Add the missing resize handle here -->
|
<!-- Add the missing resize handle here -->
|
||||||
<div class="resize-handle" id="resize-br"></div>
|
<div id="resize-strategy" class="resize-handle"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add CSS to hide scrollbars but allow scrolling and fix the resize handle -->
|
<!-- Add CSS to hide scrollbars but allow scrolling and fix the resize handle -->
|
||||||
|
|
@ -109,24 +109,36 @@
|
||||||
<!-- Indicator blocks go here -->
|
<!-- Indicator blocks go here -->
|
||||||
</category>
|
</category>
|
||||||
|
|
||||||
<category name="Values" colour="230">
|
<category name="Values" colour="230">
|
||||||
<block type="last_candle_value"></block>
|
<block type="last_candle_value"></block>
|
||||||
<block type="value_input"></block>
|
<block type="value_input"></block>
|
||||||
</category>
|
<block type="source"></block>
|
||||||
|
<block type="current_balance"></block>
|
||||||
|
<block type="starting_balance"></block>
|
||||||
|
<block type="strategy_profit_loss"></block>
|
||||||
|
<block type="active_trades"></block>
|
||||||
|
|
||||||
|
</category>
|
||||||
|
|
||||||
<category name="Logic" colour="210">
|
<category name="Logic" colour="210">
|
||||||
<block type="comparison"></block>
|
<block type="comparison"></block>
|
||||||
<block type="logical_and"></block>
|
<block type="logical_and"></block>
|
||||||
<block type="logical_or"></block>
|
<block type="logical_or"></block>
|
||||||
<block type="is_true"></block>
|
<block type="arithmetic_operator"></block>
|
||||||
|
<block type="is_false"></block>
|
||||||
|
<block type="flag_is_set"></block>
|
||||||
|
<block type="set_flag"></block>
|
||||||
</category>
|
</category>
|
||||||
|
|
||||||
<!-- New category for Trading Actions -->
|
<!-- New category for Trading Actions -->
|
||||||
<category name="Trading" colour="230">
|
<category name="Trading" colour="230">
|
||||||
<block type="trade_action"></block>
|
<block type="trade_action"></block>
|
||||||
<block type="order_type"></block>
|
<block type="order_type"></block>
|
||||||
<block type="time_in_force"></block>
|
<block type="time_in_force"></block>
|
||||||
<block type="stop_loss"></block>
|
<block type="stop_loss"></block>
|
||||||
<block type="take_profit"></block>
|
<block type="take_profit"></block>
|
||||||
</category>
|
<block type="target_market"></block>
|
||||||
|
</category>
|
||||||
</xml>
|
</xml>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
<!-- Container for the javascript chart -->
|
<!-- Container for the javascript chart -->
|
||||||
<div id="chart">
|
<div id="chart">
|
||||||
<div class="a1" >
|
<div class="a1" >
|
||||||
|
<!-- Toggle On/Off indicators-->
|
||||||
|
<button id="enable_indicators" type="button" onclick="UI.controls.showAtPos(event,'indicators')">Indicators</button>
|
||||||
|
<!-- Container exchange_name for any indicator output -->
|
||||||
|
<div id="indicator_output" ></div>
|
||||||
<!-- Chart specific controls -->
|
<!-- Chart specific controls -->
|
||||||
<div id="chart_controls">
|
<div id="chart_controls">
|
||||||
<!-- Container exchange_name for any indicator output -->
|
|
||||||
<div id="indicator_output" ></div>
|
|
||||||
<!-- Trading pair selector -->
|
<!-- Trading pair selector -->
|
||||||
<form id="tp_selector" action="/settings" method="post">
|
<form id="tp_selector" action="/settings" method="post">
|
||||||
<input type="hidden" name="setting" value="trading_pair" />
|
<input type="hidden" name="setting" value="trading_pair" />
|
||||||
|
|
@ -34,8 +36,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
<!-- Toggle On/Off indicators-->
|
|
||||||
<button id="enable_indicators" type="button" onclick="UI.controls.showAtPos(event,'indicators')">Indicators</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue