diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py
index 2dc5758..e2f0bc9 100644
--- a/src/BrighterTrades.py
+++ b/src/BrighterTrades.py
@@ -83,7 +83,8 @@ class BrighterTrades:
self.backtester = Backtester(data_cache=self.data, strategies=self.strategies,
indicators=self.indicators, socketio=socketio,
edm_client=self.edm_client,
- external_indicators=self.external_indicators)
+ external_indicators=self.external_indicators,
+ signals=self.signals, formations=self.formations)
self.backtests = {} # In-memory storage for backtests (replace with DB access in production)
# Wallet manager for Bitcoin wallets and credits ledger
@@ -1009,6 +1010,11 @@ class BrighterTrades:
indicator_owner_id=indicator_owner_id, # For subscribed strategies, use creator's indicators
)
+ # Inject signals and formations managers for strategy execution
+ instance.signals = self.signals
+ instance.formations = self.formations
+ instance.formation_owner_id = indicator_owner_id if indicator_owner_id else user_id
+
# Store fee tracking info on the instance
if strategy_run_id:
instance.strategy_run_id = strategy_run_id
diff --git a/src/PythonGenerator.py b/src/PythonGenerator.py
index 05513b6..e56d580 100644
--- a/src/PythonGenerator.py
+++ b/src/PythonGenerator.py
@@ -145,6 +145,8 @@ class PythonGenerator:
handler_method = self.handle_indicator
elif node_type.startswith('signal_'):
handler_method = self.handle_signal
+ elif node_type.startswith('formation_') or node_type == 'formation':
+ handler_method = self.handle_formation
else:
handler_method = getattr(self, f'handle_{node_type}', self.handle_default)
handler_code = handler_method(node, indent_level)
@@ -195,6 +197,8 @@ class PythonGenerator:
handler_method = self.handle_indicator
elif node_type.startswith('signal_'):
handler_method = self.handle_signal
+ elif node_type.startswith('formation_') or node_type == 'formation':
+ handler_method = self.handle_formation
else:
handler_method = getattr(self, f'handle_{node_type}', self.handle_default)
condition_code = handler_method(condition_node, indent_level=indent_level)
@@ -291,6 +295,46 @@ class PythonGenerator:
logger.debug(f"Generated signal condition: {expr}")
return expr
+ # ==============================
+ # Formations Handlers
+ # ==============================
+
+ def handle_formation(self, node: Dict[str, Any], indent_level: int) -> str:
+ """
+ Handles formation nodes by generating a function call to get formation value.
+ Uses tbl_key (stable UUID) for referencing formations.
+
+ :param node: The formation node.
+ :param indent_level: Current indentation level.
+ :return: A string representing the formation value lookup.
+ """
+ fields = node.get('fields', {})
+
+ # Get formation reference by tbl_key (stable) not name
+ tbl_key = fields.get('TBL_KEY')
+ property_name = fields.get('PROPERTY', 'line')
+ formation_name = fields.get('NAME', 'unknown')
+
+ if not tbl_key:
+ logger.error(f"formation node missing TBL_KEY. fields={fields}")
+ return 'None'
+
+ # Track formation usage for dependency resolution
+ if not hasattr(self, 'formations_used'):
+ self.formations_used = []
+ self.formations_used.append({
+ 'tbl_key': tbl_key,
+ 'name': formation_name,
+ 'property': property_name
+ })
+
+ # Generate code that calls process_formation
+ # Uses current candle time by default (timestamp=None)
+ expr = f"process_formation('{tbl_key}', '{property_name}')"
+
+ logger.debug(f"Generated formation lookup: {expr}")
+ return expr
+
# ==============================
# Balances Handlers
# ==============================
diff --git a/src/StrategyInstance.py b/src/StrategyInstance.py
index 629b4ec..f4c41f6 100644
--- a/src/StrategyInstance.py
+++ b/src/StrategyInstance.py
@@ -87,6 +87,7 @@ class StrategyInstance:
'notify_user': self.notify_user,
'process_indicator': self.process_indicator,
'process_signal': self.process_signal,
+ 'process_formation': self.process_formation,
'get_strategy_profit_loss': self.get_strategy_profit_loss,
'is_in_profit': self.is_in_profit,
'is_in_loss': self.is_in_loss,
@@ -814,6 +815,59 @@ class StrategyInstance:
traceback.print_exc()
return False if output_field == 'triggered' else None
+ def process_formation(self, tbl_key: str, property_name: str = 'line', timestamp: int = None) -> float:
+ """
+ Gets the price value of a formation property at a given timestamp.
+
+ Uses formation_owner_id (not current user) for subscribed strategies.
+ Parallel to indicator_owner_id pattern.
+
+ :param tbl_key: Unique key of the formation (UUID).
+ :param property_name: Property to retrieve ('line', 'upper', 'lower', 'midline', etc.).
+ :param timestamp: Unix timestamp in seconds UTC. If None, uses current candle time.
+ :return: Price value at the timestamp, or None on error.
+ """
+ try:
+ # Check if formations manager is available
+ if not hasattr(self, 'formations') or self.formations is None:
+ logger.warning(f"Formations manager not available in StrategyInstance")
+ return None
+
+ # Default timestamp: use current candle time if available
+ if timestamp is None:
+ timestamp = self.get_current_candle_time()
+
+ # Use formation_owner_id for subscribed strategies (parallel to indicator_owner_id)
+ owner_id = getattr(self, 'formation_owner_id', self.user_id)
+
+ # Look up the formation by tbl_key using owner's formations
+ formation = self.formations.get_by_tbl_key_for_strategy(tbl_key, owner_id)
+ if formation is None:
+ logger.warning(f"Formation with tbl_key '{tbl_key}' not found for owner {owner_id}")
+ return None
+
+ # Get the property value at the timestamp
+ value = self.formations.get_property_value(formation, property_name, timestamp)
+ logger.debug(f"Formation '{formation.get('name')}' {property_name} at {timestamp}: {value}")
+ return value
+
+ except Exception as e:
+ logger.error(
+ f"Error processing formation '{tbl_key}' in StrategyInstance '{self.strategy_instance_id}': {e}",
+ exc_info=True)
+ traceback.print_exc()
+ return None
+
+ def get_current_candle_time(self) -> int:
+ """
+ Returns the current candle timestamp in seconds UTC.
+
+ In backtest mode, this is overridden to return the historical bar time.
+ In live/paper mode, returns the current time.
+ """
+ import time
+ return int(time.time())
+
def get_strategy_profit_loss(self, strategy_id: str) -> float:
"""
Retrieves the current profit or loss of the strategy.
diff --git a/src/backtest_strategy_instance.py b/src/backtest_strategy_instance.py
index 445e8eb..68c6e0b 100644
--- a/src/backtest_strategy_instance.py
+++ b/src/backtest_strategy_instance.py
@@ -414,6 +414,16 @@ class BacktestStrategyInstance(StrategyInstance):
logger.warning(f"Could not get candle datetime: {e}")
return dt.datetime.now()
+ def get_current_candle_time(self) -> int:
+ """
+ Returns the current candle timestamp in seconds UTC.
+
+ In backtest mode, returns the historical bar time being processed.
+ This is critical for accurate formation value lookups.
+ """
+ candle_datetime = self.get_current_candle_datetime()
+ return int(candle_datetime.timestamp())
+
def get_collected_alerts(self) -> list:
"""
Returns the list of collected alerts for inclusion in backtest results.
diff --git a/src/backtesting.py b/src/backtesting.py
index f8f0a08..006e1bb 100644
--- a/src/backtesting.py
+++ b/src/backtesting.py
@@ -42,7 +42,7 @@ class EquityCurveAnalyzer(bt.Analyzer):
# Backtester Class
class Backtester:
def __init__(self, data_cache: DataCache, strategies: Strategies, indicators: Indicators, socketio,
- edm_client=None, external_indicators=None):
+ edm_client=None, external_indicators=None, signals=None, formations=None):
""" Initialize the Backtesting class with a cache for back-tests """
self.data_cache = data_cache
self.strategies = strategies
@@ -50,6 +50,8 @@ class Backtester:
self.socketio = socketio
self.edm_client = edm_client
self.external_indicators = external_indicators
+ self.signals = signals
+ self.formations = formations
# Ensure 'tests' cache exists
self.data_cache.create_cache(
@@ -883,6 +885,11 @@ class Backtester:
indicator_owner_id=indicator_owner_id, # For subscribed strategies, use creator's indicators
)
+ # Inject signals and formations for strategy execution
+ strategy_instance.signals = self.signals
+ strategy_instance.formations = self.formations
+ strategy_instance.formation_owner_id = indicator_owner_id if indicator_owner_id else user_id
+
# Cache the backtest
self.cache_backtest(backtest_key, msg_data, strategy_instance_id)
diff --git a/src/static/Strategies.js b/src/static/Strategies.js
index d1d0ce2..991af5c 100644
--- a/src/static/Strategies.js
+++ b/src/static/Strategies.js
@@ -1053,6 +1053,10 @@ class StratWorkspaceManager {
const signalBlocksModule = await import('./blocks/signal_blocks.js');
signalBlocksModule.defineSignalBlocks();
+ // Load and define formation blocks
+ const formationBlocksModule = await import('./blocks/formation_blocks.js');
+ formationBlocksModule.defineFormationBlocks();
+
} catch (error) {
console.error("Error loading Blockly modules: ", error);
return;
diff --git a/src/static/blocks/formation_blocks.js b/src/static/blocks/formation_blocks.js
new file mode 100644
index 0000000..abb2d01
--- /dev/null
+++ b/src/static/blocks/formation_blocks.js
@@ -0,0 +1,121 @@
+// client/formation_blocks.js
+
+// Define Blockly blocks for user-created formations
+export function defineFormationBlocks() {
+
+ // Ensure Blockly.JSON is available
+ if (!Blockly.JSON) {
+ console.error('Blockly.JSON is not defined. Ensure json_generators.js is loaded before formation_blocks.js.');
+ return;
+ }
+
+ // Get all user formations for current scope
+ const formations = window.UI?.formations?.dataManager?.getAllFormations?.() || [];
+ const toolboxCategory = document.querySelector('#toolbox_advanced category[name="Formations"]');
+
+ if (!toolboxCategory) {
+ console.error('Formations category not found in the toolbox.');
+ return;
+ }
+
+ // Clear existing formation blocks (for refresh)
+ const existingBlocks = toolboxCategory.querySelectorAll('block[type^="formation_"]');
+ existingBlocks.forEach(block => block.remove());
+
+ // Check if there are any formations to display
+ if (formations.length === 0) {
+ // Add helpful message when no formations exist
+ if (!toolboxCategory.querySelector('label')) {
+ const labelElement = document.createElement('label');
+ labelElement.setAttribute('text', 'No formations configured yet.');
+ toolboxCategory.appendChild(labelElement);
+
+ const labelElement2 = document.createElement('label');
+ labelElement2.setAttribute('text', 'Draw formations on the chart');
+ toolboxCategory.appendChild(labelElement2);
+
+ const labelElement3 = document.createElement('label');
+ labelElement3.setAttribute('text', 'using the Formations panel.');
+ toolboxCategory.appendChild(labelElement3);
+ }
+
+ console.log('No formations available - added help message to toolbox.');
+ return;
+ }
+
+ // Remove help labels if formations exist
+ const labels = toolboxCategory.querySelectorAll('label');
+ labels.forEach(label => label.remove());
+
+ // Property options vary by formation type
+ const propertyOptionsByType = {
+ 'support_resistance': [
+ ['line value', 'line']
+ ],
+ 'channel': [
+ ['upper line', 'upper'],
+ ['lower line', 'lower'],
+ ['midline', 'midline']
+ ]
+ };
+
+ // Default properties for unknown types
+ const defaultProperties = [
+ ['line value', 'line']
+ ];
+
+ for (const formation of formations) {
+ const formationName = formation.name;
+ const formationType = formation.formation_type;
+ const tblKey = formation.tbl_key;
+
+ // Create a unique block type using tbl_key (stable UUID)
+ const sanitizedName = formationName.replace(/\s+/g, '_').replace(/[^a-zA-Z0-9_]/g, '');
+ const blockType = 'formation_' + sanitizedName + '_' + tblKey.substring(0, 8);
+
+ // Get property options for this formation type
+ const propertyOptions = propertyOptionsByType[formationType] || defaultProperties;
+
+ // Define the block for this formation
+ Blockly.defineBlocksWithJsonArray([{
+ "type": blockType,
+ "message0": `Formation: ${formationName} %1`,
+ "args0": [
+ {
+ "type": "field_dropdown",
+ "name": "PROPERTY",
+ "options": propertyOptions
+ }
+ ],
+ "output": "dynamic_value",
+ "colour": 290, // Purple-ish color for formations
+ "tooltip": `Get the price value of formation '${formationName}' (${formationType}) at current candle time`,
+ "helpUrl": ""
+ }]);
+
+ // Define the JSON generator for this block
+ Blockly.JSON[blockType] = function(block) {
+ const selectedProperty = block.getFieldValue('PROPERTY');
+ const json = {
+ type: 'formation',
+ fields: {
+ TBL_KEY: tblKey,
+ NAME: formationName,
+ PROPERTY: selectedProperty
+ }
+ };
+ // Output as dynamic_value
+ return {
+ type: 'dynamic_value',
+ values: [json]
+ };
+ };
+
+ // Append the newly created block to the Formations category in the toolbox
+ const blockElement = document.createElement('block');
+ blockElement.setAttribute('type', blockType);
+ toolboxCategory.appendChild(blockElement);
+ }
+
+ console.log(`Formation blocks defined: ${formations.length} formations added to toolbox.`);
+}
diff --git a/src/static/formations.js b/src/static/formations.js
index e27fe0d..ac145d5 100644
--- a/src/static/formations.js
+++ b/src/static/formations.js
@@ -385,10 +385,11 @@ class Formations {
this.registerSocketHandlers();
// Get current scope from chart data
+ // Note: bt_data uses 'exchange_name' not 'exchange', and 'timeframe' not 'interval'
this.currentScope = {
- exchange: this.data?.exchange || window.bt_data?.exchange || 'kucoin',
+ exchange: this.data?.exchange || window.bt_data?.exchange_name || 'binance',
market: this.data?.trading_pair || window.bt_data?.trading_pair || 'BTC/USDT',
- timeframe: this.data?.timeframe || window.bt_data?.timeframe || '1h'
+ timeframe: this.data?.interval || window.bt_data?.timeframe || '1h'
};
// Fetch formations for current scope
diff --git a/src/templates/new_strategy_popup.html b/src/templates/new_strategy_popup.html
index 78278a5..78afb8a 100644
--- a/src/templates/new_strategy_popup.html
+++ b/src/templates/new_strategy_popup.html
@@ -201,6 +201,11 @@ and you set fee to 50%, you earn $0.50 per profitable trade.
+
+
+
+
+