Compare commits
No commits in common. "9a389d20d2f120878c966d3e542c6325c9e32de1" and "ba7b6e79ff97cf1a11ef9ef450cffd106fd831c1" have entirely different histories.
9a389d20d2
...
ba7b6e79ff
|
|
@ -11,7 +11,6 @@ from Configuration import Configuration
|
|||
from ExchangeInterface import ExchangeInterface
|
||||
from indicators import Indicators
|
||||
from Signals import Signals
|
||||
from Formations import Formations
|
||||
from ExternalSources import ExternalSources
|
||||
from ExternalIndicators import ExternalIndicatorsManager
|
||||
from trade import Trades
|
||||
|
|
@ -55,9 +54,6 @@ class BrighterTrades:
|
|||
# Object that maintains signals.
|
||||
self.signals = Signals(self.data)
|
||||
|
||||
# Object that maintains chart formations (trendlines, channels, patterns).
|
||||
self.formations = Formations(self.data)
|
||||
|
||||
# Object that maintains external data sources (custom signal types).
|
||||
self.external_sources = ExternalSources(self.data)
|
||||
|
||||
|
|
@ -83,8 +79,7 @@ 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,
|
||||
signals=self.signals, formations=self.formations)
|
||||
external_indicators=self.external_indicators)
|
||||
self.backtests = {} # In-memory storage for backtests (replace with DB access in production)
|
||||
|
||||
# Wallet manager for Bitcoin wallets and credits ledger
|
||||
|
|
@ -1010,11 +1005,6 @@ 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
|
||||
|
|
@ -1898,42 +1888,6 @@ class BrighterTrades:
|
|||
logger.error(f"Error getting public strategies: {e}", exc_info=True)
|
||||
return standard_reply("public_strategies_error", {"message": str(e)})
|
||||
|
||||
# ===== Formation Handlers =====
|
||||
|
||||
if msg_type == 'request_formations':
|
||||
# Get formations for current chart scope
|
||||
exchange = msg_data.get('exchange')
|
||||
market = msg_data.get('market')
|
||||
timeframe = msg_data.get('timeframe')
|
||||
if not all([exchange, market, timeframe]):
|
||||
return standard_reply("formation_error", {"message": "Missing scope parameters"})
|
||||
formations = self.formations.get_for_scope(user_id, exchange, market, timeframe)
|
||||
return standard_reply("formations", {"formations": formations})
|
||||
|
||||
if msg_type == 'new_formation':
|
||||
result = self.formations.create(user_id, msg_data)
|
||||
if result.get('success'):
|
||||
return standard_reply("formation_created", result)
|
||||
else:
|
||||
return standard_reply("formation_error", result)
|
||||
|
||||
if msg_type == 'edit_formation':
|
||||
result = self.formations.update(user_id, msg_data)
|
||||
if result.get('success'):
|
||||
return standard_reply("formation_updated", result)
|
||||
else:
|
||||
return standard_reply("formation_error", result)
|
||||
|
||||
if msg_type == 'delete_formation':
|
||||
tbl_key = msg_data.get('tbl_key')
|
||||
if not tbl_key:
|
||||
return standard_reply("formation_error", {"message": "Missing tbl_key"})
|
||||
result = self.formations.delete(user_id, tbl_key)
|
||||
if result.get('success'):
|
||||
return standard_reply("formation_deleted", result)
|
||||
else:
|
||||
return standard_reply("formation_error", result)
|
||||
|
||||
if msg_type == 'reply':
|
||||
# If the message is a reply log the response to the terminal.
|
||||
print(f"\napp.py:Received reply: {msg_data}")
|
||||
|
|
|
|||
|
|
@ -1,490 +0,0 @@
|
|||
"""
|
||||
Formations module for chart formation management.
|
||||
|
||||
Handles CRUD operations for chart formations (trendlines, channels, patterns)
|
||||
with database-backed storage via DataCache.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from DataCache_v3 import DataCache
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Formation:
|
||||
"""Class for individual formation properties."""
|
||||
tbl_key: str
|
||||
user_id: int
|
||||
name: str
|
||||
formation_type: str # 'support_resistance', 'channel', etc.
|
||||
exchange: str
|
||||
market: str
|
||||
timeframe: str
|
||||
lines_json: str # JSON string of line data
|
||||
color: str = '#667eea'
|
||||
visible: bool = True
|
||||
created_at: int = 0
|
||||
updated_at: int = 0
|
||||
|
||||
@property
|
||||
def lines(self) -> List[dict]:
|
||||
"""Parse lines_json and return as list of line dicts."""
|
||||
try:
|
||||
data = json.loads(self.lines_json)
|
||||
return data.get('lines', [])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return []
|
||||
|
||||
@property
|
||||
def targets(self) -> List[dict]:
|
||||
"""Parse lines_json and return targets (Phase C feature)."""
|
||||
try:
|
||||
data = json.loads(self.lines_json)
|
||||
return data.get('targets', [])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return []
|
||||
|
||||
|
||||
class Formations:
|
||||
"""Manages chart formations with database-backed storage via DataCache."""
|
||||
|
||||
TABLE_NAME = 'formations'
|
||||
|
||||
def __init__(self, data_cache: DataCache):
|
||||
"""
|
||||
Initialize the Formations class.
|
||||
|
||||
:param data_cache: Instance of DataCache to manage cache and database interactions.
|
||||
"""
|
||||
self.data_cache = data_cache
|
||||
|
||||
# Ensure the formations table exists in the database
|
||||
self._ensure_table_exists()
|
||||
|
||||
# Create a cache for formations
|
||||
self.data_cache.create_cache(
|
||||
name='formations',
|
||||
cache_type='table',
|
||||
size_limit=1000,
|
||||
eviction_policy='deny',
|
||||
columns=[
|
||||
"tbl_key",
|
||||
"user_id",
|
||||
"name",
|
||||
"formation_type",
|
||||
"exchange",
|
||||
"market",
|
||||
"timeframe",
|
||||
"lines_json",
|
||||
"color",
|
||||
"visible",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
]
|
||||
)
|
||||
|
||||
# In-memory cache of Formation objects
|
||||
self.formations: Dict[str, Formation] = {}
|
||||
|
||||
# Load existing formations from database
|
||||
self._load_formations_from_db()
|
||||
|
||||
def _ensure_table_exists(self) -> None:
|
||||
"""Create the formations table in the database if it doesn't exist."""
|
||||
try:
|
||||
if not self.data_cache.db.table_exists(self.TABLE_NAME):
|
||||
create_sql = """
|
||||
CREATE TABLE IF NOT EXISTS formations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tbl_key TEXT UNIQUE NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
formation_type TEXT NOT NULL,
|
||||
exchange TEXT NOT NULL,
|
||||
market TEXT NOT NULL,
|
||||
timeframe TEXT NOT NULL,
|
||||
lines_json TEXT NOT NULL,
|
||||
color TEXT DEFAULT '#667eea',
|
||||
visible INTEGER DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
UNIQUE(user_id, name, exchange, market, timeframe)
|
||||
)
|
||||
"""
|
||||
self.data_cache.db.execute_sql(create_sql, params=[])
|
||||
|
||||
# Create index for scope queries
|
||||
index_sql = """
|
||||
CREATE INDEX IF NOT EXISTS idx_formations_scope
|
||||
ON formations(user_id, exchange, market, timeframe)
|
||||
"""
|
||||
self.data_cache.db.execute_sql(index_sql, params=[])
|
||||
|
||||
logger.info("Created formations table in database")
|
||||
except Exception as e:
|
||||
logger.error(f"Error ensuring formations table exists: {e}", exc_info=True)
|
||||
|
||||
def _load_formations_from_db(self) -> None:
|
||||
"""Load all formations from database into memory."""
|
||||
try:
|
||||
formations_df = self.data_cache.get_all_rows_from_datacache(cache_name='formations')
|
||||
if formations_df is not None and not formations_df.empty:
|
||||
for _, row in formations_df.iterrows():
|
||||
formation = Formation(
|
||||
tbl_key=row.get('tbl_key', ''),
|
||||
user_id=int(row.get('user_id', 0)),
|
||||
name=row.get('name', ''),
|
||||
formation_type=row.get('formation_type', ''),
|
||||
exchange=row.get('exchange', ''),
|
||||
market=row.get('market', ''),
|
||||
timeframe=row.get('timeframe', ''),
|
||||
lines_json=row.get('lines_json', '{}'),
|
||||
color=row.get('color', '#667eea'),
|
||||
visible=bool(row.get('visible', True)),
|
||||
created_at=int(row.get('created_at', 0)),
|
||||
updated_at=int(row.get('updated_at', 0))
|
||||
)
|
||||
self.formations[formation.tbl_key] = formation
|
||||
logger.info(f"Loaded {len(self.formations)} formations from database")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading formations from database: {e}", exc_info=True)
|
||||
|
||||
def create(self, user_id: int, data: dict) -> dict:
|
||||
"""
|
||||
Create a new formation.
|
||||
|
||||
:param user_id: ID of the user creating the formation
|
||||
:param data: Dictionary containing formation data
|
||||
:return: Dictionary with success status and formation data or error message
|
||||
"""
|
||||
try:
|
||||
# Validate required fields
|
||||
required_fields = ['name', 'formation_type', 'exchange', 'market', 'timeframe', 'lines_json']
|
||||
for field in required_fields:
|
||||
if field not in data or not data[field]:
|
||||
return {"success": False, "message": f"Missing required field: {field}"}
|
||||
|
||||
# Check for duplicate name in same scope
|
||||
name = data['name']
|
||||
exchange = data['exchange']
|
||||
market = data['market']
|
||||
timeframe = data['timeframe']
|
||||
|
||||
for formation in self.formations.values():
|
||||
if (formation.user_id == user_id and
|
||||
formation.name == name and
|
||||
formation.exchange == exchange and
|
||||
formation.market == market and
|
||||
formation.timeframe == timeframe):
|
||||
return {"success": False, "message": "A formation with this name already exists in this scope"}
|
||||
|
||||
# Generate unique key and timestamps
|
||||
tbl_key = str(uuid.uuid4())
|
||||
now = int(time.time())
|
||||
|
||||
# Prepare formation data
|
||||
columns = (
|
||||
"tbl_key", "user_id", "name", "formation_type", "exchange",
|
||||
"market", "timeframe", "lines_json", "color", "visible",
|
||||
"created_at", "updated_at"
|
||||
)
|
||||
|
||||
values = (
|
||||
tbl_key,
|
||||
user_id,
|
||||
name,
|
||||
data['formation_type'],
|
||||
exchange,
|
||||
market,
|
||||
timeframe,
|
||||
data['lines_json'] if isinstance(data['lines_json'], str) else json.dumps(data['lines_json']),
|
||||
data.get('color', '#667eea'),
|
||||
1, # visible
|
||||
now,
|
||||
now
|
||||
)
|
||||
|
||||
# Insert into database/cache via DataCache
|
||||
self.data_cache.insert_row_into_datacache(
|
||||
cache_name='formations',
|
||||
columns=columns,
|
||||
values=values
|
||||
)
|
||||
|
||||
# Create Formation object and add to memory cache
|
||||
formation = Formation(
|
||||
tbl_key=tbl_key,
|
||||
user_id=user_id,
|
||||
name=name,
|
||||
formation_type=data['formation_type'],
|
||||
exchange=exchange,
|
||||
market=market,
|
||||
timeframe=timeframe,
|
||||
lines_json=data['lines_json'] if isinstance(data['lines_json'], str) else json.dumps(data['lines_json']),
|
||||
color=data.get('color', '#667eea'),
|
||||
visible=True,
|
||||
created_at=now,
|
||||
updated_at=now
|
||||
)
|
||||
self.formations[tbl_key] = formation
|
||||
|
||||
logger.info(f"Created formation '{name}' with tbl_key {tbl_key}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Formation created successfully",
|
||||
"formation": self._formation_to_dict(formation)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating formation: {e}", exc_info=True)
|
||||
return {"success": False, "message": f"Error creating formation: {str(e)}"}
|
||||
|
||||
def update(self, user_id: int, data: dict) -> dict:
|
||||
"""
|
||||
Update an existing formation.
|
||||
|
||||
:param user_id: ID of the user updating the formation
|
||||
:param data: Dictionary containing formation data with tbl_key
|
||||
:return: Dictionary with success status
|
||||
"""
|
||||
try:
|
||||
tbl_key = data.get('tbl_key')
|
||||
if not tbl_key:
|
||||
return {"success": False, "message": "Missing tbl_key"}
|
||||
|
||||
# Find existing formation
|
||||
formation = self.formations.get(tbl_key)
|
||||
if not formation:
|
||||
return {"success": False, "message": "Formation not found"}
|
||||
|
||||
# Verify ownership
|
||||
if formation.user_id != user_id:
|
||||
return {"success": False, "message": "Not authorized to edit this formation"}
|
||||
|
||||
# Update fields
|
||||
now = int(time.time())
|
||||
update_data = {
|
||||
'updated_at': now
|
||||
}
|
||||
|
||||
if 'name' in data:
|
||||
formation.name = data['name']
|
||||
update_data['name'] = data['name']
|
||||
|
||||
if 'lines_json' in data:
|
||||
lines_json = data['lines_json'] if isinstance(data['lines_json'], str) else json.dumps(data['lines_json'])
|
||||
formation.lines_json = lines_json
|
||||
update_data['lines_json'] = lines_json
|
||||
|
||||
if 'color' in data:
|
||||
formation.color = data['color']
|
||||
update_data['color'] = data['color']
|
||||
|
||||
if 'visible' in data:
|
||||
formation.visible = bool(data['visible'])
|
||||
update_data['visible'] = int(data['visible'])
|
||||
|
||||
formation.updated_at = now
|
||||
|
||||
# Update in database/cache
|
||||
self.data_cache.modify_datacache_item(
|
||||
cache_name='formations',
|
||||
filter_vals=[('tbl_key', tbl_key)],
|
||||
field_names=tuple(update_data.keys()),
|
||||
new_values=tuple(update_data.values()),
|
||||
key=tbl_key,
|
||||
overwrite='tbl_key'
|
||||
)
|
||||
|
||||
logger.info(f"Updated formation '{formation.name}' (tbl_key: {tbl_key})")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Formation updated successfully",
|
||||
"formation": self._formation_to_dict(formation)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating formation: {e}", exc_info=True)
|
||||
return {"success": False, "message": f"Error updating formation: {str(e)}"}
|
||||
|
||||
def delete(self, user_id: int, tbl_key: str) -> dict:
|
||||
"""
|
||||
Delete a formation.
|
||||
|
||||
:param user_id: ID of the user deleting the formation
|
||||
:param tbl_key: Unique key of the formation to delete
|
||||
:return: Dictionary with success status
|
||||
"""
|
||||
try:
|
||||
# Find existing formation
|
||||
formation = self.formations.get(tbl_key)
|
||||
if not formation:
|
||||
return {"success": False, "message": "Formation not found"}
|
||||
|
||||
# Verify ownership
|
||||
if formation.user_id != user_id:
|
||||
return {"success": False, "message": "Not authorized to delete this formation"}
|
||||
|
||||
# Remove from database/cache
|
||||
self.data_cache.remove_row_from_datacache(
|
||||
cache_name='formations',
|
||||
filter_vals=[('tbl_key', tbl_key)]
|
||||
)
|
||||
|
||||
# Remove from memory cache
|
||||
del self.formations[tbl_key]
|
||||
|
||||
logger.info(f"Deleted formation '{formation.name}' (tbl_key: {tbl_key})")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Formation deleted successfully",
|
||||
"tbl_key": tbl_key
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting formation: {e}", exc_info=True)
|
||||
return {"success": False, "message": f"Error deleting formation: {str(e)}"}
|
||||
|
||||
def get_for_scope(self, user_id: int, exchange: str, market: str, timeframe: str) -> List[dict]:
|
||||
"""
|
||||
Get all formations for a specific scope (exchange/market/timeframe).
|
||||
|
||||
:param user_id: ID of the user
|
||||
:param exchange: Exchange name
|
||||
:param market: Market/trading pair
|
||||
:param timeframe: Timeframe
|
||||
:return: List of formation dictionaries
|
||||
"""
|
||||
result = []
|
||||
for formation in self.formations.values():
|
||||
if (formation.user_id == user_id and
|
||||
formation.exchange == exchange and
|
||||
formation.market == market and
|
||||
formation.timeframe == timeframe):
|
||||
result.append(self._formation_to_dict(formation))
|
||||
return result
|
||||
|
||||
def get_by_tbl_key(self, user_id: int, tbl_key: str) -> Optional[dict]:
|
||||
"""
|
||||
Get a formation by its tbl_key.
|
||||
|
||||
:param user_id: ID of the user (for ownership verification)
|
||||
:param tbl_key: Unique key of the formation
|
||||
:return: Formation dictionary or None
|
||||
"""
|
||||
formation = self.formations.get(tbl_key)
|
||||
if formation and formation.user_id == user_id:
|
||||
return self._formation_to_dict(formation)
|
||||
return None
|
||||
|
||||
def get_by_tbl_key_for_strategy(self, tbl_key: str, owner_user_id: int) -> Optional[dict]:
|
||||
"""
|
||||
Get a formation by tbl_key for strategy execution.
|
||||
|
||||
This uses the strategy owner's formations, not the current user's.
|
||||
Parallel to indicator_owner_id pattern.
|
||||
|
||||
:param tbl_key: Unique key of the formation
|
||||
:param owner_user_id: User ID of the strategy owner
|
||||
:return: Formation dictionary or None
|
||||
"""
|
||||
formation = self.formations.get(tbl_key)
|
||||
if formation and formation.user_id == owner_user_id:
|
||||
return self._formation_to_dict(formation)
|
||||
return None
|
||||
|
||||
def calculate_line_value(self, line: dict, timestamp: int) -> float:
|
||||
"""
|
||||
Calculate price at timestamp using linear interpolation/extrapolation.
|
||||
|
||||
This implements infinite line extension - works for any timestamp,
|
||||
past or future, by extrapolating the line defined by two points.
|
||||
|
||||
:param line: Dict with point1 and point2, each having 'time' and 'price'
|
||||
:param timestamp: Unix timestamp in seconds UTC
|
||||
:return: Extrapolated price value
|
||||
"""
|
||||
t1 = line['point1']['time']
|
||||
p1 = line['point1']['price']
|
||||
t2 = line['point2']['time']
|
||||
p2 = line['point2']['price']
|
||||
|
||||
# Handle vertical line (same timestamp)
|
||||
if t1 == t2:
|
||||
logger.warning(f"Vertical line detected (t1==t2={t1}), returning average price")
|
||||
return (p1 + p2) / 2
|
||||
|
||||
# Calculate slope and extrapolate
|
||||
slope = (p2 - p1) / (t2 - t1)
|
||||
return p1 + slope * (timestamp - t1)
|
||||
|
||||
def get_property_value(self, formation: dict, property_name: str, timestamp: int) -> Optional[float]:
|
||||
"""
|
||||
Get the value of a formation property at a given timestamp.
|
||||
|
||||
:param formation: Formation dictionary
|
||||
:param property_name: Name of the property ('line', 'upper', 'lower', 'midline', etc.)
|
||||
:param timestamp: Unix timestamp in seconds UTC
|
||||
:return: Price value or None
|
||||
"""
|
||||
try:
|
||||
lines_data = json.loads(formation.get('lines_json', '{}'))
|
||||
lines = lines_data.get('lines', [])
|
||||
|
||||
if not lines:
|
||||
return None
|
||||
|
||||
formation_type = formation.get('formation_type', '')
|
||||
|
||||
# Support/Resistance - single line
|
||||
if formation_type == 'support_resistance':
|
||||
if property_name == 'line' and len(lines) > 0:
|
||||
return self.calculate_line_value(lines[0], timestamp)
|
||||
|
||||
# Channel - two parallel lines
|
||||
elif formation_type == 'channel':
|
||||
if property_name == 'upper' and len(lines) > 0:
|
||||
return self.calculate_line_value(lines[0], timestamp)
|
||||
elif property_name == 'lower' and len(lines) > 1:
|
||||
return self.calculate_line_value(lines[1], timestamp)
|
||||
elif property_name == 'midline' and len(lines) >= 2:
|
||||
upper = self.calculate_line_value(lines[0], timestamp)
|
||||
lower = self.calculate_line_value(lines[1], timestamp)
|
||||
return (upper + lower) / 2
|
||||
|
||||
# Default: return first line value
|
||||
if len(lines) > 0:
|
||||
return self.calculate_line_value(lines[0], timestamp)
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting property value: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
def _formation_to_dict(self, formation: Formation) -> dict:
|
||||
"""Convert Formation object to dictionary."""
|
||||
return {
|
||||
'tbl_key': formation.tbl_key,
|
||||
'user_id': formation.user_id,
|
||||
'name': formation.name,
|
||||
'formation_type': formation.formation_type,
|
||||
'exchange': formation.exchange,
|
||||
'market': formation.market,
|
||||
'timeframe': formation.timeframe,
|
||||
'lines_json': formation.lines_json,
|
||||
'color': formation.color,
|
||||
'visible': formation.visible,
|
||||
'created_at': formation.created_at,
|
||||
'updated_at': formation.updated_at
|
||||
}
|
||||
|
|
@ -145,8 +145,6 @@ 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)
|
||||
|
|
@ -197,8 +195,6 @@ 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)
|
||||
|
|
@ -295,46 +291,6 @@ 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
|
||||
# ==============================
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ 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,
|
||||
|
|
@ -815,59 +814,6 @@ 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.
|
||||
|
|
|
|||
|
|
@ -414,16 +414,6 @@ 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.
|
||||
|
|
|
|||
|
|
@ -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, signals=None, formations=None):
|
||||
edm_client=None, external_indicators=None):
|
||||
""" Initialize the Backtesting class with a cache for back-tests """
|
||||
self.data_cache = data_cache
|
||||
self.strategies = strategies
|
||||
|
|
@ -50,8 +50,6 @@ 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(
|
||||
|
|
@ -885,11 +883,6 @@ 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)
|
||||
|
||||
|
|
|
|||
|
|
@ -1053,10 +1053,6 @@ 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;
|
||||
|
|
@ -1461,17 +1457,9 @@ class Strategies {
|
|||
if (data.success && data.strategy) {
|
||||
this.dataManager.addNewStrategy(data.strategy);
|
||||
this.uiManager.updateStrategiesHtml(this.dataManager.getAllStrategies());
|
||||
// Close the dialog only on success
|
||||
this.uiManager.hideForm();
|
||||
} else {
|
||||
console.error("Failed to create strategy:", data.message);
|
||||
// Keep dialog open and show error - user can fix the issue
|
||||
const errorMsg = data.message || 'Unknown error';
|
||||
if (errorMsg.toLowerCase().includes('name') || errorMsg.toLowerCase().includes('exists')) {
|
||||
alert(`Strategy name already exists. Please choose a different name.`);
|
||||
} else {
|
||||
alert(`Strategy creation failed: ${errorMsg}`);
|
||||
}
|
||||
alert(`Strategy creation failed: ${data.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1483,9 +1471,6 @@ class Strategies {
|
|||
if (data.success) {
|
||||
console.log("Strategy updated successfully:", data);
|
||||
|
||||
// Close the dialog on success
|
||||
this.uiManager.hideForm();
|
||||
|
||||
// Locate the strategy in the local state by its tbl_key
|
||||
const updatedStrategyKey = data.strategy.tbl_key;
|
||||
const updatedAt = data.updated_at;
|
||||
|
|
@ -1534,13 +1519,7 @@ class Strategies {
|
|||
}
|
||||
} else {
|
||||
console.error("Failed to update strategy:", data.message);
|
||||
// Keep dialog open and show error - user can fix the issue
|
||||
const errorMsg = data.message || 'Unknown error';
|
||||
if (errorMsg.toLowerCase().includes('name') || errorMsg.toLowerCase().includes('exists')) {
|
||||
alert(`Strategy name already exists. Please choose a different name.`);
|
||||
} else {
|
||||
alert(`Strategy update failed: ${errorMsg}`);
|
||||
}
|
||||
alert(`Strategy update failed: ${data.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1721,8 +1700,7 @@ class Strategies {
|
|||
// Determine message type based on action
|
||||
const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy';
|
||||
this.comms.sendToApp(messageType, strategyData);
|
||||
// Don't hide form here - wait for server response
|
||||
// Form will be hidden in handleStrategyCreated/handleStrategyUpdated on success
|
||||
this.uiManager.hideForm();
|
||||
} else {
|
||||
console.error("Comms instance not available or invalid action type.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
// 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.`);
|
||||
}
|
||||
|
|
@ -398,7 +398,7 @@ height: 500px;
|
|||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
z-index: 150; /* Above formation overlay (z-index: 100) */
|
||||
z-index: 99;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,8 @@ class Charts {
|
|||
/* A list of bound charts this is necessary for maintaining a dynamic
|
||||
number of charts with their position and zoom factors bound.*/
|
||||
this.bound_charts=[];
|
||||
// Store unsubscribe functions for cleanup when rebinding
|
||||
this._syncUnsubscribes = [];
|
||||
// Debounce timestamp to prevent infinite loop in chart synchronization
|
||||
this._lastSyncTime = 0;
|
||||
// Flag to prevent recursive sync
|
||||
this._isSyncing = false;
|
||||
// Only the main chart is created by default.
|
||||
this.create_main_chart();
|
||||
}
|
||||
|
|
@ -38,11 +34,9 @@ class Charts {
|
|||
// Initialize the candlestick series if price_history is available
|
||||
if (this.price_history && this.price_history.length > 0) {
|
||||
this.candleSeries.setData(this.price_history);
|
||||
console.log(`Candle series initialized with ${this.price_history.length} candles`);
|
||||
console.log('First candle:', this.price_history[0]);
|
||||
console.log('Last candle:', this.price_history[this.price_history.length - 1]);
|
||||
console.log('Candle series init:', this.price_history);
|
||||
} else {
|
||||
console.error('Price history is not available or is empty. Received:', this.price_history);
|
||||
console.error('Price history is not available or is empty.');
|
||||
}
|
||||
this.bind_charts(this.chart_1);
|
||||
}
|
||||
|
|
@ -164,74 +158,148 @@ class Charts {
|
|||
|
||||
|
||||
bind_charts(chart){
|
||||
// Add chart to list if not already present
|
||||
if (!this.bound_charts.includes(chart)) {
|
||||
// keep a list of charts and bind all their position and spacing.
|
||||
// Add (arg1) to bound_charts
|
||||
this.add_to_list(chart);
|
||||
// Get the number of objects in bound_charts
|
||||
let bcl = Object.keys(this.bound_charts).length;
|
||||
// if bound_charts has two element in it bind them
|
||||
if (bcl == 2) { this.bind2charts(); }
|
||||
|
||||
// if bound_charts has three elements in it bind them
|
||||
if (bcl == 3) { this.bind3charts(); }
|
||||
|
||||
// if bound_charts has four elements in it bind them
|
||||
if (bcl == 4) { this.bind4charts(); }
|
||||
|
||||
// if bound_charts has five elements in it bind them
|
||||
if (bcl == 5) { this.bind5charts(); }
|
||||
|
||||
return;
|
||||
}
|
||||
add_to_list(chart){
|
||||
// If the chart isn't already included in the list, add it.
|
||||
if ( !this.bound_charts.includes(chart) ){
|
||||
this.bound_charts.push(chart);
|
||||
}
|
||||
|
||||
// Only need to sync when we have 2+ charts
|
||||
if (this.bound_charts.length >= 2) {
|
||||
this._rebindAllCharts();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe all existing sync handlers and set up fresh ones.
|
||||
* This prevents cumulative handlers from conflicting.
|
||||
*/
|
||||
_rebindAllCharts() {
|
||||
// Unsubscribe all existing handlers using v5 API
|
||||
for (const {chart, handler} of this._syncUnsubscribes) {
|
||||
try {
|
||||
chart.timeScale().unsubscribeVisibleTimeRangeChange(handler);
|
||||
} catch (e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
this._syncUnsubscribes = [];
|
||||
|
||||
// Create a single sync handler that works with all current charts
|
||||
const chartCount = this.bound_charts.length;
|
||||
|
||||
for (let sourceIndex = 0; sourceIndex < chartCount; sourceIndex++) {
|
||||
const sourceChart = this.bound_charts[sourceIndex];
|
||||
|
||||
const syncHandler = () => {
|
||||
// Prevent recursive sync (when we apply options, it triggers another event)
|
||||
if (this._isSyncing) return;
|
||||
|
||||
bind2charts(){
|
||||
//On change in chart 1 change chart 2
|
||||
let syncHandler1 = (e) => {
|
||||
const now = Date.now();
|
||||
if (now - this._lastSyncTime < 50) return;
|
||||
|
||||
this._isSyncing = true;
|
||||
this._lastSyncTime = now;
|
||||
// Get the barSpacing(zoom) and position of 1st chart (v5 API: options().barSpacing)
|
||||
let barSpacing1 = this.bound_charts[0].timeScale().options().barSpacing;
|
||||
let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition();
|
||||
// Apply barSpacing(zoom) and position to 2nd chart.
|
||||
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
|
||||
}
|
||||
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncHandler1);
|
||||
|
||||
try {
|
||||
const barSpacing = sourceChart.timeScale().options().barSpacing;
|
||||
const scrollPosition = sourceChart.timeScale().scrollPosition();
|
||||
//On change in chart 2 change chart 1
|
||||
let syncHandler2 = (e) => {
|
||||
const now = Date.now();
|
||||
if (now - this._lastSyncTime < 50) return;
|
||||
this._lastSyncTime = now;
|
||||
// Get the barSpacing(zoom) and position of chart 2 (v5 API: options().barSpacing)
|
||||
let barSpacing2 = this.bound_charts[1].timeScale().options().barSpacing;
|
||||
let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition();
|
||||
// Apply barSpacing(zoom) and position to chart 1
|
||||
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
|
||||
}
|
||||
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncHandler2);
|
||||
}
|
||||
bind3charts(){
|
||||
|
||||
// Apply to all other charts
|
||||
for (let i = 0; i < this.bound_charts.length; i++) {
|
||||
//On change to chart 1 change chart 2 and 3
|
||||
let syncHandler = (e) => {
|
||||
const now = Date.now();
|
||||
if (now - this._lastSyncTime < 50) return;
|
||||
this._lastSyncTime = now;
|
||||
// Get the barSpacing(zoom) and position of chart 1 (v5 API)
|
||||
let barSpacing1 = this.bound_charts[0].timeScale().options().barSpacing;
|
||||
let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition();
|
||||
// Apply barSpacing(zoom) and position to new chart
|
||||
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
|
||||
this.bound_charts[2].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
|
||||
}
|
||||
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncHandler);
|
||||
|
||||
//On change to chart 2 change chart 1 and 3
|
||||
let syncHandler2 = (e) => {
|
||||
const now = Date.now();
|
||||
if (now - this._lastSyncTime < 50) return;
|
||||
this._lastSyncTime = now;
|
||||
// Get the barSpacing(zoom) and position of chart 2 (v5 API)
|
||||
let barSpacing2 = this.bound_charts[1].timeScale().options().barSpacing;
|
||||
let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition();
|
||||
// Apply barSpacing(zoom) and position to chart 1 and 3
|
||||
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
|
||||
this.bound_charts[2].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
|
||||
}
|
||||
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncHandler2);
|
||||
|
||||
//On change to chart 3 change chart 1 and 2
|
||||
let syncHandler3 = (e) => {
|
||||
const now = Date.now();
|
||||
if (now - this._lastSyncTime < 50) return;
|
||||
this._lastSyncTime = now;
|
||||
// Get the barSpacing(zoom) and position of new chart (v5 API)
|
||||
let barSpacing2 = this.bound_charts[2].timeScale().options().barSpacing;
|
||||
let scrollPosition2 = this.bound_charts[2].timeScale().scrollPosition();
|
||||
// Apply barSpacing(zoom) and position to parent chart
|
||||
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
|
||||
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
|
||||
}
|
||||
this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncHandler3);
|
||||
}
|
||||
|
||||
bind4charts(){
|
||||
// Sync all 4 charts together (v5 API: options().barSpacing)
|
||||
let syncFromChart = (sourceIndex) => {
|
||||
return (e) => {
|
||||
const now = Date.now();
|
||||
if (now - this._lastSyncTime < 50) return;
|
||||
this._lastSyncTime = now;
|
||||
let barSpacing = this.bound_charts[sourceIndex].timeScale().options().barSpacing;
|
||||
let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (i !== sourceIndex) {
|
||||
this.bound_charts[i].timeScale().applyOptions({
|
||||
rightOffset: scrollPosition,
|
||||
barSpacing: barSpacing
|
||||
});
|
||||
this.bound_charts[i].timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Use setTimeout to reset flag after current event loop
|
||||
setTimeout(() => {
|
||||
this._isSyncing = false;
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(0));
|
||||
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(1));
|
||||
this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(2));
|
||||
this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3));
|
||||
}
|
||||
|
||||
// Subscribe and store chart+handler for later unsubscribe
|
||||
sourceChart.timeScale().subscribeVisibleTimeRangeChange(syncHandler);
|
||||
this._syncUnsubscribes.push({chart: sourceChart, handler: syncHandler});
|
||||
bind5charts(){
|
||||
// Sync all 5 charts together (main + RSI + MACD + %B + Patterns) (v5 API)
|
||||
let syncFromChart = (sourceIndex) => {
|
||||
return (e) => {
|
||||
const now = Date.now();
|
||||
if (now - this._lastSyncTime < 50) return;
|
||||
this._lastSyncTime = now;
|
||||
let barSpacing = this.bound_charts[sourceIndex].timeScale().options().barSpacing;
|
||||
let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i !== sourceIndex) {
|
||||
this.bound_charts[i].timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(0));
|
||||
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(1));
|
||||
this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(2));
|
||||
this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3));
|
||||
this.bound_charts[4].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(4));
|
||||
}
|
||||
|
||||
// Set trade markers on chart for all trades in backtest results
|
||||
setTradeMarkers(trades) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ class Comms {
|
|||
|
||||
// Initialize the message queue
|
||||
this.messageQueue = [];
|
||||
this.maxQueueSize = 250;
|
||||
|
||||
// Save the userName
|
||||
this.userName = userName;
|
||||
|
|
@ -63,9 +62,8 @@ class Comms {
|
|||
query: { 'user_name': this.userName },
|
||||
transports: ['websocket'], // Optional: Force WebSocket transport
|
||||
autoConnect: true,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 1000
|
||||
reconnectionAttempts: 5, // Optional: Number of reconnection attempts
|
||||
reconnectionDelay: 1000 // Optional: Delay between reconnections
|
||||
});
|
||||
|
||||
// Handle connection events
|
||||
|
|
@ -120,24 +118,8 @@ class Comms {
|
|||
* Flushes the message queue by sending all queued messages.
|
||||
*/
|
||||
_flushMessageQueue() {
|
||||
// Prioritize control/CRUD messages first, keep only the latest candle_data.
|
||||
const queued = this.messageQueue.splice(0);
|
||||
const priority = [];
|
||||
let latestCandleMessage = null;
|
||||
|
||||
queued.forEach((msg) => {
|
||||
if (msg.messageType === 'candle_data') {
|
||||
latestCandleMessage = msg;
|
||||
} else {
|
||||
priority.push(msg);
|
||||
}
|
||||
});
|
||||
|
||||
const messagesToSend = latestCandleMessage
|
||||
? [...priority, latestCandleMessage]
|
||||
: priority;
|
||||
|
||||
messagesToSend.forEach(({ messageType, data }) => {
|
||||
while (this.messageQueue.length > 0) {
|
||||
const { messageType, data } = this.messageQueue.shift();
|
||||
this.socket.emit('message', {
|
||||
message_type: messageType,
|
||||
data: {
|
||||
|
|
@ -146,29 +128,6 @@ class Comms {
|
|||
}
|
||||
});
|
||||
console.log(`Comms: Sent queued message-> ${JSON.stringify({ messageType, data })}`);
|
||||
});
|
||||
}
|
||||
|
||||
_enqueueMessage(messageType, data) {
|
||||
if (messageType === 'candle_data') {
|
||||
// Candle data is high-frequency. Keep only the latest unsent candle update.
|
||||
const existingIndex = this.messageQueue.findIndex(msg => msg.messageType === 'candle_data');
|
||||
if (existingIndex !== -1) {
|
||||
this.messageQueue[existingIndex] = { messageType, data };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.messageQueue.push({ messageType, data });
|
||||
|
||||
// Prevent unbounded queue growth while disconnected.
|
||||
while (this.messageQueue.length > this.maxQueueSize) {
|
||||
const candleIndex = this.messageQueue.findIndex(msg => msg.messageType === 'candle_data');
|
||||
if (candleIndex !== -1) {
|
||||
this.messageQueue.splice(candleIndex, 1);
|
||||
} else {
|
||||
this.messageQueue.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -509,7 +468,7 @@ class Comms {
|
|||
// Not an error; message will be queued
|
||||
console.warn('Socket.IO connection is not open. Queuing message.');
|
||||
// Queue the message to be sent once connected
|
||||
this._enqueueMessage(messageType, data);
|
||||
this.messageQueue.push({ messageType, data });
|
||||
console.warn(`Comms: Queued message-> ${JSON.stringify({ messageType, data })} (Connection not open)`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,694 +0,0 @@
|
|||
/**
|
||||
* FormationsUIManager - Handles DOM updates and formation card rendering
|
||||
*/
|
||||
class FormationsUIManager {
|
||||
constructor() {
|
||||
this.targetEl = null;
|
||||
this.instructionsEl = null;
|
||||
this.nameControlsEl = null;
|
||||
this.nameInputEl = null;
|
||||
this.instructionTextEl = null;
|
||||
this.pointsStatusEl = null;
|
||||
this.onDeleteFormation = null;
|
||||
this.onEditFormation = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the UI elements.
|
||||
* @param {string} targetId - ID of the formations list container
|
||||
*/
|
||||
initUI(targetId) {
|
||||
this.targetEl = document.getElementById(targetId);
|
||||
if (!this.targetEl) {
|
||||
console.warn(`Formations container "${targetId}" not found.`);
|
||||
}
|
||||
|
||||
this.instructionsEl = document.getElementById('formation_drawing_instructions');
|
||||
this.nameControlsEl = document.getElementById('formation_name_controls');
|
||||
this.nameInputEl = document.getElementById('formation_name_input');
|
||||
this.instructionTextEl = document.getElementById('formation_instruction_text');
|
||||
this.pointsStatusEl = document.getElementById('formation_points_status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register callback for delete formation.
|
||||
* @param {Function} callback - Function to call when delete is clicked
|
||||
*/
|
||||
registerDeleteCallback(callback) {
|
||||
this.onDeleteFormation = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register callback for edit formation.
|
||||
* @param {Function} callback - Function to call when edit is clicked
|
||||
*/
|
||||
registerEditCallback(callback) {
|
||||
this.onEditFormation = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show drawing instructions for a formation type.
|
||||
* @param {string} type - Formation type
|
||||
* @param {number} pointsNeeded - Number of points needed
|
||||
*/
|
||||
showDrawingInstructions(type, pointsNeeded) {
|
||||
// Hide name controls if visible
|
||||
if (this.nameControlsEl) {
|
||||
this.nameControlsEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// Show instructions
|
||||
if (this.instructionsEl) {
|
||||
this.instructionsEl.style.display = 'block';
|
||||
}
|
||||
|
||||
// Set instruction text based on type
|
||||
const instructions = {
|
||||
'support_resistance': 'Click on chart to place a line. Drag anchors to adjust.',
|
||||
'channel': 'Click to place first line, then click to place parallel line.'
|
||||
};
|
||||
if (this.instructionTextEl) {
|
||||
this.instructionTextEl.textContent = instructions[type] || 'Click on chart to place points';
|
||||
}
|
||||
|
||||
// Update points status (for single-click types, show descriptive text)
|
||||
if (type === 'support_resistance') {
|
||||
if (this.pointsStatusEl) {
|
||||
this.pointsStatusEl.textContent = 'Click anywhere on the chart';
|
||||
this.pointsStatusEl.style.color = '#667eea';
|
||||
}
|
||||
} else if (type === 'channel') {
|
||||
if (this.pointsStatusEl) {
|
||||
this.pointsStatusEl.textContent = 'Step 1: Click to place primary line';
|
||||
this.pointsStatusEl.style.color = '#667eea';
|
||||
}
|
||||
} else {
|
||||
this.updatePointsStatus(0, pointsNeeded);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the points status display.
|
||||
* @param {number} current - Current points placed
|
||||
* @param {number} needed - Points needed
|
||||
*/
|
||||
updatePointsStatus(current, needed) {
|
||||
if (this.pointsStatusEl) {
|
||||
this.pointsStatusEl.textContent = `Points: ${current} / ${needed}`;
|
||||
// Change color when complete
|
||||
this.pointsStatusEl.style.color = current >= needed ? '#28a745' : '#667eea';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show name input after points are placed.
|
||||
*/
|
||||
showNameInput() {
|
||||
// Hide instructions
|
||||
if (this.instructionsEl) {
|
||||
this.instructionsEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// Show name controls
|
||||
if (this.nameControlsEl) {
|
||||
this.nameControlsEl.style.display = 'block';
|
||||
}
|
||||
|
||||
// Focus and clear input
|
||||
if (this.nameInputEl) {
|
||||
this.nameInputEl.value = '';
|
||||
this.nameInputEl.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide all drawing-related UI.
|
||||
*/
|
||||
hideAllDrawingUI() {
|
||||
if (this.instructionsEl) {
|
||||
this.instructionsEl.style.display = 'none';
|
||||
}
|
||||
if (this.nameControlsEl) {
|
||||
this.nameControlsEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy method - kept for compatibility
|
||||
*/
|
||||
showDrawingControls() {
|
||||
// Now handled by showDrawingInstructions and showNameInput
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy method - kept for compatibility
|
||||
*/
|
||||
hideDrawingControls() {
|
||||
this.hideAllDrawingUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formation name from input.
|
||||
* @returns {string} Formation name
|
||||
*/
|
||||
getFormationName() {
|
||||
return this.nameInputEl ? this.nameInputEl.value.trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all formations as cards.
|
||||
* @param {Array} formations - List of formation objects
|
||||
*/
|
||||
renderFormations(formations) {
|
||||
if (!this.targetEl) {
|
||||
console.warn("Formations container not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
this.targetEl.innerHTML = '';
|
||||
|
||||
if (!formations || formations.length === 0) {
|
||||
this.targetEl.innerHTML = '<p style="color: #888; font-size: 12px; padding: 10px;">No formations yet. Click a button above to draw one.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
formations.forEach(formation => {
|
||||
const card = this._createFormationCard(formation);
|
||||
this.targetEl.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a formation card element.
|
||||
* @param {Object} formation - Formation data
|
||||
* @returns {HTMLElement} Card element
|
||||
*/
|
||||
_createFormationCard(formation) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'formation-item';
|
||||
card.dataset.tblKey = formation.tbl_key;
|
||||
card.dataset.type = formation.formation_type;
|
||||
|
||||
// Format type for display
|
||||
const typeDisplay = this._formatType(formation.formation_type);
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="formation-icon">
|
||||
<span class="formation-name">${this._escapeHtml(formation.name)}</span>
|
||||
<span class="formation-type">${typeDisplay}</span>
|
||||
</div>
|
||||
<button class="edit-button" title="Edit">✎</button>
|
||||
<button class="delete-button" title="Delete">×</button>
|
||||
<div class="formation-hover">
|
||||
<strong>${this._escapeHtml(formation.name)}</strong>
|
||||
<div class="formation-details">
|
||||
<span>Type: ${typeDisplay}</span>
|
||||
<span><span class="formation-color-dot" style="background: ${formation.color}"></span>Color</span>
|
||||
<span>Scope: ${formation.exchange}/${formation.market}/${formation.timeframe}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add click handlers
|
||||
const deleteBtn = card.querySelector('.delete-button');
|
||||
deleteBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.onDeleteFormation) {
|
||||
this.onDeleteFormation(formation.tbl_key, formation.name);
|
||||
}
|
||||
});
|
||||
|
||||
const editBtn = card.querySelector('.edit-button');
|
||||
editBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.onEditFormation) {
|
||||
this.onEditFormation(formation.tbl_key);
|
||||
}
|
||||
});
|
||||
|
||||
// Card click to select
|
||||
card.addEventListener('click', () => {
|
||||
this._selectCard(card);
|
||||
if (this.onEditFormation) {
|
||||
this.onEditFormation(formation.tbl_key);
|
||||
}
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a card visually.
|
||||
* @param {HTMLElement} card - Card to select
|
||||
*/
|
||||
_selectCard(card) {
|
||||
// Deselect all
|
||||
this.targetEl.querySelectorAll('.formation-item').forEach(c => {
|
||||
c.classList.remove('selected');
|
||||
});
|
||||
// Select this one
|
||||
card.classList.add('selected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format formation type for display.
|
||||
* @param {string} type - Formation type
|
||||
* @returns {string} Formatted type
|
||||
*/
|
||||
_formatType(type) {
|
||||
const typeMap = {
|
||||
'support_resistance': 'Line',
|
||||
'channel': 'Channel',
|
||||
'triangle': 'Triangle',
|
||||
'head_shoulders': 'H&S',
|
||||
'double_bottom': 'Double Bottom',
|
||||
'double_top': 'Double Top'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS.
|
||||
* @param {string} str - String to escape
|
||||
* @returns {string} Escaped string
|
||||
*/
|
||||
_escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* FormationsDataManager - Manages in-memory formations data
|
||||
*/
|
||||
class FormationsDataManager {
|
||||
constructor() {
|
||||
this.formations = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all formations.
|
||||
* @param {Array} formations - List of formations
|
||||
*/
|
||||
setFormations(formations) {
|
||||
this.formations = Array.isArray(formations) ? formations : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all formations.
|
||||
* @returns {Array} List of formations
|
||||
*/
|
||||
getAllFormations() {
|
||||
return this.formations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new formation.
|
||||
* @param {Object} formation - Formation to add
|
||||
*/
|
||||
addFormation(formation) {
|
||||
if (formation) {
|
||||
this.formations.push(formation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing formation.
|
||||
* @param {Object} updatedFormation - Updated formation data
|
||||
*/
|
||||
updateFormation(updatedFormation) {
|
||||
const index = this.formations.findIndex(f => f.tbl_key === updatedFormation.tbl_key);
|
||||
if (index !== -1) {
|
||||
this.formations[index] = { ...this.formations[index], ...updatedFormation };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a formation by tbl_key.
|
||||
* @param {string} tblKey - Formation tbl_key to remove
|
||||
*/
|
||||
removeFormation(tblKey) {
|
||||
this.formations = this.formations.filter(f => f.tbl_key !== tblKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a formation by tbl_key.
|
||||
* @param {string} tblKey - Formation tbl_key
|
||||
* @returns {Object|null} Formation or null
|
||||
*/
|
||||
getFormation(tblKey) {
|
||||
return this.formations.find(f => f.tbl_key === tblKey) || null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formations - Main coordinator class for formations feature
|
||||
*/
|
||||
class Formations {
|
||||
constructor(ui) {
|
||||
this.ui = ui;
|
||||
this.comms = ui?.data?.comms;
|
||||
this.data = ui?.data;
|
||||
|
||||
this.dataManager = new FormationsDataManager();
|
||||
this.uiManager = new FormationsUIManager();
|
||||
this.overlay = null;
|
||||
|
||||
// Current drawing state
|
||||
this.drawingMode = null;
|
||||
this.currentScope = null;
|
||||
|
||||
// Set up callbacks
|
||||
this.uiManager.registerDeleteCallback(this.deleteFormation.bind(this));
|
||||
this.uiManager.registerEditCallback(this.selectFormation.bind(this));
|
||||
|
||||
this._initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the formations system.
|
||||
* @param {string} targetId - ID of formations list container
|
||||
*/
|
||||
initialize(targetId) {
|
||||
try {
|
||||
this.uiManager.initUI(targetId);
|
||||
|
||||
if (!this.comms) {
|
||||
console.error("Communications instance not available for Formations");
|
||||
return;
|
||||
}
|
||||
|
||||
// Register socket handlers
|
||||
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_name || 'binance',
|
||||
market: this.data?.trading_pair || window.bt_data?.trading_pair || 'BTC/USDT',
|
||||
timeframe: this.data?.interval || window.bt_data?.timeframe || '1h'
|
||||
};
|
||||
|
||||
// Fetch formations for current scope
|
||||
this.fetchFormations();
|
||||
|
||||
this._initialized = true;
|
||||
console.log("Formations initialized for scope:", this.currentScope);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error initializing Formations:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the SVG overlay for drawing.
|
||||
* @param {string} chartContainerId - ID of chart container
|
||||
* @param {Object} chart - Lightweight Charts instance
|
||||
* @param {Object} candleSeries - Candlestick series for coordinate conversion
|
||||
*/
|
||||
initOverlay(chartContainerId, chart, candleSeries) {
|
||||
if (typeof FormationOverlay !== 'undefined') {
|
||||
this.overlay = new FormationOverlay(chartContainerId, chart, candleSeries);
|
||||
this.overlay.setOnSaveCallback(this.saveFormation.bind(this));
|
||||
console.log("Formation overlay initialized");
|
||||
} else {
|
||||
console.warn("FormationOverlay class not loaded");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register socket handlers for formations.
|
||||
*/
|
||||
registerSocketHandlers() {
|
||||
this.comms.on('formations', this.handleFormationsResponse.bind(this));
|
||||
this.comms.on('formation_created', this.handleFormationCreated.bind(this));
|
||||
this.comms.on('formation_updated', this.handleFormationUpdated.bind(this));
|
||||
this.comms.on('formation_deleted', this.handleFormationDeleted.bind(this));
|
||||
this.comms.on('formation_error', this.handleFormationError.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch formations for current scope.
|
||||
*/
|
||||
fetchFormations() {
|
||||
if (!this.comms || !this.currentScope) return;
|
||||
|
||||
this.comms.sendToApp('request_formations', this.currentScope);
|
||||
}
|
||||
|
||||
// ================ Socket Handlers ================
|
||||
|
||||
/**
|
||||
* Handle formations list response.
|
||||
* @param {Object} data - Response with formations array
|
||||
*/
|
||||
handleFormationsResponse(data) {
|
||||
console.log("Received formations:", data);
|
||||
const formations = data.formations || [];
|
||||
this.dataManager.setFormations(formations);
|
||||
this.uiManager.renderFormations(formations);
|
||||
|
||||
// Render on overlay if available
|
||||
if (this.overlay) {
|
||||
this.overlay.clearAllFormations();
|
||||
formations.forEach(f => this.overlay.renderFormation(f));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle formation created event.
|
||||
* @param {Object} data - Response with formation data
|
||||
*/
|
||||
handleFormationCreated(data) {
|
||||
console.log("Formation created:", data);
|
||||
if (data.success && data.formation) {
|
||||
this.dataManager.addFormation(data.formation);
|
||||
this.uiManager.renderFormations(this.dataManager.getAllFormations());
|
||||
|
||||
if (this.overlay) {
|
||||
this.overlay.renderFormation(data.formation);
|
||||
}
|
||||
|
||||
this.uiManager.hideAllDrawingUI();
|
||||
this.drawingMode = null;
|
||||
} else {
|
||||
alert(`Failed to create formation: ${data.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle formation updated event.
|
||||
* @param {Object} data - Response with updated formation data
|
||||
*/
|
||||
handleFormationUpdated(data) {
|
||||
console.log("Formation updated:", data);
|
||||
if (data.success && data.formation) {
|
||||
this.dataManager.updateFormation(data.formation);
|
||||
this.uiManager.renderFormations(this.dataManager.getAllFormations());
|
||||
|
||||
if (this.overlay) {
|
||||
this.overlay.updateFormation(data.formation);
|
||||
}
|
||||
} else {
|
||||
alert(`Failed to update formation: ${data.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle formation deleted event.
|
||||
* @param {Object} data - Response with tbl_key of deleted formation
|
||||
*/
|
||||
handleFormationDeleted(data) {
|
||||
console.log("Formation deleted:", data);
|
||||
if (data.success && data.tbl_key) {
|
||||
this.dataManager.removeFormation(data.tbl_key);
|
||||
this.uiManager.renderFormations(this.dataManager.getAllFormations());
|
||||
|
||||
if (this.overlay) {
|
||||
this.overlay.removeFormation(data.tbl_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle formation error.
|
||||
* @param {Object} data - Error data
|
||||
*/
|
||||
handleFormationError(data) {
|
||||
console.error("Formation error:", data.message);
|
||||
alert(`Formation error: ${data.message}`);
|
||||
}
|
||||
|
||||
// ================ Drawing Methods ================
|
||||
|
||||
/**
|
||||
* Get points needed for a formation type.
|
||||
* @param {string} type - Formation type
|
||||
* @returns {number}
|
||||
*/
|
||||
_getPointsNeeded(type) {
|
||||
const pointsMap = {
|
||||
'support_resistance': 1, // Single click creates line with 3 anchors
|
||||
'channel': 2 // Two clicks: primary line + parallel line placement
|
||||
};
|
||||
return pointsMap[type] || 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start drawing a new formation.
|
||||
* @param {string} type - Formation type ('support_resistance', 'channel')
|
||||
*/
|
||||
startDrawing(type) {
|
||||
console.log("Starting drawing mode:", type);
|
||||
this.drawingMode = type;
|
||||
|
||||
const pointsNeeded = this._getPointsNeeded(type);
|
||||
|
||||
// Show drawing instructions (not name input yet)
|
||||
this.uiManager.showDrawingInstructions(type, pointsNeeded);
|
||||
|
||||
// Tell overlay to start drawing, with callbacks
|
||||
if (this.overlay) {
|
||||
this.overlay.setOnPointsChangedCallback(this._onPointsChanged.bind(this));
|
||||
this.overlay.setOnDraftReadyCallback(this._onDraftReady.bind(this));
|
||||
this.overlay.startDrawing(type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when points are added/changed during drawing.
|
||||
* @param {number} currentPoints - Current number of points
|
||||
* @param {number} pointsNeeded - Points needed for completion
|
||||
*/
|
||||
_onPointsChanged(currentPoints, pointsNeeded) {
|
||||
// For single-click formations, use _onDraftReady instead
|
||||
if (this.drawingMode === 'support_resistance') {
|
||||
return;
|
||||
}
|
||||
|
||||
// For channel, update status based on step
|
||||
if (this.drawingMode === 'channel') {
|
||||
if (this.uiManager.pointsStatusEl) {
|
||||
if (currentPoints === 1) {
|
||||
this.uiManager.pointsStatusEl.textContent = 'Step 2: Move mouse and click to place parallel line';
|
||||
this.uiManager.pointsStatusEl.style.color = '#667eea';
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the UI status for other multi-point drawings
|
||||
this.uiManager.updatePointsStatus(currentPoints, pointsNeeded);
|
||||
|
||||
// If we have enough points, show the name input
|
||||
if (currentPoints >= pointsNeeded) {
|
||||
this.uiManager.showNameInput();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a draft formation is ready (line/channel placed, can be named).
|
||||
*/
|
||||
_onDraftReady() {
|
||||
// Update status based on formation type
|
||||
if (this.uiManager.pointsStatusEl) {
|
||||
if (this.drawingMode === 'channel') {
|
||||
this.uiManager.pointsStatusEl.textContent = 'Channel placed! Drag anchors to adjust.';
|
||||
} else {
|
||||
this.uiManager.pointsStatusEl.textContent = 'Line placed! Drag anchors to adjust.';
|
||||
}
|
||||
this.uiManager.pointsStatusEl.style.color = '#28a745';
|
||||
}
|
||||
|
||||
// Show name input
|
||||
this.uiManager.showNameInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the current drawing.
|
||||
*/
|
||||
completeDrawing() {
|
||||
const name = this.uiManager.getFormationName();
|
||||
if (!name) {
|
||||
alert("Please enter a formation name");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.overlay) {
|
||||
this.overlay.completeDrawing(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current drawing.
|
||||
*/
|
||||
cancelDrawing() {
|
||||
this.drawingMode = null;
|
||||
this.uiManager.hideAllDrawingUI();
|
||||
|
||||
if (this.overlay) {
|
||||
this.overlay.cancelDrawing();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a formation (callback from overlay).
|
||||
* @param {Object} formationData - Formation data to save
|
||||
*/
|
||||
saveFormation(formationData) {
|
||||
if (!this.comms) return;
|
||||
|
||||
const payload = {
|
||||
...formationData,
|
||||
...this.currentScope
|
||||
};
|
||||
|
||||
this.comms.sendToApp('new_formation', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a formation for editing.
|
||||
* @param {string} tblKey - Formation tbl_key
|
||||
*/
|
||||
selectFormation(tblKey) {
|
||||
console.log("Selecting formation:", tblKey);
|
||||
if (this.overlay) {
|
||||
this.overlay.selectFormation(tblKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a formation.
|
||||
* @param {string} tblKey - Formation tbl_key
|
||||
* @param {string} name - Formation name (for confirmation)
|
||||
*/
|
||||
deleteFormation(tblKey, name) {
|
||||
if (!confirm(`Delete formation "${name}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.comms) {
|
||||
if (!this.comms.connectionOpen) {
|
||||
alert('Server connection is offline. Delete will be sent after reconnect.');
|
||||
}
|
||||
this.comms.sendToApp('delete_formation', { tbl_key: tblKey });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a formation's lines (called from overlay after drag).
|
||||
* @param {string} tblKey - Formation tbl_key
|
||||
* @param {Object} linesData - Updated lines data
|
||||
*/
|
||||
updateFormationLines(tblKey, linesData) {
|
||||
if (!this.comms) return;
|
||||
|
||||
this.comms.sendToApp('edit_formation', {
|
||||
tbl_key: tblKey,
|
||||
lines_json: JSON.stringify(linesData)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ class User_Interface {
|
|||
this.users = new Users();
|
||||
this.indicators = new Indicators(this.data.comms);
|
||||
this.signals = new Signals(this);
|
||||
this.formations = new Formations(this);
|
||||
this.backtesting = new Backtesting(this);
|
||||
this.statistics = new Statistics(this.data.comms);
|
||||
this.account = new Account();
|
||||
|
|
@ -79,8 +78,6 @@ class User_Interface {
|
|||
this.indicators.addToCharts(this.charts, ind_init_data);
|
||||
|
||||
this.signals.initialize('signal_list', 'new_sig_form');
|
||||
this.formations.initialize('formations_list');
|
||||
this.formations.initOverlay(this.data.chart1_id, this.charts.chart_1, this.charts.candleSeries);
|
||||
this.alerts.set_target();
|
||||
this.alerts.initialize(this.data.comms);
|
||||
this.controls.init_TP_selector();
|
||||
|
|
|
|||
|
|
@ -101,10 +101,10 @@ class Indicator {
|
|||
priceScaleId: 'volume_ps',
|
||||
});
|
||||
// v5: scaleMargins must be set on the price scale, not series options
|
||||
// Volume should only take up bottom 15% of the chart
|
||||
// Volume should only take up bottom 30% of the chart
|
||||
chart.priceScale('volume_ps').applyOptions({
|
||||
scaleMargins: {
|
||||
top: 0.85,
|
||||
top: 0.7,
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
|
|
@ -481,7 +481,7 @@ class Volume extends Indicator {
|
|||
constructor(name, chart) {
|
||||
super(name);
|
||||
this.addHist(name, chart);
|
||||
// Note: scaleMargins are set in addHist() on the price scale (v5 API)
|
||||
this.hist[name].applyOptions({ scaleMargins: { top: 0.95, bottom: 0.0 } });
|
||||
this.outputs = ['value'];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@
|
|||
{% include "indicators_hud.html" %}
|
||||
<button class="collapsible bg_blue">Signals</button>
|
||||
{% include "signals_hud.html" %}
|
||||
<button class="collapsible bg_blue">Formations</button>
|
||||
{% include "formations_hud.html" %}
|
||||
<button class="collapsible bg_blue">Strategies</button>
|
||||
{% include "strategies_hud.html" %}
|
||||
<button class="collapsible bg_blue">Statistics</button>
|
||||
|
|
|
|||
|
|
@ -1,267 +0,0 @@
|
|||
<div class="content" id="formations_panel">
|
||||
<h4 style="margin: 5px 0 10px 0;">Draw Formation</h4>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 0 5px;">
|
||||
<button class="btn btn-sm" onclick="UI.formations.startDrawing('support_resistance')">
|
||||
<span style="font-size: 18px;">━</span> Line
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="UI.formations.startDrawing('channel')">
|
||||
<span style="font-size: 18px;">═</span> Channel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Drawing instructions (shown when drawing starts) -->
|
||||
<div id="formation_drawing_instructions" style="display: none; margin-top: 10px; padding: 10px; background: #2a2a2a; border-radius: 5px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span id="formation_instruction_text" style="color: #8899aa; font-size: 12px;">Click on chart to place points...</span>
|
||||
<button class="btn btn-sm" style="background: #dc3545; padding: 4px 8px;" onclick="UI.formations.cancelDrawing()">Cancel</button>
|
||||
</div>
|
||||
<div style="margin-top: 8px; color: #667eea; font-size: 11px;">
|
||||
<span id="formation_points_status">Points: 0 / 2</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name input (shown after points are placed) -->
|
||||
<div id="formation_name_controls" style="display: none; margin-top: 10px; padding: 10px; background: #2a2a2a; border-radius: 5px;">
|
||||
<div style="margin-bottom: 8px; color: #28a745; font-size: 12px;">Points placed. Enter a name:</div>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<input type="text" id="formation_name_input" placeholder="Formation name"
|
||||
style="flex: 1; padding: 5px; border-radius: 3px; border: 1px solid #444; background: #1e1e1e; color: #e0e0e0;">
|
||||
<button class="btn btn-sm" style="background: #28a745;" onclick="UI.formations.completeDrawing()">Save</button>
|
||||
<button class="btn btn-sm" style="background: #dc3545;" onclick="UI.formations.cancelDrawing()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h3>Formations</h3>
|
||||
<div class="formations-container" id="formations_list"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Formations container - flex grid for cards */
|
||||
.formations-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Individual formation card */
|
||||
.formation-item {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(145deg, #2a3a5a, #1e2a40);
|
||||
box-shadow: 5px 5px 10px #151f30, -5px -5px 10px #253550;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
overflow: visible;
|
||||
border: 2px solid #3a5a8a;
|
||||
}
|
||||
|
||||
.formation-item:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 8px 8px 15px #151f30, -8px -8px 15px #253550;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.formation-item.selected {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 15px rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
|
||||
/* Formation icon area */
|
||||
.formation-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* CSS-based formation icon */
|
||||
.formation-icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #4a5cd8 100%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Line icon */
|
||||
.formation-item[data-type="support_resistance"] .formation-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
width: 30px;
|
||||
height: 3px;
|
||||
background: white;
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
|
||||
/* Channel icon - two parallel lines */
|
||||
.formation-item[data-type="channel"] .formation-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
width: 28px;
|
||||
height: 3px;
|
||||
background: white;
|
||||
box-shadow: 0 10px 0 white;
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
|
||||
/* Formation name */
|
||||
.formation-name {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: #e0e0e0;
|
||||
margin-top: 50px;
|
||||
word-wrap: break-word;
|
||||
max-width: 90px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Formation type label */
|
||||
.formation-type {
|
||||
font-size: 9px;
|
||||
color: #8899aa;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Delete button */
|
||||
.formation-item .delete-button {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: 2px solid #1e2a40;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.formation-item:hover .delete-button {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.formation-item .delete-button:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Edit button */
|
||||
.formation-item .edit-button {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: 18px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: 2px solid #1e2a40;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.formation-item:hover .edit-button {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.formation-item .edit-button:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Hover details panel */
|
||||
.formation-hover {
|
||||
position: absolute;
|
||||
top: 110px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 180px;
|
||||
padding: 10px;
|
||||
background: #2a3a5a;
|
||||
border: 1px solid #4a6a9a;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
z-index: 100;
|
||||
display: none;
|
||||
font-size: 11px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.formation-item:hover .formation-hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.formation-hover strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #e0e0e0;
|
||||
border-bottom: 1px solid #4a6a9a;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.formation-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.formation-details span {
|
||||
color: #aabbcc;
|
||||
}
|
||||
|
||||
/* Color indicator */
|
||||
.formation-color-dot {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.btn-sm {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
background: linear-gradient(145deg, #3a4a6a, #2a3a5a);
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-sm:hover {
|
||||
background: linear-gradient(145deg, #4a5a7a, #3a4a6a);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-sm:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -29,8 +29,6 @@
|
|||
<script src="{{ url_for('static', filename='communication.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='formation_overlay.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='formations.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='Statistics.js') }}?v=1"></script>
|
||||
|
|
|
|||
|
|
@ -201,11 +201,6 @@ and you set fee to 50%, you earn $0.50 per profitable trade.
|
|||
<!-- Signal blocks will be added here dynamically -->
|
||||
</category>
|
||||
|
||||
<!-- Formations Category -->
|
||||
<category name="Formations" colour="290" tooltip="Use chart formations (trendlines, channels) in strategy logic">
|
||||
<!-- Formation blocks will be added here dynamically -->
|
||||
</category>
|
||||
|
||||
<!-- Balances Subcategory -->
|
||||
<category name="Balances" colour="#E69500" tooltip="Access strategy and account balance information">
|
||||
<label text="Track your trading capital"></label>
|
||||
|
|
|
|||
Loading…
Reference in New Issue