forexsmartbot 3.2.5__tar.gz → 3.3.0__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 (149) hide show
  1. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/PKG-INFO +1 -1
  2. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/__init__.py +1 -1
  3. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/cloud/__init__.py +3 -0
  4. forexsmartbot-3.3.0/forexsmartbot/cloud/companion_api.py +324 -0
  5. forexsmartbot-3.3.0/forexsmartbot/cloud/mobile_api.py +184 -0
  6. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/cloud/remote_monitor.py +139 -2
  7. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/monitoring/strategy_monitor.py +37 -13
  8. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/PKG-INFO +1 -1
  9. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/SOURCES.txt +2 -0
  10. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/pyproject.toml +1 -1
  11. forexsmartbot-3.3.0/tests/test_companion_q2.py +190 -0
  12. forexsmartbot-3.2.5/forexsmartbot/cloud/mobile_api.py +0 -213
  13. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/LICENSE +0 -0
  14. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/README.md +0 -0
  15. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/__init__.py +0 -0
  16. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/brokers/__init__.py +0 -0
  17. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/brokers/ib_tws_broker.py +0 -0
  18. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/brokers/mt4_broker.py +0 -0
  19. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/brokers/paper_broker.py +0 -0
  20. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/brokers/rest_broker.py +0 -0
  21. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/__init__.py +0 -0
  22. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/alpha_vantage_provider.py +0 -0
  23. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/config.py +0 -0
  24. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/csv_provider.py +0 -0
  25. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/dummy_provider.py +0 -0
  26. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/mt4_provider.py +0 -0
  27. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/multi_provider.py +0 -0
  28. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/oanda_provider.py +0 -0
  29. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/stooq_provider.py +0 -0
  30. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/twelve_data_provider.py +0 -0
  31. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/adapters/data/yfinance_provider.py +0 -0
  32. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/__init__.py +0 -0
  33. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/chart_patterns.py +0 -0
  34. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/correlation_matrix.py +0 -0
  35. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/economic_calendar.py +0 -0
  36. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/enhanced_chart.py +0 -0
  37. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/live_metrics.py +0 -0
  38. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/market_depth.py +0 -0
  39. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/performance_attribution.py +0 -0
  40. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/portfolio_analytics.py +0 -0
  41. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/risk_analytics.py +0 -0
  42. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/strategy_comparison.py +0 -0
  43. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/analytics/trade_journal.py +0 -0
  44. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/brokers/mt4_bridge.py +0 -0
  45. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/builder/__init__.py +0 -0
  46. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/builder/code_generator.py +0 -0
  47. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/builder/strategy_builder.py +0 -0
  48. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/builder/strategy_template.py +0 -0
  49. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/cloud/api_client.py +0 -0
  50. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/cloud/api_server.py +0 -0
  51. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/cloud/auth_utils.py +0 -0
  52. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/cloud/cloud_sync.py +0 -0
  53. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/cloud/error_handler.py +0 -0
  54. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/cloud/security.py +0 -0
  55. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/core/backtester.py +0 -0
  56. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/core/config.py +0 -0
  57. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/core/interfaces.py +0 -0
  58. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/core/paper_broker.py +0 -0
  59. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/core/portfolio.py +0 -0
  60. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/core/risk.py +0 -0
  61. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/core/risk_engine.py +0 -0
  62. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/core/strategy.py +0 -0
  63. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/core/trade_manager.py +0 -0
  64. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/gui/main_window.py +0 -0
  65. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/gui/theme.py +0 -0
  66. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/marketplace/__init__.py +0 -0
  67. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/marketplace/marketplace.py +0 -0
  68. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/marketplace/strategy_rating.py +0 -0
  69. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/monitoring/__init__.py +0 -0
  70. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/monitoring/health_check.py +0 -0
  71. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/monitoring/performance_tracker.py +0 -0
  72. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/optimization/__init__.py +0 -0
  73. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/optimization/adaptive_parameters.py +0 -0
  74. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/optimization/genetic_optimizer.py +0 -0
  75. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/optimization/hyperparameter_optimizer.py +0 -0
  76. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/optimization/monte_carlo.py +0 -0
  77. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/optimization/multi_objective_optimizer.py +0 -0
  78. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/optimization/parameter_sensitivity.py +0 -0
  79. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/optimization/walk_forward.py +0 -0
  80. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/__init__.py +0 -0
  81. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/backtest.py +0 -0
  82. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/calculation_engine.py +0 -0
  83. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/controller.py +0 -0
  84. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/data_streaming.py +0 -0
  85. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/enhanced_backtest.py +0 -0
  86. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/fast_trading_controller.py +0 -0
  87. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/gpu_backtest.py +0 -0
  88. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/language_manager.py +0 -0
  89. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/notification_service.py +0 -0
  90. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/persistence.py +0 -0
  91. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/services/startup_manager.py +0 -0
  92. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/__init__.py +0 -0
  93. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/adaptive_trend_flow.py +0 -0
  94. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/breakout_atr.py +0 -0
  95. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/ensemble_ml_strategy.py +0 -0
  96. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/fear_index.py +0 -0
  97. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/lstm_strategy.py +0 -0
  98. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/mean_reversion.py +0 -0
  99. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/ml_adaptive_supertrend.py +0 -0
  100. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/momentum_breakout.py +0 -0
  101. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/multi_timeframe_strategy.py +0 -0
  102. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/news_trading.py +0 -0
  103. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/rl_strategy.py +0 -0
  104. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/rsi_reversion.py +0 -0
  105. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/scalping_ma.py +0 -0
  106. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/sma_crossover.py +0 -0
  107. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/svm_strategy.py +0 -0
  108. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/strategies/transformer_strategy.py +0 -0
  109. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/testing/__init__.py +0 -0
  110. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/testing/ci_cd_pipeline.py +0 -0
  111. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/testing/paper_account_tester.py +0 -0
  112. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/testing/strategy_sandbox.py +0 -0
  113. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/__init__.py +0 -0
  114. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/analytics_dialog.py +0 -0
  115. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/analytics_widget.py +0 -0
  116. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/backtest_dialog.py +0 -0
  117. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/charts.py +0 -0
  118. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/cloud_dialog.py +0 -0
  119. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/enhanced_charts.py +0 -0
  120. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/enhanced_main_window.py +0 -0
  121. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/export_dialog.py +0 -0
  122. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/main_window.py +0 -0
  123. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/marketplace_dialog.py +0 -0
  124. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/monitoring_dialog.py +0 -0
  125. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/monitoring_widget.py +0 -0
  126. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/optimization_dialog.py +0 -0
  127. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/settings_dialog.py +0 -0
  128. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/simple_chart.py +0 -0
  129. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/strategy_builder_dialog.py +0 -0
  130. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/theme.py +0 -0
  131. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/ui/tradingview_chart.py +0 -0
  132. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/utils/__init__.py +0 -0
  133. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/utils/backtest_utils.py +0 -0
  134. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/utils/gpu_numpy.py +0 -0
  135. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/utils/gpu_utils.py +0 -0
  136. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot/utils/native_indicators.py +0 -0
  137. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/dependency_links.txt +0 -0
  138. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/entry_points.txt +0 -0
  139. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/requires.txt +0 -0
  140. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/forexsmartbot.egg-info/top_level.txt +0 -0
  141. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/setup.cfg +0 -0
  142. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/tests/test_backtest_dialog_ui.py +0 -0
  143. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/tests/test_cloud_hardening.py +0 -0
  144. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/tests/test_config.py +0 -0
  145. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/tests/test_live_metrics.py +0 -0
  146. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/tests/test_multi_timeframe_strategy.py +0 -0
  147. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/tests/test_native_indicators.py +0 -0
  148. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/tests/test_risk.py +0 -0
  149. {forexsmartbot-3.2.5 → forexsmartbot-3.3.0}/tests/test_strategies.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forexsmartbot
3
- Version: 3.2.5
3
+ Version: 3.3.0
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.2.5"
2
+ __version__ = "3.3.0"
@@ -7,6 +7,7 @@ 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
11
  from .security import SecurityManager, RateLimiter
11
12
  from .api_client import APIClient, WebSocketClient
12
13
  from .error_handler import CloudErrorHandler, CloudHealthMonitor, retry_call
@@ -19,6 +20,8 @@ __all__ = [
19
20
  'WebSocketServer',
20
21
  'MobileAppAPI',
21
22
  'PushNotificationService',
23
+ 'CompanionAPI',
24
+ 'WebhookNotifier',
22
25
  'SecurityManager',
23
26
  'RateLimiter',
24
27
  'APIClient',
@@ -0,0 +1,324 @@
1
+ """
2
+ Read-only companion API for mobile / web monitoring clients.
3
+
4
+ Designed for GrapheneOS-friendly clients: Bearer / X-API-Key auth, local-first
5
+ polling, optional HTTP webhook alerts (no Google Play Services / FCM required).
6
+ Broker keys and order routing stay on the desktop host.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import threading
13
+ from datetime import datetime
14
+ from typing import Any, Callable, Dict, List, Optional
15
+ from urllib.parse import urlparse
16
+
17
+ import requests
18
+ from flask import Flask, jsonify, request
19
+
20
+ from .auth_utils import (
21
+ constant_time_equals,
22
+ extract_bearer_or_api_key,
23
+ is_insecure_secret,
24
+ package_version,
25
+ resolve_api_key,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ PortfolioProvider = Callable[[], Dict[str, Any]]
31
+ AlertsProvider = Callable[[], List[Dict[str, Any]]]
32
+ StrategiesProvider = Callable[[], Dict[str, Any]]
33
+
34
+
35
+ def _normalize_portfolio(raw: Dict[str, Any]) -> Dict[str, Any]:
36
+ """Normalize host trading snapshot for companion clients."""
37
+ positions = raw.get("positions") or []
38
+ if isinstance(positions, dict):
39
+ positions = list(positions.values())
40
+ trades = raw.get("trades") or []
41
+ return {
42
+ "balance": float(raw.get("balance") or 0.0),
43
+ "equity": float(raw.get("equity") or raw.get("balance") or 0.0),
44
+ "status": str(raw.get("status") or "disconnected"),
45
+ "positions_count": len(positions),
46
+ "trades_count": len(trades),
47
+ "positions": positions,
48
+ "last_update": raw.get("last_update"),
49
+ "timestamp": datetime.now().isoformat(),
50
+ "read_only": True,
51
+ }
52
+
53
+
54
+ class WebhookNotifier:
55
+ """
56
+ Deliver alert payloads to registered HTTPS (or localhost HTTP) webhook URLs.
57
+
58
+ Prefer this over FCM on hardened Android / GrapheneOS builds.
59
+ """
60
+
61
+ def __init__(self, *, timeout: float = 5.0, max_endpoints: int = 32):
62
+ self.timeout = timeout
63
+ self.max_endpoints = max_endpoints
64
+ self._endpoints: List[str] = []
65
+ self._lock = threading.Lock()
66
+
67
+ @staticmethod
68
+ def _validate_url(url: str) -> Optional[str]:
69
+ cleaned = (url or "").strip()
70
+ if not cleaned or len(cleaned) > 2048:
71
+ return None
72
+ parsed = urlparse(cleaned)
73
+ if parsed.scheme not in {"https", "http"}:
74
+ return None
75
+ if parsed.scheme == "http":
76
+ host = (parsed.hostname or "").lower()
77
+ if host not in {"127.0.0.1", "localhost", "::1"}:
78
+ return None
79
+ if not parsed.netloc:
80
+ return None
81
+ return cleaned
82
+
83
+ def register(self, url: str) -> bool:
84
+ """Register a webhook endpoint. Returns False if invalid or at capacity."""
85
+ cleaned = self._validate_url(url)
86
+ if not cleaned:
87
+ return False
88
+ with self._lock:
89
+ if cleaned in self._endpoints:
90
+ return True
91
+ if len(self._endpoints) >= self.max_endpoints:
92
+ return False
93
+ self._endpoints.append(cleaned)
94
+ return True
95
+
96
+ def unregister(self, url: str) -> bool:
97
+ cleaned = (url or "").strip()
98
+ with self._lock:
99
+ if cleaned in self._endpoints:
100
+ self._endpoints.remove(cleaned)
101
+ return True
102
+ return False
103
+
104
+ def list_endpoints(self) -> List[str]:
105
+ with self._lock:
106
+ return list(self._endpoints)
107
+
108
+ def notify(self, title: str, body: str, data: Optional[Dict[str, Any]] = None) -> int:
109
+ """POST JSON alert to all endpoints. Returns success count."""
110
+ payload = {
111
+ "title": title,
112
+ "body": body,
113
+ "data": data or {},
114
+ "timestamp": datetime.now().isoformat(),
115
+ "source": "forexsmartbot.companion",
116
+ }
117
+ ok = 0
118
+ for url in self.list_endpoints():
119
+ try:
120
+ resp = requests.post(url, json=payload, timeout=self.timeout)
121
+ if 200 <= resp.status_code < 300:
122
+ ok += 1
123
+ else:
124
+ logger.warning("Webhook %s returned %s", url, resp.status_code)
125
+ except Exception as exc:
126
+ logger.warning("Webhook notify failed for %s: %s", url, exc)
127
+ return ok
128
+
129
+
130
+ class CompanionAPI:
131
+ """
132
+ Read-only companion REST surface for phones and external dashboards.
133
+
134
+ Mounts under ``/api/v1/companion/*`` on an existing or new Flask app.
135
+ """
136
+
137
+ def __init__(
138
+ self,
139
+ app: Optional[Flask] = None,
140
+ *,
141
+ api_key: Optional[str] = None,
142
+ require_api_key: Optional[bool] = None,
143
+ portfolio_provider: Optional[PortfolioProvider] = None,
144
+ alerts_provider: Optional[AlertsProvider] = None,
145
+ strategies_provider: Optional[StrategiesProvider] = None,
146
+ allow_control: bool = False,
147
+ url_prefix: str = "/api/v1/companion",
148
+ ):
149
+ self.app = app or Flask(__name__)
150
+ self.api_key = resolve_api_key(
151
+ api_key, env_names=("API_KEY", "REMOTE_MONITOR_API_KEY", "COMPANION_API_KEY")
152
+ )
153
+ if require_api_key is None:
154
+ require_api_key = not is_insecure_secret(self.api_key)
155
+ self.require_api_key = require_api_key
156
+ self.allow_control = allow_control
157
+ self.url_prefix = url_prefix.rstrip("/") or "/api/v1/companion"
158
+
159
+ self._portfolio_provider = portfolio_provider or (lambda: {})
160
+ self._alerts_provider = alerts_provider or (lambda: [])
161
+ self._strategies_provider = strategies_provider or (lambda: {"strategies": {}})
162
+ self.webhook_notifier = WebhookNotifier()
163
+ self.control_callbacks: Dict[str, Callable[[], bool]] = {}
164
+
165
+ self.setup_routes()
166
+
167
+ def set_portfolio_provider(self, provider: PortfolioProvider) -> None:
168
+ self._portfolio_provider = provider
169
+
170
+ def set_alerts_provider(self, provider: AlertsProvider) -> None:
171
+ self._alerts_provider = provider
172
+
173
+ def set_strategies_provider(self, provider: StrategiesProvider) -> None:
174
+ self._strategies_provider = provider
175
+
176
+ def register_control_callback(self, action: str, callback: Callable[[], bool]) -> None:
177
+ """Register optional start/stop callbacks (disabled unless allow_control)."""
178
+ self.control_callbacks[action] = callback
179
+
180
+ def _auth_response(self):
181
+ if not self._check_auth():
182
+ return jsonify({"error": "Unauthorized"}), 401
183
+ return None
184
+
185
+ def _check_auth(self) -> bool:
186
+ if not self.require_api_key:
187
+ return True
188
+ if is_insecure_secret(self.api_key):
189
+ return False
190
+ token = extract_bearer_or_api_key(
191
+ request.headers.get("Authorization", ""),
192
+ request.headers.get("X-API-Key", ""),
193
+ )
194
+ return constant_time_equals(token, self.api_key)
195
+
196
+ def setup_routes(self) -> None:
197
+ prefix = self.url_prefix
198
+
199
+ @self.app.route(f"{prefix}/health", methods=["GET"])
200
+ def companion_health():
201
+ """Public companion health (no trading secrets)."""
202
+ return jsonify(
203
+ {
204
+ "status": "healthy",
205
+ "service": "companion",
206
+ "version": package_version(),
207
+ "auth_required": self.require_api_key,
208
+ "read_only": not self.allow_control,
209
+ "features": {
210
+ "portfolio": True,
211
+ "alerts": True,
212
+ "strategies": True,
213
+ "webhooks": True,
214
+ "control": self.allow_control,
215
+ },
216
+ "timestamp": datetime.now().isoformat(),
217
+ }
218
+ )
219
+
220
+ @self.app.route(f"{prefix}/portfolio", methods=["GET"])
221
+ def companion_portfolio():
222
+ denied = self._auth_response()
223
+ if denied:
224
+ return denied
225
+ try:
226
+ return jsonify(_normalize_portfolio(self._portfolio_provider() or {}))
227
+ except Exception as exc:
228
+ logger.exception("Companion portfolio failed")
229
+ return jsonify({"error": str(exc)}), 500
230
+
231
+ @self.app.route(f"{prefix}/positions", methods=["GET"])
232
+ def companion_positions():
233
+ denied = self._auth_response()
234
+ if denied:
235
+ return denied
236
+ try:
237
+ snapshot = _normalize_portfolio(self._portfolio_provider() or {})
238
+ return jsonify(
239
+ {
240
+ "positions": snapshot["positions"],
241
+ "count": snapshot["positions_count"],
242
+ "timestamp": snapshot["timestamp"],
243
+ "read_only": True,
244
+ }
245
+ )
246
+ except Exception as exc:
247
+ return jsonify({"error": str(exc)}), 500
248
+
249
+ @self.app.route(f"{prefix}/alerts", methods=["GET"])
250
+ def companion_alerts():
251
+ denied = self._auth_response()
252
+ if denied:
253
+ return denied
254
+ try:
255
+ alerts = self._alerts_provider() or []
256
+ return jsonify({"alerts": list(alerts), "count": len(alerts)})
257
+ except Exception as exc:
258
+ return jsonify({"error": str(exc)}), 500
259
+
260
+ @self.app.route(f"{prefix}/strategies", methods=["GET"])
261
+ def companion_strategies():
262
+ denied = self._auth_response()
263
+ if denied:
264
+ return denied
265
+ try:
266
+ payload = self._strategies_provider() or {}
267
+ if "strategies" not in payload:
268
+ payload = {"strategies": payload}
269
+ payload["timestamp"] = datetime.now().isoformat()
270
+ return jsonify(payload)
271
+ except Exception as exc:
272
+ return jsonify({"error": str(exc)}), 500
273
+
274
+ @self.app.route(f"{prefix}/devices/webhook", methods=["POST"])
275
+ def register_webhook():
276
+ denied = self._auth_response()
277
+ if denied:
278
+ return denied
279
+ data = request.get_json(silent=True) or {}
280
+ url = str(data.get("url") or data.get("webhook_url") or "").strip()
281
+ if not self.webhook_notifier.register(url):
282
+ return jsonify(
283
+ {
284
+ "error": "Invalid webhook URL. Use https://… or http://127.0.0.1/…",
285
+ }
286
+ ), 400
287
+ return jsonify(
288
+ {
289
+ "success": True,
290
+ "endpoints": self.webhook_notifier.list_endpoints(),
291
+ }
292
+ ), 201
293
+
294
+ @self.app.route(f"{prefix}/devices/webhook", methods=["DELETE"])
295
+ def unregister_webhook():
296
+ denied = self._auth_response()
297
+ if denied:
298
+ return denied
299
+ data = request.get_json(silent=True) or {}
300
+ url = str(data.get("url") or data.get("webhook_url") or "").strip()
301
+ removed = self.webhook_notifier.unregister(url)
302
+ return jsonify({"success": removed})
303
+
304
+ if self.allow_control:
305
+
306
+ @self.app.route(f"{prefix}/control/start", methods=["POST"])
307
+ def companion_start():
308
+ denied = self._auth_response()
309
+ if denied:
310
+ return denied
311
+ cb = self.control_callbacks.get("start_trading")
312
+ if not cb:
313
+ return jsonify({"error": "Control not available"}), 400
314
+ return jsonify({"success": bool(cb())})
315
+
316
+ @self.app.route(f"{prefix}/control/stop", methods=["POST"])
317
+ def companion_stop():
318
+ denied = self._auth_response()
319
+ if denied:
320
+ return denied
321
+ cb = self.control_callbacks.get("stop_trading")
322
+ if not cb:
323
+ return jsonify({"error": "Control not available"}), 400
324
+ return jsonify({"success": bool(cb())})
@@ -0,0 +1,184 @@
1
+ """
2
+ Mobile companion helpers.
3
+
4
+ Prefer :class:`forexsmartbot.cloud.companion_api.CompanionAPI` for new code.
5
+ This module keeps a thin compatibility layer and optional FCM push support.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import os
12
+ from typing import Dict, List, Optional
13
+
14
+ import requests
15
+ from dotenv import load_dotenv
16
+ from flask import Flask
17
+
18
+ from .companion_api import CompanionAPI, WebhookNotifier
19
+
20
+ load_dotenv(override=False)
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class MobileAppAPI(CompanionAPI):
26
+ """
27
+ Backward-compatible mobile API wrapper.
28
+
29
+ Defaults to **read-only** companion endpoints. Pass ``allow_control=True``
30
+ only when high-level start/stop from a trusted device is intentional.
31
+ Legacy ``/api/v1/mobile/*`` routes are not registered; use
32
+ ``/api/v1/companion/*`` (also mounted by RemoteMonitorServer).
33
+ """
34
+
35
+ def __init__(self, app: Flask = None, **kwargs):
36
+ kwargs.setdefault("allow_control", False)
37
+ kwargs.setdefault("url_prefix", "/api/v1/companion")
38
+ super().__init__(app=app, **kwargs)
39
+ self.push_notification_service: Optional[PushNotificationService] = None
40
+ self.trading_controller = None
41
+ self.broker = None
42
+
43
+ def set_trading_controller(self, controller):
44
+ """Set trading controller reference (optional control path)."""
45
+ self.trading_controller = controller
46
+
47
+ def start():
48
+ if hasattr(controller, "start"):
49
+ controller.start()
50
+ return True
51
+ return False
52
+
53
+ def stop():
54
+ if hasattr(controller, "stop"):
55
+ controller.stop()
56
+ return True
57
+ return False
58
+
59
+ self.register_control_callback("start_trading", start)
60
+ self.register_control_callback("stop_trading", stop)
61
+
62
+ def set_broker(self, broker):
63
+ """Bind a live broker as the portfolio provider."""
64
+ self.broker = broker
65
+
66
+ def portfolio():
67
+ if not broker:
68
+ return {}
69
+ positions = []
70
+ try:
71
+ raw = broker.get_positions() or {}
72
+ for symbol, pos in raw.items():
73
+ positions.append(
74
+ {
75
+ "symbol": getattr(pos, "symbol", symbol),
76
+ "side": "Long" if getattr(pos, "side", 0) > 0 else "Short",
77
+ "quantity": getattr(pos, "quantity", 0),
78
+ "entry_price": getattr(pos, "entry_price", 0),
79
+ "current_price": getattr(pos, "current_price", 0),
80
+ "pnl": getattr(pos, "pnl", 0),
81
+ "stop_loss": getattr(pos, "stop_loss", None),
82
+ "take_profit": getattr(pos, "take_profit", None),
83
+ }
84
+ )
85
+ except Exception as exc:
86
+ logger.warning("Broker positions failed: %s", exc)
87
+ balance = 0.0
88
+ equity = 0.0
89
+ try:
90
+ balance = float(broker.get_balance())
91
+ equity = (
92
+ float(broker.get_equity())
93
+ if hasattr(broker, "get_equity")
94
+ else balance
95
+ )
96
+ except Exception as exc:
97
+ logger.warning("Broker balance failed: %s", exc)
98
+ status = "disconnected"
99
+ try:
100
+ status = "connected" if broker.is_connected() else "disconnected"
101
+ except Exception:
102
+ pass
103
+ return {
104
+ "balance": balance,
105
+ "equity": equity,
106
+ "positions": positions,
107
+ "trades": [],
108
+ "status": status,
109
+ }
110
+
111
+ self.set_portfolio_provider(portfolio)
112
+
113
+
114
+ class PushNotificationService:
115
+ """
116
+ Optional FCM push (requires Google infrastructure).
117
+
118
+ For GrapheneOS / de-Googled devices prefer :class:`WebhookNotifier`.
119
+ """
120
+
121
+ def __init__(self, fcm_server_key: Optional[str] = None):
122
+ self.fcm_server_key = fcm_server_key or os.getenv("FCM_SERVER_KEY", "")
123
+ self.fcm_url = "https://fcm.googleapis.com/fcm/send"
124
+ self.device_tokens: List[str] = []
125
+ self.webhook_notifier = WebhookNotifier()
126
+
127
+ def register_device(self, token: str):
128
+ if token and token not in self.device_tokens:
129
+ self.device_tokens.append(token)
130
+
131
+ def unregister_device(self, token: str):
132
+ if token in self.device_tokens:
133
+ self.device_tokens.remove(token)
134
+
135
+ def register_webhook(self, url: str) -> bool:
136
+ """GrapheneOS-friendly alert delivery (no FCM)."""
137
+ return self.webhook_notifier.register(url)
138
+
139
+ def send_notification(self, title: str, body: str, data: Optional[Dict] = None):
140
+ webhook_ok = self.webhook_notifier.notify(title, body, data)
141
+ if not self.fcm_server_key or not self.device_tokens:
142
+ return webhook_ok > 0
143
+
144
+ try:
145
+ for token in self.device_tokens:
146
+ payload = {
147
+ "to": token,
148
+ "notification": {
149
+ "title": title,
150
+ "body": body,
151
+ "sound": "default",
152
+ },
153
+ "data": data or {},
154
+ }
155
+ headers = {
156
+ "Authorization": f"key={self.fcm_server_key}",
157
+ "Content-Type": "application/json",
158
+ }
159
+ response = requests.post(
160
+ self.fcm_url, json=payload, headers=headers, timeout=5
161
+ )
162
+ if response.status_code != 200:
163
+ logger.warning("FCM failed: %s", response.status_code)
164
+ return True
165
+ except Exception as exc:
166
+ logger.warning("FCM error: %s", exc)
167
+ return webhook_ok > 0
168
+
169
+ def send_trade_alert(
170
+ self, trade_type: str, symbol: str, price: float, pnl: Optional[float] = None
171
+ ):
172
+ title = f"Trade {trade_type}: {symbol}"
173
+ body = f"Price: {price:.4f}"
174
+ if pnl is not None:
175
+ body += f", PnL: ${pnl:.2f}"
176
+ data = {"type": "trade", "symbol": symbol, "price": price, "pnl": pnl}
177
+ return self.send_notification(title, body, data)
178
+
179
+ def send_alert(self, alert_type: str, message: str):
180
+ return self.send_notification(
181
+ f"Alert: {alert_type}",
182
+ message,
183
+ {"type": "alert", "alert_type": alert_type, "message": message},
184
+ )