forexsmartbot 3.3.1__tar.gz → 3.3.2__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.1 → forexsmartbot-3.3.2}/PKG-INFO +1 -1
  2. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/__init__.py +1 -1
  3. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/cloud/__init__.py +4 -0
  4. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/cloud/companion_api.py +30 -2
  5. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/cloud/remote_monitor.py +110 -3
  6. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot.egg-info/PKG-INFO +1 -1
  7. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/pyproject.toml +1 -1
  8. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/tests/test_companion_q2.py +49 -1
  9. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/LICENSE +0 -0
  10. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/README.md +0 -0
  11. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/__init__.py +0 -0
  12. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/brokers/__init__.py +0 -0
  13. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/brokers/ib_tws_broker.py +0 -0
  14. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/brokers/mt4_broker.py +0 -0
  15. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/brokers/paper_broker.py +0 -0
  16. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/brokers/rest_broker.py +0 -0
  17. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/__init__.py +0 -0
  18. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/alpha_vantage_provider.py +0 -0
  19. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/config.py +0 -0
  20. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/csv_provider.py +0 -0
  21. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/dummy_provider.py +0 -0
  22. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/mt4_provider.py +0 -0
  23. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/multi_provider.py +0 -0
  24. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/oanda_provider.py +0 -0
  25. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/stooq_provider.py +0 -0
  26. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/twelve_data_provider.py +0 -0
  27. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/adapters/data/yfinance_provider.py +0 -0
  28. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/__init__.py +0 -0
  29. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/chart_patterns.py +0 -0
  30. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/correlation_matrix.py +0 -0
  31. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/economic_calendar.py +0 -0
  32. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/enhanced_chart.py +0 -0
  33. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/live_metrics.py +0 -0
  34. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/market_depth.py +0 -0
  35. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/performance_attribution.py +0 -0
  36. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/portfolio_analytics.py +0 -0
  37. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/risk_analytics.py +0 -0
  38. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/strategy_comparison.py +0 -0
  39. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/analytics/trade_journal.py +0 -0
  40. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/brokers/mt4_bridge.py +0 -0
  41. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/builder/__init__.py +0 -0
  42. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/builder/code_generator.py +0 -0
  43. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/builder/strategy_builder.py +0 -0
  44. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/builder/strategy_template.py +0 -0
  45. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/cloud/api_client.py +0 -0
  46. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/cloud/api_server.py +0 -0
  47. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/cloud/auth_utils.py +0 -0
  48. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/cloud/cloud_sync.py +0 -0
  49. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/cloud/error_handler.py +0 -0
  50. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/cloud/mobile_api.py +0 -0
  51. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/cloud/security.py +0 -0
  52. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/core/backtester.py +0 -0
  53. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/core/config.py +0 -0
  54. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/core/interfaces.py +0 -0
  55. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/core/paper_broker.py +0 -0
  56. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/core/portfolio.py +0 -0
  57. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/core/risk.py +0 -0
  58. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/core/risk_engine.py +0 -0
  59. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/core/strategy.py +0 -0
  60. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/core/trade_manager.py +0 -0
  61. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/gui/main_window.py +0 -0
  62. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/gui/theme.py +0 -0
  63. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/marketplace/__init__.py +0 -0
  64. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/marketplace/marketplace.py +0 -0
  65. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/marketplace/strategy_rating.py +0 -0
  66. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/monitoring/__init__.py +0 -0
  67. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/monitoring/health_check.py +0 -0
  68. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/monitoring/performance_tracker.py +0 -0
  69. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/monitoring/strategy_monitor.py +0 -0
  70. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/optimization/__init__.py +0 -0
  71. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/optimization/adaptive_parameters.py +0 -0
  72. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/optimization/genetic_optimizer.py +0 -0
  73. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/optimization/hyperparameter_optimizer.py +0 -0
  74. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/optimization/monte_carlo.py +0 -0
  75. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/optimization/multi_objective_optimizer.py +0 -0
  76. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/optimization/parameter_sensitivity.py +0 -0
  77. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/optimization/walk_forward.py +0 -0
  78. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/__init__.py +0 -0
  79. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/backtest.py +0 -0
  80. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/calculation_engine.py +0 -0
  81. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/controller.py +0 -0
  82. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/data_streaming.py +0 -0
  83. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/enhanced_backtest.py +0 -0
  84. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/fast_trading_controller.py +0 -0
  85. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/gpu_backtest.py +0 -0
  86. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/language_manager.py +0 -0
  87. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/notification_service.py +0 -0
  88. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/persistence.py +0 -0
  89. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/services/startup_manager.py +0 -0
  90. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/__init__.py +0 -0
  91. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/adaptive_trend_flow.py +0 -0
  92. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/breakout_atr.py +0 -0
  93. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/ensemble_ml_strategy.py +0 -0
  94. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/fear_index.py +0 -0
  95. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/lstm_strategy.py +0 -0
  96. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/mean_reversion.py +0 -0
  97. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/ml_adaptive_supertrend.py +0 -0
  98. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/momentum_breakout.py +0 -0
  99. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/multi_timeframe_strategy.py +0 -0
  100. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/news_trading.py +0 -0
  101. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/rl_strategy.py +0 -0
  102. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/rsi_reversion.py +0 -0
  103. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/scalping_ma.py +0 -0
  104. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/sma_crossover.py +0 -0
  105. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/svm_strategy.py +0 -0
  106. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/strategies/transformer_strategy.py +0 -0
  107. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/testing/__init__.py +0 -0
  108. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/testing/ci_cd_pipeline.py +0 -0
  109. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/testing/paper_account_tester.py +0 -0
  110. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/testing/strategy_sandbox.py +0 -0
  111. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/__init__.py +0 -0
  112. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/analytics_dialog.py +0 -0
  113. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/analytics_widget.py +0 -0
  114. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/backtest_dialog.py +0 -0
  115. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/charts.py +0 -0
  116. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/cloud_dialog.py +0 -0
  117. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/enhanced_charts.py +0 -0
  118. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/enhanced_main_window.py +0 -0
  119. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/export_dialog.py +0 -0
  120. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/main_window.py +0 -0
  121. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/marketplace_dialog.py +0 -0
  122. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/monitoring_dialog.py +0 -0
  123. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/monitoring_widget.py +0 -0
  124. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/optimization_dialog.py +0 -0
  125. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/settings_dialog.py +0 -0
  126. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/simple_chart.py +0 -0
  127. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/strategy_builder_dialog.py +0 -0
  128. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/theme.py +0 -0
  129. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/ui/tradingview_chart.py +0 -0
  130. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/utils/__init__.py +0 -0
  131. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/utils/backtest_utils.py +0 -0
  132. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/utils/gpu_numpy.py +0 -0
  133. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/utils/gpu_utils.py +0 -0
  134. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot/utils/native_indicators.py +0 -0
  135. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot.egg-info/SOURCES.txt +0 -0
  136. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot.egg-info/dependency_links.txt +0 -0
  137. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot.egg-info/entry_points.txt +0 -0
  138. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot.egg-info/requires.txt +0 -0
  139. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/forexsmartbot.egg-info/top_level.txt +0 -0
  140. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/setup.cfg +0 -0
  141. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/tests/test_backtest_dialog_ui.py +0 -0
  142. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/tests/test_cloud_hardening.py +0 -0
  143. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/tests/test_config.py +0 -0
  144. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/tests/test_live_metrics.py +0 -0
  145. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/tests/test_multi_timeframe_strategy.py +0 -0
  146. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/tests/test_native_indicators.py +0 -0
  147. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/tests/test_risk.py +0 -0
  148. {forexsmartbot-3.3.1 → forexsmartbot-3.3.2}/tests/test_strategies.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forexsmartbot
3
- Version: 3.3.1
3
+ Version: 3.3.2
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.1"
2
+ __version__ = "3.3.2"
@@ -12,6 +12,8 @@ from .companion_api import (
12
12
  CompanionActionQueue,
13
13
  CompanionPairingStore,
14
14
  WebhookNotifier,
15
+ normalize_cert_pin,
16
+ resolve_companion_cert_pin,
15
17
  )
16
18
  from .security import SecurityManager, RateLimiter
17
19
  from .api_client import APIClient, WebSocketClient
@@ -29,6 +31,8 @@ __all__ = [
29
31
  'CompanionActionQueue',
30
32
  'CompanionPairingStore',
31
33
  'WebhookNotifier',
34
+ 'normalize_cert_pin',
35
+ 'resolve_companion_cert_pin',
32
36
  'SecurityManager',
33
37
  'RateLimiter',
34
38
  'APIClient',
@@ -40,6 +40,32 @@ PAIRING_TTL_SECONDS = 300
40
40
  ALLOWED_ACTIONS = frozenset({"start_trading", "stop_trading", "pause_trading"})
41
41
 
42
42
 
43
+ def normalize_cert_pin(pin: Optional[str]) -> str:
44
+ """Normalize SPKI pin to ``sha256/...`` form (empty string if unset)."""
45
+ value = (pin or "").strip()
46
+ if not value:
47
+ return ""
48
+ if value.startswith("sha256/"):
49
+ return value
50
+ return f"sha256/{value}"
51
+
52
+
53
+ def resolve_companion_cert_pin(explicit: Optional[str] = None) -> str:
54
+ """
55
+ Production cert-pin default for pairing QR payloads.
56
+
57
+ Order: explicit arg → ``FSB_COMPANION_CERT_PIN`` → ``COMPANION_CERT_PIN``.
58
+ """
59
+ import os
60
+
61
+ if explicit and str(explicit).strip():
62
+ return normalize_cert_pin(str(explicit))
63
+ return normalize_cert_pin(
64
+ os.getenv("FSB_COMPANION_CERT_PIN") or os.getenv("COMPANION_CERT_PIN") or ""
65
+ )
66
+
67
+
68
+
43
69
  class CompanionActionQueue:
44
70
  """Pending high-level companion actions awaiting host approve/reject."""
45
71
 
@@ -282,7 +308,7 @@ class CompanionAPI:
282
308
  self.require_api_key = require_api_key
283
309
  self.allow_control = allow_control
284
310
  self.public_base_url = (public_base_url or "").rstrip("/")
285
- self.pin_sha256 = (pin_sha256 or "").strip()
311
+ self.pin_sha256 = resolve_companion_cert_pin(pin_sha256)
286
312
  self.url_prefix = url_prefix.rstrip("/") or "/api/v1/companion"
287
313
 
288
314
  self._portfolio_provider = portfolio_provider or (lambda: {})
@@ -480,7 +506,9 @@ class CompanionAPI:
480
506
  or self.public_base_url
481
507
  or request.host_url.rstrip("/")
482
508
  ).rstrip("/")
483
- pin = str(data.get("pin_sha256") or self.pin_sha256 or "").strip()
509
+ pin = resolve_companion_cert_pin(
510
+ str(data.get("pin_sha256") or self.pin_sha256 or "").strip() or None
511
+ )
484
512
  session = self.pairing_store.create(
485
513
  base_url=base, api_key=self.api_key, pin_sha256=pin or None
486
514
  )
@@ -222,7 +222,13 @@ class RemoteMonitorServer:
222
222
 
223
223
  def _mount_companion(self) -> None:
224
224
  """Mount companion API on the same Flask app."""
225
- base = f"http://{self.host}:{self.port}"
225
+ from .companion_api import resolve_companion_cert_pin
226
+
227
+ public = (
228
+ os.getenv("FSB_PUBLIC_BASE_URL")
229
+ or os.getenv("COMPANION_PUBLIC_BASE_URL")
230
+ or f"http://{self.host}:{self.port}"
231
+ ).rstrip("/")
226
232
  self.companion = CompanionAPI(
227
233
  app=self.app,
228
234
  api_key=self.api_key,
@@ -232,7 +238,8 @@ class RemoteMonitorServer:
232
238
  strategies_provider=self._strategies_payload,
233
239
  history_provider=lambda name, limit: self._history_payload(name, limit),
234
240
  allow_control=False,
235
- public_base_url=base,
241
+ public_base_url=public,
242
+ pin_sha256=resolve_companion_cert_pin(),
236
243
  )
237
244
 
238
245
  def set_strategy_monitor(self, monitor: Any) -> None:
@@ -387,7 +394,7 @@ class RemoteMonitorServer:
387
394
  <body>
388
395
  <div class="container">
389
396
  <h1>ForexSmartBot Remote Monitor</h1>
390
- <p style="color:#aaa;">Strategy health + history charts + companion pairing / approve-reject at <code>/api/v1/companion/*</code></p>
397
+ <p style="color:#aaa;">Strategy health + history charts + companion pairing / approve-reject at <code>/api/v1/companion/*</code> · multi-host health poll</p>
391
398
 
392
399
  <div class="status-card">
393
400
  <h2>API Key</h2>
@@ -396,6 +403,27 @@ class RemoteMonitorServer:
396
403
  <span id="auth-hint" style="color:#aaa;margin-left:8px;"></span>
397
404
  </div>
398
405
 
406
+ <div class="status-card">
407
+ <h2>Multi-Host Monitors</h2>
408
+ <p style="color:#aaa;">Poll additional remote-monitor hosts (browser-side). Keys stay in this browser session only.</p>
409
+ <input id="remote-host-url" type="text" placeholder="https://host:8080" style="width:360px;" />
410
+ <input id="remote-host-key" type="password" placeholder="API key (optional)" style="width:200px;" />
411
+ <button class="btn" onclick="addRemoteHost()">Add host</button>
412
+ <button class="btn" onclick="refreshRemoteHosts()">Refresh</button>
413
+ <table id="remote-hosts-table" style="margin-top:12px;">
414
+ <thead>
415
+ <tr>
416
+ <th>Host</th>
417
+ <th>Health</th>
418
+ <th>Version</th>
419
+ <th>Strategies</th>
420
+ <th></th>
421
+ </tr>
422
+ </thead>
423
+ <tbody id="remote-hosts-body"></tbody>
424
+ </table>
425
+ </div>
426
+
399
427
  <div class="status-card">
400
428
  <h2>Status</h2>
401
429
  <div id="status-indicator" class="status-indicator status-disconnected"></div>
@@ -503,6 +531,83 @@ class RemoteMonitorServer:
503
531
  updateStatus();
504
532
  }
505
533
 
534
+ function loadRemoteHosts() {
535
+ try {
536
+ return JSON.parse(localStorage.getItem('fsb_remote_hosts') || '[]');
537
+ } catch (e) {
538
+ return [];
539
+ }
540
+ }
541
+
542
+ function saveRemoteHosts(hosts) {
543
+ localStorage.setItem('fsb_remote_hosts', JSON.stringify(hosts || []));
544
+ }
545
+
546
+ function addRemoteHost() {
547
+ const url = document.getElementById('remote-host-url').value.trim().replace(/\\/$/, '');
548
+ const key = document.getElementById('remote-host-key').value.trim();
549
+ if (!url) return alert('Enter a host base URL');
550
+ const hosts = loadRemoteHosts().filter(h => h.url !== url);
551
+ hosts.push({url: url, key: key});
552
+ saveRemoteHosts(hosts);
553
+ document.getElementById('remote-host-url').value = '';
554
+ document.getElementById('remote-host-key').value = '';
555
+ refreshRemoteHosts();
556
+ }
557
+
558
+ function removeRemoteHost(url) {
559
+ saveRemoteHosts(loadRemoteHosts().filter(h => h.url !== url));
560
+ refreshRemoteHosts();
561
+ }
562
+
563
+ function refreshRemoteHosts() {
564
+ const hosts = loadRemoteHosts();
565
+ const tbody = document.getElementById('remote-hosts-body');
566
+ if (!hosts.length) {
567
+ tbody.innerHTML = '<tr><td colspan="5" style="color:#888;">No remote hosts — add a monitor URL</td></tr>';
568
+ return;
569
+ }
570
+ tbody.innerHTML = hosts.map(h =>
571
+ '<tr data-url="' + h.url + '">' +
572
+ '<td>' + h.url + '</td>' +
573
+ '<td class="host-health">…</td>' +
574
+ '<td class="host-version">…</td>' +
575
+ '<td class="host-strats">…</td>' +
576
+ '<td><button class="btn btn-stop" onclick="removeRemoteHost(\\'' + h.url.replace(/'/g, '') + '\\')">Remove</button></td>' +
577
+ '</tr>'
578
+ ).join('');
579
+ hosts.forEach(h => {
580
+ const headers = {'Content-Type': 'application/json'};
581
+ if (h.key) {
582
+ headers['Authorization'] = 'Bearer ' + h.key;
583
+ headers['X-API-Key'] = h.key;
584
+ }
585
+ const row = tbody.querySelector('tr[data-url="' + h.url + '"]');
586
+ if (!row) return;
587
+ fetch(h.url + '/api/v1/health').then(r => r.json()).then(health => {
588
+ row.querySelector('.host-health').textContent = health.status || 'ok';
589
+ row.querySelector('.host-health').className = 'host-health ' +
590
+ (health.status === 'healthy' ? 'health-healthy' : 'health-warning');
591
+ row.querySelector('.host-version').textContent = health.version || '—';
592
+ }).catch(() => {
593
+ row.querySelector('.host-health').textContent = 'unreachable';
594
+ row.querySelector('.host-health').className = 'host-health health-critical';
595
+ row.querySelector('.host-version').textContent = '—';
596
+ row.querySelector('.host-strats').textContent = '—';
597
+ });
598
+ fetch(h.url + '/api/v1/strategies', {headers: headers}).then(async r => {
599
+ if (!r.ok) throw new Error('auth');
600
+ return r.json();
601
+ }).then(data => {
602
+ const total = (data.summary && data.summary.total) ||
603
+ Object.keys(data.strategies || {}).length;
604
+ row.querySelector('.host-strats').textContent = String(total);
605
+ }).catch(() => {
606
+ row.querySelector('.host-strats').textContent = 'n/a';
607
+ });
608
+ });
609
+ }
610
+
506
611
  function statusClass(status) {
507
612
  const s = (status || 'unknown').toLowerCase();
508
613
  if (s === 'healthy') return 'health-healthy';
@@ -726,7 +831,9 @@ class RemoteMonitorServer:
726
831
  const existing = sessionStorage.getItem('fsb_api_key');
727
832
  if (existing) document.getElementById('api-key').value = existing;
728
833
  setInterval(updateStatus, 3000);
834
+ setInterval(refreshRemoteHosts, 15000);
729
835
  updateStatus();
836
+ refreshRemoteHosts();
730
837
  </script>
731
838
  </body>
732
839
  </html>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forexsmartbot
3
- Version: 3.3.1
3
+ Version: 3.3.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "forexsmartbot"
7
- version = "3.3.1"
7
+ version = "3.3.2"
8
8
  description = "Professional forex trading bot with advanced risk management and strategy plugins"
9
9
  authors = [{name = "VoxHash", email = "contact@voxhash.dev"}]
10
10
  license = {text = "MIT"}
@@ -2,11 +2,31 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from forexsmartbot.cloud.companion_api import CompanionAPI, WebhookNotifier
5
+ from forexsmartbot.cloud.companion_api import (
6
+ CompanionAPI,
7
+ WebhookNotifier,
8
+ normalize_cert_pin,
9
+ resolve_companion_cert_pin,
10
+ )
6
11
  from forexsmartbot.cloud.remote_monitor import RemoteMonitorServer
7
12
  from forexsmartbot.monitoring.strategy_monitor import StrategyMonitor
8
13
 
9
14
 
15
+ class TestCertPinHelpers:
16
+ def test_normalize_cert_pin(self):
17
+ assert normalize_cert_pin("") == ""
18
+ assert normalize_cert_pin("sha256/ABC=") == "sha256/ABC="
19
+ assert normalize_cert_pin("ABC=") == "sha256/ABC="
20
+
21
+ def test_resolve_from_env(self, monkeypatch):
22
+ monkeypatch.delenv("FSB_COMPANION_CERT_PIN", raising=False)
23
+ monkeypatch.delenv("COMPANION_CERT_PIN", raising=False)
24
+ assert resolve_companion_cert_pin(None) == ""
25
+ monkeypatch.setenv("FSB_COMPANION_CERT_PIN", "DEADBEEF")
26
+ assert resolve_companion_cert_pin(None) == "sha256/DEADBEEF"
27
+ assert resolve_companion_cert_pin("sha256/EXPLICIT=") == "sha256/EXPLICIT="
28
+
29
+
10
30
  class TestWebhookNotifier:
11
31
  def test_rejects_plain_http_non_loopback(self):
12
32
  n = WebhookNotifier()
@@ -278,6 +298,34 @@ class TestStrategyHealthDashboard:
278
298
  assert "History Charts" in html
279
299
  assert "pairing/session" in html
280
300
  assert "Pending Companion Actions" in html
301
+ assert "Multi-Host Monitors" in html
302
+ assert "fsb_remote_hosts" in html
303
+
304
+ def test_companion_pin_from_env_on_monitor(self, monkeypatch):
305
+ monkeypatch.setenv(
306
+ "FSB_COMPANION_CERT_PIN",
307
+ "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
308
+ )
309
+ monkeypatch.setenv("FSB_PUBLIC_BASE_URL", "https://monitor.example")
310
+ monitor = RemoteMonitorServer(
311
+ host="127.0.0.1",
312
+ port=18085,
313
+ api_key="monitor-test-key-16",
314
+ require_api_key=True,
315
+ )
316
+ assert monitor.companion is not None
317
+ assert monitor.companion.pin_sha256.startswith("sha256/")
318
+ assert monitor.companion.public_base_url == "https://monitor.example"
319
+ client = monitor.app.test_client()
320
+ created = client.post(
321
+ "/api/v1/companion/pairing/session",
322
+ headers={"X-API-Key": "monitor-test-key-16"},
323
+ json={},
324
+ )
325
+ assert created.status_code == 201
326
+ payload = created.get_json()["qr_payload"]
327
+ assert payload["base_url"] == "https://monitor.example"
328
+ assert payload["pin_sha256"].startswith("sha256/")
281
329
 
282
330
  def test_strategy_history_endpoint(self):
283
331
  sm = StrategyMonitor()
File without changes
File without changes
File without changes