forexsmartbot 3.3.0__tar.gz → 3.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/PKG-INFO +1 -1
  2. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/__init__.py +1 -1
  3. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/cloud/__init__.py +8 -1
  4. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/cloud/companion_api.py +256 -2
  5. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/cloud/remote_monitor.py +209 -12
  6. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/monitoring/strategy_monitor.py +53 -16
  7. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot.egg-info/PKG-INFO +1 -1
  8. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/pyproject.toml +1 -1
  9. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/tests/test_companion_q2.py +124 -0
  10. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/LICENSE +0 -0
  11. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/README.md +0 -0
  12. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/__init__.py +0 -0
  13. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/brokers/__init__.py +0 -0
  14. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/brokers/ib_tws_broker.py +0 -0
  15. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/brokers/mt4_broker.py +0 -0
  16. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/brokers/paper_broker.py +0 -0
  17. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/brokers/rest_broker.py +0 -0
  18. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/__init__.py +0 -0
  19. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/alpha_vantage_provider.py +0 -0
  20. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/config.py +0 -0
  21. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/csv_provider.py +0 -0
  22. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/dummy_provider.py +0 -0
  23. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/mt4_provider.py +0 -0
  24. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/multi_provider.py +0 -0
  25. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/oanda_provider.py +0 -0
  26. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/stooq_provider.py +0 -0
  27. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/twelve_data_provider.py +0 -0
  28. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/adapters/data/yfinance_provider.py +0 -0
  29. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/__init__.py +0 -0
  30. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/chart_patterns.py +0 -0
  31. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/correlation_matrix.py +0 -0
  32. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/economic_calendar.py +0 -0
  33. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/enhanced_chart.py +0 -0
  34. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/live_metrics.py +0 -0
  35. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/market_depth.py +0 -0
  36. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/performance_attribution.py +0 -0
  37. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/portfolio_analytics.py +0 -0
  38. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/risk_analytics.py +0 -0
  39. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/strategy_comparison.py +0 -0
  40. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/analytics/trade_journal.py +0 -0
  41. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/brokers/mt4_bridge.py +0 -0
  42. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/builder/__init__.py +0 -0
  43. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/builder/code_generator.py +0 -0
  44. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/builder/strategy_builder.py +0 -0
  45. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/builder/strategy_template.py +0 -0
  46. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/cloud/api_client.py +0 -0
  47. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/cloud/api_server.py +0 -0
  48. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/cloud/auth_utils.py +0 -0
  49. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/cloud/cloud_sync.py +0 -0
  50. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/cloud/error_handler.py +0 -0
  51. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/cloud/mobile_api.py +0 -0
  52. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/cloud/security.py +0 -0
  53. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/core/backtester.py +0 -0
  54. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/core/config.py +0 -0
  55. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/core/interfaces.py +0 -0
  56. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/core/paper_broker.py +0 -0
  57. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/core/portfolio.py +0 -0
  58. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/core/risk.py +0 -0
  59. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/core/risk_engine.py +0 -0
  60. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/core/strategy.py +0 -0
  61. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/core/trade_manager.py +0 -0
  62. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/gui/main_window.py +0 -0
  63. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/gui/theme.py +0 -0
  64. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/marketplace/__init__.py +0 -0
  65. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/marketplace/marketplace.py +0 -0
  66. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/marketplace/strategy_rating.py +0 -0
  67. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/monitoring/__init__.py +0 -0
  68. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/monitoring/health_check.py +0 -0
  69. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/monitoring/performance_tracker.py +0 -0
  70. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/optimization/__init__.py +0 -0
  71. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/optimization/adaptive_parameters.py +0 -0
  72. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/optimization/genetic_optimizer.py +0 -0
  73. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/optimization/hyperparameter_optimizer.py +0 -0
  74. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/optimization/monte_carlo.py +0 -0
  75. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/optimization/multi_objective_optimizer.py +0 -0
  76. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/optimization/parameter_sensitivity.py +0 -0
  77. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/optimization/walk_forward.py +0 -0
  78. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/__init__.py +0 -0
  79. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/backtest.py +0 -0
  80. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/calculation_engine.py +0 -0
  81. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/controller.py +0 -0
  82. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/data_streaming.py +0 -0
  83. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/enhanced_backtest.py +0 -0
  84. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/fast_trading_controller.py +0 -0
  85. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/gpu_backtest.py +0 -0
  86. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/language_manager.py +0 -0
  87. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/notification_service.py +0 -0
  88. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/persistence.py +0 -0
  89. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/services/startup_manager.py +0 -0
  90. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/__init__.py +0 -0
  91. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/adaptive_trend_flow.py +0 -0
  92. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/breakout_atr.py +0 -0
  93. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/ensemble_ml_strategy.py +0 -0
  94. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/fear_index.py +0 -0
  95. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/lstm_strategy.py +0 -0
  96. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/mean_reversion.py +0 -0
  97. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/ml_adaptive_supertrend.py +0 -0
  98. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/momentum_breakout.py +0 -0
  99. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/multi_timeframe_strategy.py +0 -0
  100. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/news_trading.py +0 -0
  101. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/rl_strategy.py +0 -0
  102. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/rsi_reversion.py +0 -0
  103. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/scalping_ma.py +0 -0
  104. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/sma_crossover.py +0 -0
  105. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/svm_strategy.py +0 -0
  106. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/strategies/transformer_strategy.py +0 -0
  107. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/testing/__init__.py +0 -0
  108. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/testing/ci_cd_pipeline.py +0 -0
  109. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/testing/paper_account_tester.py +0 -0
  110. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/testing/strategy_sandbox.py +0 -0
  111. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/__init__.py +0 -0
  112. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/analytics_dialog.py +0 -0
  113. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/analytics_widget.py +0 -0
  114. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/backtest_dialog.py +0 -0
  115. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/charts.py +0 -0
  116. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/cloud_dialog.py +0 -0
  117. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/enhanced_charts.py +0 -0
  118. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/enhanced_main_window.py +0 -0
  119. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/export_dialog.py +0 -0
  120. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/main_window.py +0 -0
  121. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/marketplace_dialog.py +0 -0
  122. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/monitoring_dialog.py +0 -0
  123. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/monitoring_widget.py +0 -0
  124. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/optimization_dialog.py +0 -0
  125. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/settings_dialog.py +0 -0
  126. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/simple_chart.py +0 -0
  127. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/strategy_builder_dialog.py +0 -0
  128. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/theme.py +0 -0
  129. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/ui/tradingview_chart.py +0 -0
  130. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/utils/__init__.py +0 -0
  131. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/utils/backtest_utils.py +0 -0
  132. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/utils/gpu_numpy.py +0 -0
  133. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/utils/gpu_utils.py +0 -0
  134. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot/utils/native_indicators.py +0 -0
  135. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot.egg-info/SOURCES.txt +0 -0
  136. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot.egg-info/dependency_links.txt +0 -0
  137. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot.egg-info/entry_points.txt +0 -0
  138. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot.egg-info/requires.txt +0 -0
  139. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/forexsmartbot.egg-info/top_level.txt +0 -0
  140. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/setup.cfg +0 -0
  141. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/tests/test_backtest_dialog_ui.py +0 -0
  142. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/tests/test_cloud_hardening.py +0 -0
  143. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/tests/test_config.py +0 -0
  144. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/tests/test_live_metrics.py +0 -0
  145. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/tests/test_multi_timeframe_strategy.py +0 -0
  146. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/tests/test_native_indicators.py +0 -0
  147. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/tests/test_risk.py +0 -0
  148. {forexsmartbot-3.3.0 → forexsmartbot-3.3.1}/tests/test_strategies.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forexsmartbot
3
- Version: 3.3.0
3
+ Version: 3.3.1
4
4
  Summary: Professional forex trading bot with advanced risk management and strategy plugins
5
5
  Author-email: VoxHash <contact@voxhash.dev>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  # ForexSmartBot Package
2
- __version__ = "3.3.0"
2
+ __version__ = "3.3.1"
@@ -7,7 +7,12 @@ from .cloud_sync import CloudSyncManager
7
7
  from .remote_monitor import RemoteMonitorServer
8
8
  from .api_server import APIServer, WebSocketServer
9
9
  from .mobile_api import MobileAppAPI, PushNotificationService
10
- from .companion_api import CompanionAPI, WebhookNotifier
10
+ from .companion_api import (
11
+ CompanionAPI,
12
+ CompanionActionQueue,
13
+ CompanionPairingStore,
14
+ WebhookNotifier,
15
+ )
11
16
  from .security import SecurityManager, RateLimiter
12
17
  from .api_client import APIClient, WebSocketClient
13
18
  from .error_handler import CloudErrorHandler, CloudHealthMonitor, retry_call
@@ -21,6 +26,8 @@ __all__ = [
21
26
  'MobileAppAPI',
22
27
  'PushNotificationService',
23
28
  'CompanionAPI',
29
+ 'CompanionActionQueue',
30
+ 'CompanionPairingStore',
24
31
  'WebhookNotifier',
25
32
  'SecurityManager',
26
33
  'RateLimiter',
@@ -8,9 +8,12 @@ Broker keys and order routing stay on the desktop host.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import json
11
12
  import logging
13
+ import secrets
12
14
  import threading
13
- from datetime import datetime
15
+ import uuid
16
+ from datetime import datetime, timedelta
14
17
  from typing import Any, Callable, Dict, List, Optional
15
18
  from urllib.parse import urlparse
16
19
 
@@ -30,6 +33,127 @@ logger = logging.getLogger(__name__)
30
33
  PortfolioProvider = Callable[[], Dict[str, Any]]
31
34
  AlertsProvider = Callable[[], List[Dict[str, Any]]]
32
35
  StrategiesProvider = Callable[[], Dict[str, Any]]
36
+ HistoryProvider = Callable[[Optional[str], int], Dict[str, Any]]
37
+
38
+ PAIRING_SCHEME = "fsb-companion"
39
+ PAIRING_TTL_SECONDS = 300
40
+ ALLOWED_ACTIONS = frozenset({"start_trading", "stop_trading", "pause_trading"})
41
+
42
+
43
+ class CompanionActionQueue:
44
+ """Pending high-level companion actions awaiting host approve/reject."""
45
+
46
+ def __init__(self, *, max_pending: int = 64):
47
+ self.max_pending = max_pending
48
+ self._lock = threading.Lock()
49
+ self._items: Dict[str, Dict[str, Any]] = {}
50
+
51
+ def request(self, action: str, *, requested_by: str = "companion") -> Optional[Dict[str, Any]]:
52
+ action = (action or "").strip()
53
+ if action not in ALLOWED_ACTIONS:
54
+ return None
55
+ with self._lock:
56
+ pending = [i for i in self._items.values() if i["status"] == "pending"]
57
+ if len(pending) >= self.max_pending:
58
+ return None
59
+ item_id = uuid.uuid4().hex
60
+ item = {
61
+ "id": item_id,
62
+ "action": action,
63
+ "status": "pending",
64
+ "requested_by": requested_by,
65
+ "created_at": datetime.now().isoformat(),
66
+ "resolved_at": None,
67
+ "reason": None,
68
+ }
69
+ self._items[item_id] = item
70
+ return dict(item)
71
+
72
+ def list_pending(self) -> List[Dict[str, Any]]:
73
+ with self._lock:
74
+ return [dict(i) for i in self._items.values() if i["status"] == "pending"]
75
+
76
+ def get(self, action_id: str) -> Optional[Dict[str, Any]]:
77
+ with self._lock:
78
+ item = self._items.get(action_id)
79
+ return dict(item) if item else None
80
+
81
+ def resolve(
82
+ self, action_id: str, *, approve: bool, reason: Optional[str] = None
83
+ ) -> Optional[Dict[str, Any]]:
84
+ with self._lock:
85
+ item = self._items.get(action_id)
86
+ if not item or item["status"] != "pending":
87
+ return None
88
+ item["status"] = "approved" if approve else "rejected"
89
+ item["resolved_at"] = datetime.now().isoformat()
90
+ item["reason"] = reason
91
+ return dict(item)
92
+
93
+
94
+ class CompanionPairingStore:
95
+ """Short-lived pairing sessions for QR onboarding (phone redeems without prior key)."""
96
+
97
+ def __init__(self, *, ttl_seconds: int = PAIRING_TTL_SECONDS):
98
+ self.ttl_seconds = ttl_seconds
99
+ self._lock = threading.Lock()
100
+ self._sessions: Dict[str, Dict[str, Any]] = {}
101
+
102
+ def create(
103
+ self,
104
+ *,
105
+ base_url: str,
106
+ api_key: str,
107
+ pin_sha256: Optional[str] = None,
108
+ ) -> Dict[str, Any]:
109
+ code = secrets.token_urlsafe(12)
110
+ expires = datetime.now() + timedelta(seconds=self.ttl_seconds)
111
+ payload = {
112
+ "v": 1,
113
+ "scheme": PAIRING_SCHEME,
114
+ "base_url": base_url.rstrip("/"),
115
+ "pairing_code": code,
116
+ "expires_at": expires.isoformat(),
117
+ "pin_sha256": pin_sha256 or "",
118
+ }
119
+ with self._lock:
120
+ self._sessions[code] = {
121
+ "api_key": api_key,
122
+ "base_url": base_url.rstrip("/"),
123
+ "pin_sha256": pin_sha256 or "",
124
+ "expires_at": expires,
125
+ "redeemed": False,
126
+ }
127
+ # Drop expired
128
+ now = datetime.now()
129
+ dead = [k for k, v in self._sessions.items() if v["expires_at"] < now]
130
+ for k in dead:
131
+ del self._sessions[k]
132
+ return {
133
+ "pairing_code": code,
134
+ "expires_at": expires.isoformat(),
135
+ "ttl_seconds": self.ttl_seconds,
136
+ "qr_payload": payload,
137
+ "qr_text": json.dumps(payload, separators=(",", ":")),
138
+ }
139
+
140
+ def redeem(self, pairing_code: str) -> Optional[Dict[str, Any]]:
141
+ code = (pairing_code or "").strip()
142
+ with self._lock:
143
+ session = self._sessions.get(code)
144
+ if not session:
145
+ return None
146
+ if session["expires_at"] < datetime.now() or session["redeemed"]:
147
+ self._sessions.pop(code, None)
148
+ return None
149
+ session["redeemed"] = True
150
+ return {
151
+ "scheme": PAIRING_SCHEME,
152
+ "base_url": session["base_url"],
153
+ "api_key": session["api_key"],
154
+ "pin_sha256": session["pin_sha256"],
155
+ "redeemed_at": datetime.now().isoformat(),
156
+ }
33
157
 
34
158
 
35
159
  def _normalize_portfolio(raw: Dict[str, Any]) -> Dict[str, Any]:
@@ -143,7 +267,10 @@ class CompanionAPI:
143
267
  portfolio_provider: Optional[PortfolioProvider] = None,
144
268
  alerts_provider: Optional[AlertsProvider] = None,
145
269
  strategies_provider: Optional[StrategiesProvider] = None,
270
+ history_provider: Optional[HistoryProvider] = None,
146
271
  allow_control: bool = False,
272
+ public_base_url: Optional[str] = None,
273
+ pin_sha256: Optional[str] = None,
147
274
  url_prefix: str = "/api/v1/companion",
148
275
  ):
149
276
  self.app = app or Flask(__name__)
@@ -154,13 +281,18 @@ class CompanionAPI:
154
281
  require_api_key = not is_insecure_secret(self.api_key)
155
282
  self.require_api_key = require_api_key
156
283
  self.allow_control = allow_control
284
+ self.public_base_url = (public_base_url or "").rstrip("/")
285
+ self.pin_sha256 = (pin_sha256 or "").strip()
157
286
  self.url_prefix = url_prefix.rstrip("/") or "/api/v1/companion"
158
287
 
159
288
  self._portfolio_provider = portfolio_provider or (lambda: {})
160
289
  self._alerts_provider = alerts_provider or (lambda: [])
161
290
  self._strategies_provider = strategies_provider or (lambda: {"strategies": {}})
291
+ self._history_provider = history_provider
162
292
  self.webhook_notifier = WebhookNotifier()
163
293
  self.control_callbacks: Dict[str, Callable[[], bool]] = {}
294
+ self.action_queue = CompanionActionQueue()
295
+ self.pairing_store = CompanionPairingStore()
164
296
 
165
297
  self.setup_routes()
166
298
 
@@ -173,8 +305,14 @@ class CompanionAPI:
173
305
  def set_strategies_provider(self, provider: StrategiesProvider) -> None:
174
306
  self._strategies_provider = provider
175
307
 
308
+ def set_history_provider(self, provider: HistoryProvider) -> None:
309
+ self._history_provider = provider
310
+
311
+ def set_public_base_url(self, base_url: str) -> None:
312
+ self.public_base_url = (base_url or "").rstrip("/")
313
+
176
314
  def register_control_callback(self, action: str, callback: Callable[[], bool]) -> None:
177
- """Register optional start/stop callbacks (disabled unless allow_control)."""
315
+ """Register optional start/stop callbacks (used on approve or allow_control)."""
178
316
  self.control_callbacks[action] = callback
179
317
 
180
318
  def _auth_response(self):
@@ -210,7 +348,10 @@ class CompanionAPI:
210
348
  "portfolio": True,
211
349
  "alerts": True,
212
350
  "strategies": True,
351
+ "history": self._history_provider is not None,
213
352
  "webhooks": True,
353
+ "pairing": True,
354
+ "actions": True,
214
355
  "control": self.allow_control,
215
356
  },
216
357
  "timestamp": datetime.now().isoformat(),
@@ -271,6 +412,32 @@ class CompanionAPI:
271
412
  except Exception as exc:
272
413
  return jsonify({"error": str(exc)}), 500
273
414
 
415
+ @self.app.route(f"{prefix}/strategies/history", methods=["GET"])
416
+ def companion_strategies_history():
417
+ denied = self._auth_response()
418
+ if denied:
419
+ return denied
420
+ if not self._history_provider:
421
+ return jsonify({"error": "History not available"}), 404
422
+ try:
423
+ limit = int(request.args.get("limit", 100))
424
+ return jsonify(self._history_provider(None, limit))
425
+ except Exception as exc:
426
+ return jsonify({"error": str(exc)}), 500
427
+
428
+ @self.app.route(f"{prefix}/strategies/<strategy_name>/history", methods=["GET"])
429
+ def companion_strategy_history(strategy_name: str):
430
+ denied = self._auth_response()
431
+ if denied:
432
+ return denied
433
+ if not self._history_provider:
434
+ return jsonify({"error": "History not available"}), 404
435
+ try:
436
+ limit = int(request.args.get("limit", 100))
437
+ return jsonify(self._history_provider(strategy_name, limit))
438
+ except Exception as exc:
439
+ return jsonify({"error": str(exc)}), 500
440
+
274
441
  @self.app.route(f"{prefix}/devices/webhook", methods=["POST"])
275
442
  def register_webhook():
276
443
  denied = self._auth_response()
@@ -301,6 +468,93 @@ class CompanionAPI:
301
468
  removed = self.webhook_notifier.unregister(url)
302
469
  return jsonify({"success": removed})
303
470
 
471
+ @self.app.route(f"{prefix}/pairing/session", methods=["POST"])
472
+ def pairing_session():
473
+ """Create QR pairing payload (authenticated host)."""
474
+ denied = self._auth_response()
475
+ if denied:
476
+ return denied
477
+ data = request.get_json(silent=True) or {}
478
+ base = str(
479
+ data.get("base_url")
480
+ or self.public_base_url
481
+ or request.host_url.rstrip("/")
482
+ ).rstrip("/")
483
+ pin = str(data.get("pin_sha256") or self.pin_sha256 or "").strip()
484
+ session = self.pairing_store.create(
485
+ base_url=base, api_key=self.api_key, pin_sha256=pin or None
486
+ )
487
+ return jsonify(session), 201
488
+
489
+ @self.app.route(f"{prefix}/pairing/redeem", methods=["POST"])
490
+ def pairing_redeem():
491
+ """Phone redeems short-lived pairing code (no prior API key)."""
492
+ data = request.get_json(silent=True) or {}
493
+ code = str(data.get("pairing_code") or data.get("code") or "").strip()
494
+ result = self.pairing_store.redeem(code)
495
+ if not result:
496
+ return jsonify({"error": "Invalid or expired pairing code"}), 400
497
+ return jsonify(result)
498
+
499
+ @self.app.route(f"{prefix}/actions", methods=["GET"])
500
+ def list_actions():
501
+ denied = self._auth_response()
502
+ if denied:
503
+ return denied
504
+ pending = self.action_queue.list_pending()
505
+ return jsonify({"pending": pending, "count": len(pending)})
506
+
507
+ @self.app.route(f"{prefix}/actions", methods=["POST"])
508
+ def request_action():
509
+ """Phone requests a high-level action (queued until host approves)."""
510
+ denied = self._auth_response()
511
+ if denied:
512
+ return denied
513
+ data = request.get_json(silent=True) or {}
514
+ action = str(data.get("action") or "").strip()
515
+ requested_by = str(data.get("requested_by") or "companion").strip() or "companion"
516
+ item = self.action_queue.request(action, requested_by=requested_by)
517
+ if not item:
518
+ return jsonify(
519
+ {
520
+ "error": "Invalid action or queue full",
521
+ "allowed": sorted(ALLOWED_ACTIONS),
522
+ }
523
+ ), 400
524
+ return jsonify({"action": item}), 201
525
+
526
+ @self.app.route(f"{prefix}/actions/<action_id>/approve", methods=["POST"])
527
+ def approve_action(action_id: str):
528
+ denied = self._auth_response()
529
+ if denied:
530
+ return denied
531
+ item = self.action_queue.resolve(action_id, approve=True)
532
+ if not item:
533
+ return jsonify({"error": "Action not found or already resolved"}), 404
534
+ cb = self.control_callbacks.get(item["action"])
535
+ executed = False
536
+ if cb:
537
+ try:
538
+ executed = bool(cb())
539
+ except Exception as exc:
540
+ logger.exception("Action callback failed")
541
+ return jsonify(
542
+ {"action": item, "executed": False, "error": str(exc)}
543
+ ), 500
544
+ return jsonify({"action": item, "executed": executed})
545
+
546
+ @self.app.route(f"{prefix}/actions/<action_id>/reject", methods=["POST"])
547
+ def reject_action(action_id: str):
548
+ denied = self._auth_response()
549
+ if denied:
550
+ return denied
551
+ data = request.get_json(silent=True) or {}
552
+ reason = str(data.get("reason") or "").strip() or None
553
+ item = self.action_queue.resolve(action_id, approve=False, reason=reason)
554
+ if not item:
555
+ return jsonify({"error": "Action not found or already resolved"}), 404
556
+ return jsonify({"action": item})
557
+
304
558
  if self.allow_control:
305
559
 
306
560
  @self.app.route(f"{prefix}/control/start", methods=["POST"])
@@ -192,6 +192,22 @@ class RemoteMonitorServer:
192
192
  return denied
193
193
  return jsonify(self._strategies_payload())
194
194
 
195
+ @self.app.route("/api/v1/strategies/history", methods=["GET"])
196
+ def get_strategies_history():
197
+ """All strategy metrics history for charts (authenticated)."""
198
+ denied = self._auth_response()
199
+ if denied:
200
+ return denied
201
+ return jsonify(self._history_payload(None))
202
+
203
+ @self.app.route("/api/v1/strategies/<strategy_name>/history", methods=["GET"])
204
+ def get_strategy_history(strategy_name: str):
205
+ """Single strategy metrics history (authenticated)."""
206
+ denied = self._auth_response()
207
+ if denied:
208
+ return denied
209
+ return jsonify(self._history_payload(strategy_name))
210
+
195
211
  @self.app.route("/api/v1/strategies/<strategy_name>", methods=["GET"])
196
212
  def get_strategy(strategy_name: str):
197
213
  """Single strategy health (authenticated)."""
@@ -205,7 +221,8 @@ class RemoteMonitorServer:
205
221
  return jsonify({"strategy": strategies[strategy_name], "timestamp": payload.get("timestamp")})
206
222
 
207
223
  def _mount_companion(self) -> None:
208
- """Mount read-only companion API on the same Flask app."""
224
+ """Mount companion API on the same Flask app."""
225
+ base = f"http://{self.host}:{self.port}"
209
226
  self.companion = CompanionAPI(
210
227
  app=self.app,
211
228
  api_key=self.api_key,
@@ -213,12 +230,30 @@ class RemoteMonitorServer:
213
230
  portfolio_provider=lambda: self.trading_data,
214
231
  alerts_provider=self._get_recent_alerts,
215
232
  strategies_provider=self._strategies_payload,
233
+ history_provider=lambda name, limit: self._history_payload(name, limit),
216
234
  allow_control=False,
235
+ public_base_url=base,
217
236
  )
218
237
 
219
238
  def set_strategy_monitor(self, monitor: Any) -> None:
220
239
  """Attach a StrategyMonitor for /api/v1/strategies and companion clients."""
221
240
  self.strategy_monitor = monitor
241
+ if self.companion is not None:
242
+ self.companion.set_history_provider(
243
+ lambda name, limit: self._history_payload(name, limit)
244
+ )
245
+
246
+ def register_control_callback(self, action: str, callback: Callable):
247
+ """
248
+ Register callback for remote control actions.
249
+
250
+ Args:
251
+ action: Action name ('start_trading', 'stop_trading', etc.)
252
+ callback: Callback function
253
+ """
254
+ self.control_callbacks[action] = callback
255
+ if self.companion is not None:
256
+ self.companion.register_control_callback(action, callback)
222
257
 
223
258
  def _strategies_payload(self) -> Dict:
224
259
  """Build strategy health JSON for dashboard / companion."""
@@ -239,6 +274,34 @@ class RemoteMonitorServer:
239
274
  payload["timestamp"] = datetime.now().isoformat()
240
275
  return payload
241
276
 
277
+ def _history_payload(
278
+ self, strategy_name: Optional[str] = None, limit: int = 100
279
+ ) -> Dict:
280
+ """Build strategy history JSON for charts."""
281
+ try:
282
+ from flask import has_request_context
283
+
284
+ if has_request_context() and request.args.get("limit") is not None:
285
+ limit = int(request.args.get("limit"))
286
+ except (TypeError, ValueError):
287
+ pass
288
+ if self.strategy_monitor is not None and hasattr(
289
+ self.strategy_monitor, "get_history_payload"
290
+ ):
291
+ return self.strategy_monitor.get_history_payload(strategy_name, limit=limit)
292
+ if strategy_name:
293
+ return {
294
+ "strategy": strategy_name,
295
+ "history": [],
296
+ "count": 0,
297
+ "timestamp": datetime.now().isoformat(),
298
+ }
299
+ return {
300
+ "histories": {},
301
+ "count": 0,
302
+ "timestamp": datetime.now().isoformat(),
303
+ }
304
+
242
305
  def _auth_response(self):
243
306
  """Return 401 response tuple when auth fails, else None."""
244
307
  if not self._check_auth():
@@ -312,12 +375,19 @@ class RemoteMonitorServer:
312
375
  .health-critical { color: #f44336; }
313
376
  .health-unknown { color: #9e9e9e; }
314
377
  .badge { display:inline-block; padding:2px 8px; border-radius:4px; background:#444; font-size:12px; }
378
+ .chart-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap:12px; margin-top:12px; }
379
+ .chart-card { background:#3d3d3d; padding:12px; border-radius:6px; }
380
+ .chart-card h3 { margin:0 0 8px; font-size:14px; color:#ccc; }
381
+ canvas.spark { width:100%; height:80px; background:#2a2a2a; border-radius:4px; }
382
+ #pairing-qr { background:#fff; padding:12px; display:inline-block; border-radius:8px; }
383
+ #pairing-text { width:100%; min-height:72px; font-family:monospace; font-size:12px; }
384
+ .action-row button { margin-right:6px; }
315
385
  </style>
316
386
  </head>
317
387
  <body>
318
388
  <div class="container">
319
389
  <h1>ForexSmartBot Remote Monitor</h1>
320
- <p style="color:#aaa;">Strategy health dashboard + read-only companion API at <code>/api/v1/companion/*</code></p>
390
+ <p style="color:#aaa;">Strategy health + history charts + companion pairing / approve-reject at <code>/api/v1/companion/*</code></p>
321
391
 
322
392
  <div class="status-card">
323
393
  <h2>API Key</h2>
@@ -372,6 +442,23 @@ class RemoteMonitorServer:
372
442
  </thead>
373
443
  <tbody id="strategies-body"></tbody>
374
444
  </table>
445
+ <h2 style="margin-top:20px;">History Charts</h2>
446
+ <div id="history-charts" class="chart-grid"></div>
447
+ </div>
448
+
449
+ <div class="status-card">
450
+ <h2>Companion Pairing (QR)</h2>
451
+ <p style="color:#aaa;">Create a short-lived session; scan or paste on the Android companion.</p>
452
+ <button class="btn btn-start" onclick="createPairing()">Create pairing QR</button>
453
+ <div id="pairing-meta" style="color:#aaa;margin:8px 0;"></div>
454
+ <div id="pairing-qr"></div>
455
+ <textarea id="pairing-text" readonly placeholder="QR JSON payload appears here"></textarea>
456
+ </div>
457
+
458
+ <div class="status-card">
459
+ <h2>Pending Companion Actions</h2>
460
+ <p style="color:#aaa;">Phone-requested start/stop/pause — approve or reject on this host.</p>
461
+ <div id="actions-list" style="color:#aaa;">No pending actions</div>
375
462
  </div>
376
463
 
377
464
  <div class="status-card">
@@ -397,6 +484,7 @@ class RemoteMonitorServer:
397
484
  </div>
398
485
  </div>
399
486
 
487
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
400
488
  <script>
401
489
  function authHeaders() {
402
490
  const key = sessionStorage.getItem('fsb_api_key') || '';
@@ -423,6 +511,59 @@ class RemoteMonitorServer:
423
511
  return 'health-unknown';
424
512
  }
425
513
 
514
+ function drawSpark(canvas, series, key, color) {
515
+ const ctx = canvas.getContext('2d');
516
+ const w = canvas.width = canvas.clientWidth * (window.devicePixelRatio || 1);
517
+ const h = canvas.height = 80 * (window.devicePixelRatio || 1);
518
+ ctx.clearRect(0, 0, w, h);
519
+ const vals = (series || []).map(p => Number(p[key] || 0));
520
+ if (vals.length < 2) {
521
+ ctx.fillStyle = '#666';
522
+ ctx.font = '12px sans-serif';
523
+ ctx.fillText('Not enough samples', 8, h/2);
524
+ return;
525
+ }
526
+ const min = Math.min(...vals), max = Math.max(...vals);
527
+ const span = (max - min) || 1;
528
+ ctx.strokeStyle = color;
529
+ ctx.lineWidth = 2;
530
+ ctx.beginPath();
531
+ vals.forEach((v, i) => {
532
+ const x = (i / (vals.length - 1)) * (w - 4) + 2;
533
+ const y = h - 4 - ((v - min) / span) * (h - 8);
534
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
535
+ });
536
+ ctx.stroke();
537
+ }
538
+
539
+ function updateHistoryCharts() {
540
+ fetch('/api/v1/strategies/history?limit=80', {headers: authHeaders()})
541
+ .then(async r => {
542
+ if (r.status === 401) throw new Error('unauthorized');
543
+ return r.json();
544
+ })
545
+ .then(data => {
546
+ const histories = data.histories || {};
547
+ const root = document.getElementById('history-charts');
548
+ const names = Object.keys(histories);
549
+ if (!names.length) {
550
+ root.innerHTML = '<div style="color:#888;">No history yet — signals populate charts</div>';
551
+ return;
552
+ }
553
+ root.innerHTML = names.map(name => {
554
+ const id = 'spark-' + name.replace(/[^a-zA-Z0-9_-]/g, '_');
555
+ return '<div class="chart-card"><h3>' + name + ' · signals</h3>' +
556
+ '<canvas class="spark" id="' + id + '"></canvas></div>';
557
+ }).join('');
558
+ names.forEach(name => {
559
+ const id = 'spark-' + name.replace(/[^a-zA-Z0-9_-]/g, '_');
560
+ const el = document.getElementById(id);
561
+ if (el) drawSpark(el, histories[name], 'signal_count', '#4CAF50');
562
+ });
563
+ })
564
+ .catch(() => {});
565
+ }
566
+
426
567
  function updateStrategies() {
427
568
  fetch('/api/v1/strategies', {headers: authHeaders()})
428
569
  .then(async r => {
@@ -456,6 +597,72 @@ class RemoteMonitorServer:
456
597
  }).join('');
457
598
  })
458
599
  .catch(() => {});
600
+ updateHistoryCharts();
601
+ updateActions();
602
+ }
603
+
604
+ function createPairing() {
605
+ fetch('/api/v1/companion/pairing/session', {
606
+ method: 'POST',
607
+ headers: authHeaders(),
608
+ body: JSON.stringify({}),
609
+ })
610
+ .then(async r => {
611
+ if (r.status === 401) throw new Error('unauthorized');
612
+ return r.json();
613
+ })
614
+ .then(data => {
615
+ const text = data.qr_text || JSON.stringify(data.qr_payload || {});
616
+ document.getElementById('pairing-text').value = text;
617
+ document.getElementById('pairing-meta').textContent =
618
+ 'Code ' + data.pairing_code + ' · expires ' + data.expires_at +
619
+ ' · TTL ' + data.ttl_seconds + 's';
620
+ const box = document.getElementById('pairing-qr');
621
+ box.innerHTML = '';
622
+ if (window.QRCode) {
623
+ new QRCode(box, { text: text, width: 192, height: 192 });
624
+ } else {
625
+ box.textContent = 'QR library unavailable — copy JSON below';
626
+ }
627
+ })
628
+ .catch(err => alert('Pairing failed: ' + err));
629
+ }
630
+
631
+ function updateActions() {
632
+ fetch('/api/v1/companion/actions', {headers: authHeaders()})
633
+ .then(async r => {
634
+ if (r.status === 401) throw new Error('unauthorized');
635
+ return r.json();
636
+ })
637
+ .then(data => {
638
+ const pending = data.pending || [];
639
+ const root = document.getElementById('actions-list');
640
+ if (!pending.length) {
641
+ root.innerHTML = 'No pending actions';
642
+ return;
643
+ }
644
+ root.innerHTML = pending.map(a =>
645
+ '<div class="action-row" style="margin:8px 0;">' +
646
+ '<span class="badge">' + a.action + '</span> ' +
647
+ '<span style="color:#aaa;">' + a.id.slice(0,8) + ' · ' + a.requested_by + ' · ' + a.created_at + '</span> ' +
648
+ '<button class="btn btn-start" onclick="resolveAction(\\'' + a.id + '\\', true)">Approve</button>' +
649
+ '<button class="btn btn-stop" onclick="resolveAction(\\'' + a.id + '\\', false)">Reject</button>' +
650
+ '</div>'
651
+ ).join('');
652
+ })
653
+ .catch(() => {});
654
+ }
655
+
656
+ function resolveAction(id, approve) {
657
+ const path = approve ? 'approve' : 'reject';
658
+ fetch('/api/v1/companion/actions/' + id + '/' + path, {
659
+ method: 'POST',
660
+ headers: authHeaders(),
661
+ body: JSON.stringify(approve ? {} : {reason: 'rejected from dashboard'}),
662
+ })
663
+ .then(r => r.json())
664
+ .then(() => updateActions())
665
+ .catch(err => alert(err));
459
666
  }
460
667
 
461
668
  function updateStatus() {
@@ -535,16 +742,6 @@ class RemoteMonitorServer:
535
742
  self.trading_data.update(data)
536
743
  self.trading_data["last_update"] = datetime.now().isoformat()
537
744
 
538
- def register_control_callback(self, action: str, callback: Callable):
539
- """
540
- Register callback for remote control actions.
541
-
542
- Args:
543
- action: Action name ('start_trading', 'stop_trading', etc.)
544
- callback: Callback function
545
- """
546
- self.control_callbacks[action] = callback
547
-
548
745
  def register_alert_callback(self, callback: Callable):
549
746
  """
550
747
  Register callback for alerts.