forexsmartbot 3.2.4__tar.gz → 3.2.5__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 (151) hide show
  1. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/PKG-INFO +1 -1
  2. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/__init__.py +1 -0
  3. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/cloud/__init__.py +8 -1
  4. forexsmartbot-3.2.5/forexsmartbot/cloud/api_client.py +231 -0
  5. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/cloud/api_server.py +76 -28
  6. forexsmartbot-3.2.5/forexsmartbot/cloud/auth_utils.py +151 -0
  7. forexsmartbot-3.2.5/forexsmartbot/cloud/cloud_sync.py +389 -0
  8. forexsmartbot-3.2.5/forexsmartbot/cloud/error_handler.py +194 -0
  9. forexsmartbot-3.2.5/forexsmartbot/cloud/remote_monitor.py +445 -0
  10. forexsmartbot-3.2.5/forexsmartbot/cloud/security.py +249 -0
  11. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/testing/ci_cd_pipeline.py +39 -15
  12. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/testing/paper_account_tester.py +11 -4
  13. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/testing/strategy_sandbox.py +21 -7
  14. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot.egg-info/PKG-INFO +1 -1
  15. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot.egg-info/SOURCES.txt +2 -0
  16. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/pyproject.toml +1 -1
  17. forexsmartbot-3.2.5/tests/test_cloud_hardening.py +226 -0
  18. forexsmartbot-3.2.4/forexsmartbot/cloud/api_client.py +0 -188
  19. forexsmartbot-3.2.4/forexsmartbot/cloud/cloud_sync.py +0 -279
  20. forexsmartbot-3.2.4/forexsmartbot/cloud/error_handler.py +0 -150
  21. forexsmartbot-3.2.4/forexsmartbot/cloud/remote_monitor.py +0 -310
  22. forexsmartbot-3.2.4/forexsmartbot/cloud/security.py +0 -208
  23. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/LICENSE +0 -0
  24. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/README.md +0 -0
  25. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/__init__.py +0 -0
  26. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/brokers/__init__.py +0 -0
  27. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/brokers/ib_tws_broker.py +0 -0
  28. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/brokers/mt4_broker.py +0 -0
  29. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/brokers/paper_broker.py +0 -0
  30. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/brokers/rest_broker.py +0 -0
  31. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/__init__.py +0 -0
  32. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/alpha_vantage_provider.py +0 -0
  33. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/config.py +0 -0
  34. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/csv_provider.py +0 -0
  35. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/dummy_provider.py +0 -0
  36. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/mt4_provider.py +0 -0
  37. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/multi_provider.py +0 -0
  38. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/oanda_provider.py +0 -0
  39. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/stooq_provider.py +0 -0
  40. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/twelve_data_provider.py +0 -0
  41. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/adapters/data/yfinance_provider.py +0 -0
  42. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/__init__.py +0 -0
  43. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/chart_patterns.py +0 -0
  44. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/correlation_matrix.py +0 -0
  45. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/economic_calendar.py +0 -0
  46. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/enhanced_chart.py +0 -0
  47. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/live_metrics.py +0 -0
  48. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/market_depth.py +0 -0
  49. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/performance_attribution.py +0 -0
  50. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/portfolio_analytics.py +0 -0
  51. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/risk_analytics.py +0 -0
  52. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/strategy_comparison.py +0 -0
  53. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/analytics/trade_journal.py +0 -0
  54. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/brokers/mt4_bridge.py +0 -0
  55. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/builder/__init__.py +0 -0
  56. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/builder/code_generator.py +0 -0
  57. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/builder/strategy_builder.py +0 -0
  58. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/builder/strategy_template.py +0 -0
  59. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/cloud/mobile_api.py +0 -0
  60. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/core/backtester.py +0 -0
  61. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/core/config.py +0 -0
  62. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/core/interfaces.py +0 -0
  63. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/core/paper_broker.py +0 -0
  64. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/core/portfolio.py +0 -0
  65. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/core/risk.py +0 -0
  66. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/core/risk_engine.py +0 -0
  67. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/core/strategy.py +0 -0
  68. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/core/trade_manager.py +0 -0
  69. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/gui/main_window.py +0 -0
  70. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/gui/theme.py +0 -0
  71. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/marketplace/__init__.py +0 -0
  72. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/marketplace/marketplace.py +0 -0
  73. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/marketplace/strategy_rating.py +0 -0
  74. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/monitoring/__init__.py +0 -0
  75. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/monitoring/health_check.py +0 -0
  76. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/monitoring/performance_tracker.py +0 -0
  77. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/monitoring/strategy_monitor.py +0 -0
  78. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/optimization/__init__.py +0 -0
  79. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/optimization/adaptive_parameters.py +0 -0
  80. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/optimization/genetic_optimizer.py +0 -0
  81. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/optimization/hyperparameter_optimizer.py +0 -0
  82. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/optimization/monte_carlo.py +0 -0
  83. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/optimization/multi_objective_optimizer.py +0 -0
  84. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/optimization/parameter_sensitivity.py +0 -0
  85. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/optimization/walk_forward.py +0 -0
  86. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/__init__.py +0 -0
  87. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/backtest.py +0 -0
  88. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/calculation_engine.py +0 -0
  89. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/controller.py +0 -0
  90. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/data_streaming.py +0 -0
  91. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/enhanced_backtest.py +0 -0
  92. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/fast_trading_controller.py +0 -0
  93. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/gpu_backtest.py +0 -0
  94. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/language_manager.py +0 -0
  95. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/notification_service.py +0 -0
  96. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/persistence.py +0 -0
  97. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/services/startup_manager.py +0 -0
  98. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/__init__.py +0 -0
  99. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/adaptive_trend_flow.py +0 -0
  100. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/breakout_atr.py +0 -0
  101. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/ensemble_ml_strategy.py +0 -0
  102. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/fear_index.py +0 -0
  103. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/lstm_strategy.py +0 -0
  104. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/mean_reversion.py +0 -0
  105. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/ml_adaptive_supertrend.py +0 -0
  106. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/momentum_breakout.py +0 -0
  107. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/multi_timeframe_strategy.py +0 -0
  108. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/news_trading.py +0 -0
  109. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/rl_strategy.py +0 -0
  110. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/rsi_reversion.py +0 -0
  111. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/scalping_ma.py +0 -0
  112. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/sma_crossover.py +0 -0
  113. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/svm_strategy.py +0 -0
  114. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/strategies/transformer_strategy.py +0 -0
  115. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/testing/__init__.py +0 -0
  116. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/__init__.py +0 -0
  117. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/analytics_dialog.py +0 -0
  118. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/analytics_widget.py +0 -0
  119. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/backtest_dialog.py +0 -0
  120. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/charts.py +0 -0
  121. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/cloud_dialog.py +0 -0
  122. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/enhanced_charts.py +0 -0
  123. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/enhanced_main_window.py +0 -0
  124. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/export_dialog.py +0 -0
  125. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/main_window.py +0 -0
  126. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/marketplace_dialog.py +0 -0
  127. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/monitoring_dialog.py +0 -0
  128. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/monitoring_widget.py +0 -0
  129. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/optimization_dialog.py +0 -0
  130. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/settings_dialog.py +0 -0
  131. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/simple_chart.py +0 -0
  132. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/strategy_builder_dialog.py +0 -0
  133. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/theme.py +0 -0
  134. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/ui/tradingview_chart.py +0 -0
  135. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/utils/__init__.py +0 -0
  136. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/utils/backtest_utils.py +0 -0
  137. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/utils/gpu_numpy.py +0 -0
  138. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/utils/gpu_utils.py +0 -0
  139. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot/utils/native_indicators.py +0 -0
  140. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot.egg-info/dependency_links.txt +0 -0
  141. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot.egg-info/entry_points.txt +0 -0
  142. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot.egg-info/requires.txt +0 -0
  143. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/forexsmartbot.egg-info/top_level.txt +0 -0
  144. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/setup.cfg +0 -0
  145. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/tests/test_backtest_dialog_ui.py +0 -0
  146. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/tests/test_config.py +0 -0
  147. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/tests/test_live_metrics.py +0 -0
  148. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/tests/test_multi_timeframe_strategy.py +0 -0
  149. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/tests/test_native_indicators.py +0 -0
  150. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/tests/test_risk.py +0 -0
  151. {forexsmartbot-3.2.4 → forexsmartbot-3.2.5}/tests/test_strategies.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forexsmartbot
3
- Version: 3.2.4
3
+ Version: 3.2.5
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 +1,2 @@
1
1
  # ForexSmartBot Package
2
+ __version__ = "3.2.5"
@@ -1,5 +1,5 @@
1
1
  """
2
- Cloud Integration Module for ForexSmartBot v3.3.0
2
+ Cloud Integration Module for ForexSmartBot
3
3
  Provides cloud sync, remote monitoring, and API access.
4
4
  """
5
5
 
@@ -9,6 +9,8 @@ from .api_server import APIServer, WebSocketServer
9
9
  from .mobile_api import MobileAppAPI, PushNotificationService
10
10
  from .security import SecurityManager, RateLimiter
11
11
  from .api_client import APIClient, WebSocketClient
12
+ from .error_handler import CloudErrorHandler, CloudHealthMonitor, retry_call
13
+ from .auth_utils import generate_api_token, package_version
12
14
 
13
15
  __all__ = [
14
16
  'CloudSyncManager',
@@ -21,4 +23,9 @@ __all__ = [
21
23
  'RateLimiter',
22
24
  'APIClient',
23
25
  'WebSocketClient',
26
+ 'CloudErrorHandler',
27
+ 'CloudHealthMonitor',
28
+ 'retry_call',
29
+ 'generate_api_token',
30
+ 'package_version',
24
31
  ]
@@ -0,0 +1,231 @@
1
+ """
2
+ Python SDK for ForexSmartBot API
3
+ Provides easy-to-use client for REST API and WebSocket API.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ from typing import Dict, List, Optional
11
+
12
+ import requests
13
+ import websockets
14
+ from dotenv import load_dotenv
15
+
16
+ from .auth_utils import resolve_api_key
17
+ from .error_handler import retry_call
18
+
19
+ load_dotenv(override=False)
20
+
21
+
22
+ class APIClient:
23
+ """Python client for ForexSmartBot REST API."""
24
+
25
+ def __init__(
26
+ self,
27
+ api_key: Optional[str] = None,
28
+ base_url: Optional[str] = None,
29
+ *,
30
+ max_retries: int = 3,
31
+ timeout: float = 10.0,
32
+ ):
33
+ """
34
+ Initialize API client.
35
+
36
+ Args:
37
+ api_key: API key for authentication (defaults to API_KEY env var)
38
+ base_url: Base URL of the API server (defaults to API_BASE_URL env var or http://localhost:5000)
39
+ max_retries: Transient failure retries
40
+ timeout: Request timeout seconds
41
+ """
42
+ self.api_key = resolve_api_key(api_key, env_names=("API_KEY",))
43
+ default_base = os.getenv("API_BASE_URL", "http://localhost:5000")
44
+ self.base_url = (base_url or default_base).rstrip("/")
45
+ self.max_retries = max_retries
46
+ self.timeout = timeout
47
+ self.session = requests.Session()
48
+ self.session.headers.update({"Content-Type": "application/json"})
49
+ if self.api_key:
50
+ self.session.headers["Authorization"] = f"Bearer {self.api_key}"
51
+ self.session.headers["X-API-Key"] = self.api_key
52
+
53
+ def _request(self, method: str, path: str, **kwargs) -> requests.Response:
54
+ url = f"{self.base_url}{path}"
55
+ kwargs.setdefault("timeout", self.timeout)
56
+
57
+ def _do() -> requests.Response:
58
+ response = self.session.request(method, url, **kwargs)
59
+ if response.status_code >= 500:
60
+ response.raise_for_status()
61
+ response.raise_for_status()
62
+ return response
63
+
64
+ return retry_call(_do, max_retries=self.max_retries, base_delay=1.0)
65
+
66
+ def health(self) -> Dict:
67
+ """Call public health endpoint."""
68
+ for path in ("/healthz", "/api/v1/health"):
69
+ try:
70
+ response = self.session.get(
71
+ f"{self.base_url}{path}", timeout=min(5.0, self.timeout)
72
+ )
73
+ if response.status_code == 200:
74
+ return response.json()
75
+ except Exception:
76
+ continue
77
+ raise requests.exceptions.ConnectionError(
78
+ f"Health endpoint unreachable at {self.base_url}"
79
+ )
80
+
81
+ def get_balance(self) -> float:
82
+ """Get account balance."""
83
+ response = self._request("GET", "/api/v1/account/balance")
84
+ return response.json()["balance"]
85
+
86
+ def get_positions(self) -> List[Dict]:
87
+ """Get open positions."""
88
+ response = self._request("GET", "/api/v1/account/positions")
89
+ return response.json()["positions"]
90
+
91
+ def get_trades(self) -> List[Dict]:
92
+ """Get trade history."""
93
+ response = self._request("GET", "/api/v1/trades")
94
+ return response.json()["trades"]
95
+
96
+ def create_order(
97
+ self,
98
+ symbol: str,
99
+ side: int,
100
+ quantity: float,
101
+ stop_loss: Optional[float] = None,
102
+ take_profit: Optional[float] = None,
103
+ ) -> Dict:
104
+ """
105
+ Create a new order.
106
+
107
+ Args:
108
+ symbol: Trading symbol
109
+ side: 1 for buy, -1 for sell
110
+ quantity: Order quantity
111
+ stop_loss: Stop loss price (optional)
112
+ take_profit: Take profit price (optional)
113
+
114
+ Returns:
115
+ Order response dictionary
116
+ """
117
+ payload = {"symbol": symbol, "side": side, "quantity": quantity}
118
+ if stop_loss:
119
+ payload["stop_loss"] = stop_loss
120
+ if take_profit:
121
+ payload["take_profit"] = take_profit
122
+
123
+ response = self._request("POST", "/api/v1/orders", json=payload)
124
+ return response.json()
125
+
126
+ def close_positions(self, symbol: str) -> Dict:
127
+ """
128
+ Close all positions for a symbol.
129
+
130
+ Args:
131
+ symbol: Trading symbol
132
+
133
+ Returns:
134
+ Response dictionary
135
+ """
136
+ response = self._request("DELETE", f"/api/v1/orders/{symbol}")
137
+ return response.json()
138
+
139
+ def get_price(self, symbol: str) -> float:
140
+ """
141
+ Get current price for a symbol.
142
+
143
+ Args:
144
+ symbol: Trading symbol
145
+
146
+ Returns:
147
+ Current price
148
+ """
149
+ response = self._request("GET", f"/api/v1/market/price/{symbol}")
150
+ return response.json()["price"]
151
+
152
+
153
+ class WebSocketClient:
154
+ """Python client for ForexSmartBot WebSocket API."""
155
+
156
+ def __init__(self, url: Optional[str] = None, api_key: Optional[str] = None):
157
+ """
158
+ Initialize WebSocket client.
159
+
160
+ Args:
161
+ url: WebSocket server URL (defaults to WS_URL env var or ws://localhost:8765)
162
+ api_key: API key for authentication (defaults to API_KEY env var)
163
+ """
164
+ default_url = os.getenv("WS_URL", "ws://localhost:8765")
165
+ self.url = url or default_url
166
+ self.api_key = resolve_api_key(api_key, env_names=("API_KEY", "WS_API_KEY"))
167
+ self.websocket = None
168
+
169
+ async def connect(self):
170
+ """Connect to WebSocket server and authenticate when a key is set."""
171
+ self.websocket = await websockets.connect(self.url)
172
+ if self.api_key:
173
+ await self.websocket.send(
174
+ json.dumps({"command": "auth", "token": self.api_key})
175
+ )
176
+ raw = await self.websocket.recv()
177
+ data = json.loads(raw)
178
+ if not data.get("ok"):
179
+ await self.disconnect()
180
+ raise PermissionError("WebSocket authentication failed")
181
+
182
+ async def disconnect(self):
183
+ """Disconnect from WebSocket server."""
184
+ if self.websocket:
185
+ await self.websocket.close()
186
+ self.websocket = None
187
+
188
+ async def subscribe(self, symbol: str):
189
+ """Subscribe to price updates for a symbol."""
190
+ if not self.websocket:
191
+ await self.connect()
192
+
193
+ message = {"command": "subscribe", "symbol": symbol}
194
+ await self.websocket.send(json.dumps(message))
195
+
196
+ async def get_balance(self) -> float:
197
+ """Get balance via WebSocket."""
198
+ if not self.websocket:
199
+ await self.connect()
200
+
201
+ message = {"command": "get_balance"}
202
+ await self.websocket.send(json.dumps(message))
203
+
204
+ response = await self.websocket.recv()
205
+ data = json.loads(response)
206
+ return data.get("value", 0.0)
207
+
208
+ async def get_positions(self) -> List[Dict]:
209
+ """Get positions via WebSocket."""
210
+ if not self.websocket:
211
+ await self.connect()
212
+
213
+ message = {"command": "get_positions"}
214
+ await self.websocket.send(json.dumps(message))
215
+
216
+ response = await self.websocket.recv()
217
+ data = json.loads(response)
218
+ return data.get("data", [])
219
+
220
+ async def listen(self, callback):
221
+ """Listen for real-time updates."""
222
+ if not self.websocket:
223
+ await self.connect()
224
+
225
+ while True:
226
+ try:
227
+ message = await self.websocket.recv()
228
+ data = json.loads(message)
229
+ callback(data)
230
+ except websockets.exceptions.ConnectionClosed:
231
+ break
@@ -1,5 +1,5 @@
1
1
  """
2
- API Server for ForexSmartBot v3.3.0
2
+ API Server for ForexSmartBot
3
3
  Provides REST API and WebSocket API for external integrations.
4
4
  """
5
5
 
@@ -13,16 +13,26 @@ from flask import Flask, jsonify, request
13
13
  from flask_cors import CORS
14
14
  from flask_limiter import Limiter
15
15
  from flask_limiter.util import get_remote_address
16
- import jwt
17
16
  from functools import wraps
18
17
  from threading import Thread
19
18
  import websockets
20
19
  from websockets.server import serve
21
20
  from dotenv import load_dotenv
22
21
 
22
+ from .auth_utils import (
23
+ constant_time_equals,
24
+ extract_bearer_or_api_key,
25
+ is_insecure_secret,
26
+ package_version,
27
+ resolve_api_key,
28
+ validate_server_credentials,
29
+ )
30
+
23
31
  # Ensure .env is loaded
24
32
  load_dotenv(override=False)
25
33
 
34
+ logger = logging.getLogger(__name__)
35
+
26
36
 
27
37
  class APIServer:
28
38
  """REST API server for external integrations."""
@@ -40,8 +50,8 @@ class APIServer:
40
50
  """
41
51
  self.host = host or os.getenv('API_HOST', '127.0.0.1')
42
52
  self.port = port or int(os.getenv('API_PORT', '5000'))
43
- self.api_key = api_key or os.getenv('API_KEY', '')
44
- self.secret_key = secret_key or os.getenv('API_SECRET_KEY', 'default-secret-key-change-in-production')
53
+ self.api_key = resolve_api_key(api_key, env_names=('API_KEY',))
54
+ self.secret_key = secret_key or os.getenv('API_SECRET_KEY', '')
45
55
 
46
56
  self.app = Flask(__name__)
47
57
  CORS(self.app)
@@ -60,16 +70,28 @@ class APIServer:
60
70
  self.setup_routes()
61
71
  self.server_thread = None
62
72
  self.running = False
73
+ self.started_at: Optional[datetime] = None
63
74
 
64
75
  def setup_routes(self):
65
76
  """Setup API routes."""
66
-
77
+
78
+ @self.app.route('/healthz', methods=['GET'])
67
79
  @self.app.route('/api/v1/health', methods=['GET'])
68
80
  def health():
69
- """Health check endpoint."""
81
+ """Health check endpoint (public)."""
82
+ broker_ok = False
83
+ if self.broker is not None:
84
+ try:
85
+ broker_ok = bool(self.broker.is_connected()) if hasattr(self.broker, 'is_connected') else True
86
+ except Exception:
87
+ broker_ok = False
70
88
  return jsonify({
71
- 'status': 'healthy',
72
- 'version': '3.3.0',
89
+ 'status': 'healthy' if self.running else 'starting',
90
+ 'service': 'api_server',
91
+ 'version': package_version(),
92
+ 'auth_configured': not is_insecure_secret(self.api_key),
93
+ 'broker_connected': broker_ok,
94
+ 'started_at': self.started_at.isoformat() if self.started_at else None,
73
95
  'timestamp': datetime.now().isoformat()
74
96
  })
75
97
 
@@ -193,20 +215,19 @@ class APIServer:
193
215
  return jsonify({'error': str(e)}), 500
194
216
 
195
217
  def require_auth(self, f):
196
- """Decorator for API authentication."""
218
+ """Decorator for API authentication (Bearer or X-API-Key, constant-time)."""
197
219
  @wraps(f)
198
220
  def decorated_function(*args, **kwargs):
199
- if not self.api_key:
221
+ if is_insecure_secret(self.api_key):
200
222
  return jsonify({'error': 'API key not configured'}), 500
201
-
202
- auth_header = request.headers.get('Authorization', '')
203
- if not auth_header.startswith('Bearer '):
204
- return jsonify({'error': 'Missing or invalid authorization header'}), 401
205
-
206
- token = auth_header[7:]
207
- if token != self.api_key:
223
+
224
+ token = extract_bearer_or_api_key(
225
+ request.headers.get('Authorization', ''),
226
+ request.headers.get('X-API-Key', ''),
227
+ )
228
+ if not constant_time_equals(token, self.api_key):
208
229
  return jsonify({'error': 'Invalid API key'}), 401
209
-
230
+
210
231
  return f(*args, **kwargs)
211
232
  return decorated_function
212
233
 
@@ -226,10 +247,19 @@ class APIServer:
226
247
  """Start the API server."""
227
248
  if self.running:
228
249
  return
229
-
250
+
251
+ validate_server_credentials(
252
+ self.api_key,
253
+ host=self.host,
254
+ secret_key=self.secret_key or None,
255
+ service_name='APIServer',
256
+ )
257
+
230
258
  self.running = True
259
+ self.started_at = datetime.now()
231
260
  self.server_thread = Thread(target=self._run_server, daemon=True)
232
261
  self.server_thread.start()
262
+ logger.info("API server started on http://%s:%s", self.host, self.port)
233
263
  print(f"API server started on http://{self.host}:{self.port}")
234
264
 
235
265
  def stop(self):
@@ -257,36 +287,54 @@ class WebSocketServer:
257
287
  """
258
288
  self.host = host or os.getenv('WS_HOST', '127.0.0.1')
259
289
  self.port = port or int(os.getenv('WS_PORT', '8765'))
260
- self.api_key = api_key or os.getenv('API_KEY', '')
290
+ self.api_key = resolve_api_key(api_key, env_names=('API_KEY', 'WS_API_KEY'))
261
291
  self.clients = set()
292
+ self.authenticated_clients = set()
262
293
  self.running = False
263
294
  self.broker = None
264
295
 
265
296
  async def register_client(self, websocket, path):
266
- """Register a new WebSocket client."""
267
- # Simple authentication check
268
- if self.api_key:
269
- # In production, implement proper authentication
270
- pass
271
-
297
+ """Register a new WebSocket client (must auth when API key configured)."""
272
298
  self.clients.add(websocket)
273
299
  print(f"WebSocket client connected: {websocket.remote_address}")
274
300
 
275
301
  try:
276
302
  async for message in websocket:
277
- # Handle incoming messages
278
303
  await self.handle_message(websocket, message)
279
304
  except websockets.exceptions.ConnectionClosed:
280
305
  pass
281
306
  finally:
282
- self.clients.remove(websocket)
307
+ self.clients.discard(websocket)
308
+ self.authenticated_clients.discard(websocket)
283
309
  print(f"WebSocket client disconnected: {websocket.remote_address}")
310
+
311
+ def _ws_authorized(self, websocket) -> bool:
312
+ if is_insecure_secret(self.api_key):
313
+ return True
314
+ return websocket in self.authenticated_clients
284
315
 
285
316
  async def handle_message(self, websocket, message):
286
317
  """Handle incoming WebSocket message."""
287
318
  try:
288
319
  data = json.loads(message)
289
320
  command = data.get('command')
321
+
322
+ if command == 'auth':
323
+ token = data.get('token') or data.get('api_key') or ''
324
+ if constant_time_equals(token, self.api_key):
325
+ self.authenticated_clients.add(websocket)
326
+ await websocket.send(json.dumps({'type': 'auth', 'ok': True}))
327
+ else:
328
+ self.authenticated_clients.discard(websocket)
329
+ await websocket.send(json.dumps({'type': 'auth', 'ok': False}))
330
+ return
331
+
332
+ if not self._ws_authorized(websocket):
333
+ await websocket.send(json.dumps({
334
+ 'type': 'error',
335
+ 'message': 'Unauthorized — send {"command":"auth","token":"..."} first'
336
+ }))
337
+ return
290
338
 
291
339
  if command == 'subscribe':
292
340
  symbol = data.get('symbol')
@@ -0,0 +1,151 @@
1
+ """
2
+ Shared authentication helpers for remote monitoring and cloud APIs.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import hmac
8
+ import logging
9
+ import os
10
+ import secrets
11
+ from typing import Optional, Tuple
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Weak / placeholder secrets that must never be used in production.
16
+ _INSECURE_SECRETS = frozenset(
17
+ {
18
+ "",
19
+ "default-secret-key-change-in-production",
20
+ "changeme",
21
+ "secret",
22
+ "password",
23
+ "api_key",
24
+ "test",
25
+ }
26
+ )
27
+
28
+
29
+ def package_version() -> str:
30
+ """Return installed package version, falling back to local __version__."""
31
+ try:
32
+ from importlib.metadata import version
33
+
34
+ return version("forexsmartbot")
35
+ except Exception:
36
+ try:
37
+ from forexsmartbot import __version__
38
+
39
+ return __version__
40
+ except Exception:
41
+ return "0.0.0"
42
+
43
+
44
+ def is_insecure_secret(value: Optional[str]) -> bool:
45
+ """True if the value is empty or a known placeholder secret."""
46
+ if value is None:
47
+ return True
48
+ normalized = value.strip().lower()
49
+ return normalized in _INSECURE_SECRETS or len(value.strip()) < 16
50
+
51
+
52
+ def allow_insecure_mode() -> bool:
53
+ """Explicit opt-in for local/dev use without a strong API key."""
54
+ return os.getenv("FSB_ALLOW_INSECURE_MONITOR", "").strip().lower() in {
55
+ "1",
56
+ "true",
57
+ "yes",
58
+ "on",
59
+ }
60
+
61
+
62
+ def extract_bearer_or_api_key(
63
+ authorization_header: str = "",
64
+ api_key_header: str = "",
65
+ ) -> Optional[str]:
66
+ """
67
+ Extract a token from Authorization: Bearer <token> or X-API-Key.
68
+
69
+ Returns None when neither header provides a token.
70
+ """
71
+ auth = (authorization_header or "").strip()
72
+ if auth.lower().startswith("bearer "):
73
+ token = auth[7:].strip()
74
+ if token:
75
+ return token
76
+ key = (api_key_header or "").strip()
77
+ return key or None
78
+
79
+
80
+ def constant_time_equals(provided: Optional[str], expected: Optional[str]) -> bool:
81
+ """Constant-time string compare; False if either side is missing."""
82
+ if provided is None or expected is None:
83
+ return False
84
+ try:
85
+ return hmac.compare_digest(provided.encode("utf-8"), expected.encode("utf-8"))
86
+ except Exception:
87
+ return False
88
+
89
+
90
+ def generate_api_token(nbytes: int = 32) -> str:
91
+ """Generate a cryptographically strong URL-safe API token."""
92
+ return secrets.token_urlsafe(nbytes)
93
+
94
+
95
+ def resolve_api_key(
96
+ explicit: Optional[str] = None,
97
+ env_names: Tuple[str, ...] = ("API_KEY", "CLOUD_API_KEY"),
98
+ ) -> str:
99
+ """Resolve API key from constructor arg or environment (first non-empty)."""
100
+ if explicit is not None and explicit.strip():
101
+ return explicit.strip()
102
+ for name in env_names:
103
+ value = os.getenv(name, "")
104
+ if value and value.strip():
105
+ return value.strip()
106
+ return ""
107
+
108
+
109
+ def validate_server_credentials(
110
+ api_key: str,
111
+ *,
112
+ host: str = "127.0.0.1",
113
+ secret_key: Optional[str] = None,
114
+ service_name: str = "service",
115
+ ) -> None:
116
+ """
117
+ Enforce safe defaults before binding a network service.
118
+
119
+ Raises ValueError when credentials are too weak for the bind address
120
+ unless FSB_ALLOW_INSECURE_MONITOR is set.
121
+ """
122
+ loopback = host in {"127.0.0.1", "localhost", "::1"}
123
+ insecure_key = is_insecure_secret(api_key)
124
+ insecure_secret = secret_key is not None and is_insecure_secret(secret_key)
125
+
126
+ if not insecure_key and not insecure_secret:
127
+ return
128
+
129
+ if allow_insecure_mode():
130
+ logger.warning(
131
+ "%s starting with insecure credentials (FSB_ALLOW_INSECURE_MONITOR=1)",
132
+ service_name,
133
+ )
134
+ return
135
+
136
+ if not loopback:
137
+ raise ValueError(
138
+ f"{service_name}: strong API_KEY required when binding to {host}. "
139
+ "Set API_KEY (min 16 chars) or FSB_ALLOW_INSECURE_MONITOR=1 for local testing only."
140
+ )
141
+
142
+ if insecure_key:
143
+ logger.warning(
144
+ "%s has no strong API key; localhost-only. Set API_KEY for authenticated access.",
145
+ service_name,
146
+ )
147
+ if insecure_secret:
148
+ logger.warning(
149
+ "%s using weak API_SECRET_KEY; set a strong secret for JWT signing.",
150
+ service_name,
151
+ )