forexsmartbot 3.2.0__py3-none-any.whl
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.
- forexsmartbot/__init__.py +1 -0
- forexsmartbot/adapters/__init__.py +4 -0
- forexsmartbot/adapters/brokers/__init__.py +8 -0
- forexsmartbot/adapters/brokers/ib_tws_broker.py +244 -0
- forexsmartbot/adapters/brokers/mt4_broker.py +266 -0
- forexsmartbot/adapters/brokers/paper_broker.py +131 -0
- forexsmartbot/adapters/brokers/rest_broker.py +304 -0
- forexsmartbot/adapters/data/__init__.py +27 -0
- forexsmartbot/adapters/data/alpha_vantage_provider.py +261 -0
- forexsmartbot/adapters/data/config.py +101 -0
- forexsmartbot/adapters/data/csv_provider.py +137 -0
- forexsmartbot/adapters/data/dummy_provider.py +35 -0
- forexsmartbot/adapters/data/mt4_provider.py +217 -0
- forexsmartbot/adapters/data/multi_provider.py +191 -0
- forexsmartbot/adapters/data/oanda_provider.py +173 -0
- forexsmartbot/adapters/data/stooq_provider.py +114 -0
- forexsmartbot/adapters/data/twelve_data_provider.py +117 -0
- forexsmartbot/adapters/data/yfinance_provider.py +246 -0
- forexsmartbot/analytics/__init__.py +28 -0
- forexsmartbot/analytics/chart_patterns.py +322 -0
- forexsmartbot/analytics/correlation_matrix.py +186 -0
- forexsmartbot/analytics/economic_calendar.py +616 -0
- forexsmartbot/analytics/enhanced_chart.py +57 -0
- forexsmartbot/analytics/market_depth.py +134 -0
- forexsmartbot/analytics/performance_attribution.py +210 -0
- forexsmartbot/analytics/portfolio_analytics.py +303 -0
- forexsmartbot/analytics/risk_analytics.py +308 -0
- forexsmartbot/analytics/strategy_comparison.py +194 -0
- forexsmartbot/analytics/trade_journal.py +235 -0
- forexsmartbot/brokers/mt4_bridge.py +49 -0
- forexsmartbot/builder/__init__.py +12 -0
- forexsmartbot/builder/code_generator.py +173 -0
- forexsmartbot/builder/strategy_builder.py +119 -0
- forexsmartbot/builder/strategy_template.py +138 -0
- forexsmartbot/cloud/__init__.py +24 -0
- forexsmartbot/cloud/api_client.py +188 -0
- forexsmartbot/cloud/api_server.py +366 -0
- forexsmartbot/cloud/cloud_sync.py +279 -0
- forexsmartbot/cloud/error_handler.py +150 -0
- forexsmartbot/cloud/mobile_api.py +213 -0
- forexsmartbot/cloud/remote_monitor.py +310 -0
- forexsmartbot/cloud/security.py +208 -0
- forexsmartbot/core/backtester.py +57 -0
- forexsmartbot/core/config.py +38 -0
- forexsmartbot/core/interfaces.py +253 -0
- forexsmartbot/core/paper_broker.py +28 -0
- forexsmartbot/core/portfolio.py +227 -0
- forexsmartbot/core/risk.py +24 -0
- forexsmartbot/core/risk_engine.py +203 -0
- forexsmartbot/core/strategy.py +44 -0
- forexsmartbot/core/trade_manager.py +261 -0
- forexsmartbot/gui/main_window.py +168 -0
- forexsmartbot/gui/theme.py +32 -0
- forexsmartbot/marketplace/__init__.py +10 -0
- forexsmartbot/marketplace/marketplace.py +178 -0
- forexsmartbot/marketplace/strategy_rating.py +64 -0
- forexsmartbot/monitoring/__init__.py +12 -0
- forexsmartbot/monitoring/health_check.py +109 -0
- forexsmartbot/monitoring/performance_tracker.py +213 -0
- forexsmartbot/monitoring/strategy_monitor.py +175 -0
- forexsmartbot/optimization/__init__.py +26 -0
- forexsmartbot/optimization/adaptive_parameters.py +224 -0
- forexsmartbot/optimization/genetic_optimizer.py +116 -0
- forexsmartbot/optimization/hyperparameter_optimizer.py +77 -0
- forexsmartbot/optimization/monte_carlo.py +114 -0
- forexsmartbot/optimization/multi_objective_optimizer.py +299 -0
- forexsmartbot/optimization/parameter_sensitivity.py +187 -0
- forexsmartbot/optimization/walk_forward.py +131 -0
- forexsmartbot/services/__init__.py +7 -0
- forexsmartbot/services/backtest.py +354 -0
- forexsmartbot/services/calculation_engine.py +99 -0
- forexsmartbot/services/controller.py +419 -0
- forexsmartbot/services/data_streaming.py +116 -0
- forexsmartbot/services/enhanced_backtest.py +390 -0
- forexsmartbot/services/fast_trading_controller.py +317 -0
- forexsmartbot/services/gpu_backtest.py +303 -0
- forexsmartbot/services/language_manager.py +621 -0
- forexsmartbot/services/notification_service.py +283 -0
- forexsmartbot/services/persistence.py +299 -0
- forexsmartbot/services/startup_manager.py +200 -0
- forexsmartbot/strategies/__init__.py +120 -0
- forexsmartbot/strategies/adaptive_trend_flow.py +311 -0
- forexsmartbot/strategies/breakout_atr.py +136 -0
- forexsmartbot/strategies/ensemble_ml_strategy.py +209 -0
- forexsmartbot/strategies/fear_index.py +174 -0
- forexsmartbot/strategies/lstm_strategy.py +269 -0
- forexsmartbot/strategies/mean_reversion.py +158 -0
- forexsmartbot/strategies/ml_adaptive_supertrend.py +272 -0
- forexsmartbot/strategies/momentum_breakout.py +183 -0
- forexsmartbot/strategies/multi_timeframe_strategy.py +186 -0
- forexsmartbot/strategies/news_trading.py +187 -0
- forexsmartbot/strategies/rl_strategy.py +277 -0
- forexsmartbot/strategies/rsi_reversion.py +162 -0
- forexsmartbot/strategies/scalping_ma.py +195 -0
- forexsmartbot/strategies/sma_crossover.py +118 -0
- forexsmartbot/strategies/svm_strategy.py +199 -0
- forexsmartbot/strategies/transformer_strategy.py +233 -0
- forexsmartbot/testing/__init__.py +14 -0
- forexsmartbot/testing/ci_cd_pipeline.py +286 -0
- forexsmartbot/testing/paper_account_tester.py +326 -0
- forexsmartbot/testing/strategy_sandbox.py +284 -0
- forexsmartbot/ui/__init__.py +8 -0
- forexsmartbot/ui/analytics_dialog.py +262 -0
- forexsmartbot/ui/analytics_widget.py +68 -0
- forexsmartbot/ui/backtest_dialog.py +908 -0
- forexsmartbot/ui/charts.py +172 -0
- forexsmartbot/ui/cloud_dialog.py +371 -0
- forexsmartbot/ui/enhanced_charts.py +491 -0
- forexsmartbot/ui/enhanced_main_window.py +2800 -0
- forexsmartbot/ui/export_dialog.py +333 -0
- forexsmartbot/ui/main_window.py +648 -0
- forexsmartbot/ui/marketplace_dialog.py +438 -0
- forexsmartbot/ui/monitoring_dialog.py +215 -0
- forexsmartbot/ui/monitoring_widget.py +80 -0
- forexsmartbot/ui/optimization_dialog.py +603 -0
- forexsmartbot/ui/settings_dialog.py +671 -0
- forexsmartbot/ui/simple_chart.py +309 -0
- forexsmartbot/ui/strategy_builder_dialog.py +595 -0
- forexsmartbot/ui/theme.py +109 -0
- forexsmartbot/ui/tradingview_chart.py +736 -0
- forexsmartbot/utils/__init__.py +42 -0
- forexsmartbot/utils/gpu_numpy.py +194 -0
- forexsmartbot/utils/gpu_utils.py +260 -0
- forexsmartbot-3.2.0.dist-info/METADATA +177 -0
- forexsmartbot-3.2.0.dist-info/RECORD +129 -0
- forexsmartbot-3.2.0.dist-info/WHEEL +5 -0
- forexsmartbot-3.2.0.dist-info/entry_points.txt +2 -0
- forexsmartbot-3.2.0.dist-info/licenses/LICENSE +150 -0
- forexsmartbot-3.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""OANDA data provider implementation."""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
import time
|
|
8
|
+
from ...core.interfaces import IDataProvider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OANDAProvider(IDataProvider):
|
|
12
|
+
"""OANDA data provider for forex data."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, api_key: str = None, account_id: str = None):
|
|
15
|
+
self.api_key = api_key or "demo" # Use demo if none provided
|
|
16
|
+
self.account_id = account_id or "101-001-123456-001" # Demo account
|
|
17
|
+
self.base_url = "https://api-fxpractice.oanda.com" # Demo environment
|
|
18
|
+
self._available = True
|
|
19
|
+
|
|
20
|
+
def get_data(self, symbol: str, start: str, end: str,
|
|
21
|
+
interval: str = "1h") -> pd.DataFrame:
|
|
22
|
+
"""Get historical data from OANDA."""
|
|
23
|
+
try:
|
|
24
|
+
# Convert forex symbol to OANDA format
|
|
25
|
+
if symbol.endswith('=X'):
|
|
26
|
+
symbol = symbol.replace('=X', '')
|
|
27
|
+
|
|
28
|
+
# OANDA uses different format (e.g., EUR_USD instead of EURUSD)
|
|
29
|
+
oanda_symbol = f"{symbol[:3]}_{symbol[3:]}"
|
|
30
|
+
|
|
31
|
+
# Map interval to OANDA granularity
|
|
32
|
+
granularity = self._map_interval(interval)
|
|
33
|
+
|
|
34
|
+
# Convert dates to OANDA format
|
|
35
|
+
start_date = pd.to_datetime(start).strftime('%Y-%m-%dT%H:%M:%S.000000000Z')
|
|
36
|
+
end_date = pd.to_datetime(end).strftime('%Y-%m-%dT%H:%M:%S.000000000Z')
|
|
37
|
+
|
|
38
|
+
url = f"{self.base_url}/v3/instruments/{oanda_symbol}/candles"
|
|
39
|
+
params = {
|
|
40
|
+
'from': start_date,
|
|
41
|
+
'to': end_date,
|
|
42
|
+
'granularity': granularity,
|
|
43
|
+
'price': 'M' # Mid prices
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
headers = {
|
|
47
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
48
|
+
'Content-Type': 'application/json'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
response = requests.get(url, params=params, headers=headers, timeout=30)
|
|
52
|
+
data = response.json()
|
|
53
|
+
|
|
54
|
+
if 'candles' not in data:
|
|
55
|
+
print(f"No candles data for {symbol}: {data.get('errorMessage', 'Unknown error')}")
|
|
56
|
+
return pd.DataFrame(columns=['Open', 'High', 'Low', 'Close', 'Volume'])
|
|
57
|
+
|
|
58
|
+
candles = data['candles']
|
|
59
|
+
if not candles:
|
|
60
|
+
print(f"No candles available for {symbol}")
|
|
61
|
+
return pd.DataFrame(columns=['Open', 'High', 'Low', 'Close', 'Volume'])
|
|
62
|
+
|
|
63
|
+
# Convert to DataFrame
|
|
64
|
+
df_data = []
|
|
65
|
+
for candle in candles:
|
|
66
|
+
if candle['complete']: # Only use complete candles
|
|
67
|
+
df_data.append({
|
|
68
|
+
'Open': float(candle['mid']['o']),
|
|
69
|
+
'High': float(candle['mid']['h']),
|
|
70
|
+
'Low': float(candle['mid']['l']),
|
|
71
|
+
'Close': float(candle['mid']['c']),
|
|
72
|
+
'Volume': int(candle['volume'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
if not df_data:
|
|
76
|
+
print(f"No complete candles for {symbol}")
|
|
77
|
+
return pd.DataFrame(columns=['Open', 'High', 'Low', 'Close', 'Volume'])
|
|
78
|
+
|
|
79
|
+
df = pd.DataFrame(df_data)
|
|
80
|
+
df.index = pd.to_datetime([candle['time'] for candle in candles if candle['complete']])
|
|
81
|
+
|
|
82
|
+
return df.sort_index()
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Error getting OANDA data for {symbol}: {e}")
|
|
86
|
+
return pd.DataFrame(columns=['Open', 'High', 'Low', 'Close', 'Volume'])
|
|
87
|
+
|
|
88
|
+
def get_latest_price(self, symbol: str) -> Optional[float]:
|
|
89
|
+
"""Get latest price from OANDA."""
|
|
90
|
+
try:
|
|
91
|
+
# Convert forex symbol to OANDA format
|
|
92
|
+
if symbol.endswith('=X'):
|
|
93
|
+
symbol = symbol.replace('=X', '')
|
|
94
|
+
|
|
95
|
+
oanda_symbol = f"{symbol[:3]}_{symbol[3:]}"
|
|
96
|
+
|
|
97
|
+
url = f"{self.base_url}/v3/accounts/{self.account_id}/pricing"
|
|
98
|
+
params = {
|
|
99
|
+
'instruments': oanda_symbol
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
headers = {
|
|
103
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
104
|
+
'Content-Type': 'application/json'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
response = requests.get(url, params=params, headers=headers, timeout=30)
|
|
108
|
+
data = response.json()
|
|
109
|
+
|
|
110
|
+
if 'prices' in data and data['prices']:
|
|
111
|
+
price_data = data['prices'][0]
|
|
112
|
+
if 'bids' in price_data and 'asks' in price_data:
|
|
113
|
+
bid = float(price_data['bids'][0]['price'])
|
|
114
|
+
ask = float(price_data['asks'][0]['price'])
|
|
115
|
+
return (bid + ask) / 2 # Return mid price
|
|
116
|
+
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f"Error getting OANDA latest price for {symbol}: {e}")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def get_historical_data(self, symbol: str, period: str = '1d', interval: str = '1h') -> pd.DataFrame:
|
|
124
|
+
"""Get historical data for a symbol with specified period and interval."""
|
|
125
|
+
try:
|
|
126
|
+
# Convert period to start/end dates
|
|
127
|
+
now = datetime.now()
|
|
128
|
+
|
|
129
|
+
if period == '1d':
|
|
130
|
+
end_date = now
|
|
131
|
+
start_date = now - timedelta(days=1)
|
|
132
|
+
elif period == '5d':
|
|
133
|
+
end_date = now
|
|
134
|
+
start_date = now - timedelta(days=5)
|
|
135
|
+
elif period == '1mo':
|
|
136
|
+
end_date = now
|
|
137
|
+
start_date = now - timedelta(days=30)
|
|
138
|
+
elif period == '3mo':
|
|
139
|
+
end_date = now
|
|
140
|
+
start_date = now - timedelta(days=90)
|
|
141
|
+
elif period == '6mo':
|
|
142
|
+
end_date = now
|
|
143
|
+
start_date = now - timedelta(days=180)
|
|
144
|
+
elif period == '1y':
|
|
145
|
+
end_date = now
|
|
146
|
+
start_date = now - timedelta(days=365)
|
|
147
|
+
else:
|
|
148
|
+
# Default to 1 day
|
|
149
|
+
end_date = now
|
|
150
|
+
start_date = now - timedelta(days=1)
|
|
151
|
+
|
|
152
|
+
return self.get_data(symbol, start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d'), interval)
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
print(f"Error getting OANDA historical data for {symbol}: {e}")
|
|
156
|
+
return pd.DataFrame(columns=['Open', 'High', 'Low', 'Close', 'Volume'])
|
|
157
|
+
|
|
158
|
+
def _map_interval(self, interval: str) -> str:
|
|
159
|
+
"""Map interval to OANDA granularity."""
|
|
160
|
+
mapping = {
|
|
161
|
+
'1m': 'M1',
|
|
162
|
+
'5m': 'M5',
|
|
163
|
+
'15m': 'M15',
|
|
164
|
+
'30m': 'M30',
|
|
165
|
+
'1h': 'H1',
|
|
166
|
+
'4h': 'H4',
|
|
167
|
+
'1d': 'D'
|
|
168
|
+
}
|
|
169
|
+
return mapping.get(interval, 'H1')
|
|
170
|
+
|
|
171
|
+
def is_available(self) -> bool:
|
|
172
|
+
"""Check if OANDA is available."""
|
|
173
|
+
return self._available
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Stooq provider implementation (public CSV market data endpoint)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from ...core.interfaces import IDataProvider
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StooqProvider(IDataProvider):
|
|
16
|
+
"""Stooq provider for free forex data."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._available = True
|
|
20
|
+
|
|
21
|
+
def _to_stooq_symbol(self, symbol: str) -> str:
|
|
22
|
+
raw = symbol.replace("=X", "").replace("/", "").upper()
|
|
23
|
+
if len(raw) != 6:
|
|
24
|
+
raise ValueError(f"Unsupported forex symbol format: {symbol}")
|
|
25
|
+
return raw.lower()
|
|
26
|
+
|
|
27
|
+
def _map_interval(self, interval: str) -> str:
|
|
28
|
+
mapping = {
|
|
29
|
+
"1m": "1",
|
|
30
|
+
"5m": "5",
|
|
31
|
+
"15m": "15",
|
|
32
|
+
"30m": "30",
|
|
33
|
+
"1h": "60",
|
|
34
|
+
"4h": "240",
|
|
35
|
+
"1d": "d",
|
|
36
|
+
}
|
|
37
|
+
return mapping.get(interval, "d")
|
|
38
|
+
|
|
39
|
+
def get_data(self, symbol: str, start: str, end: str, interval: str = "1h") -> pd.DataFrame:
|
|
40
|
+
try:
|
|
41
|
+
s = self._to_stooq_symbol(symbol)
|
|
42
|
+
i = self._map_interval(interval)
|
|
43
|
+
# Stooq CSV endpoint
|
|
44
|
+
url = f"https://stooq.com/q/d/l/?s={s}&i={i}"
|
|
45
|
+
response = requests.get(url, timeout=15)
|
|
46
|
+
response.raise_for_status()
|
|
47
|
+
csv_txt = response.text
|
|
48
|
+
df = pd.read_csv(StringIO(csv_txt))
|
|
49
|
+
if df.empty or "Date" not in df.columns:
|
|
50
|
+
return pd.DataFrame(columns=["Open", "High", "Low", "Close", "Volume"])
|
|
51
|
+
|
|
52
|
+
if "Time" in df.columns:
|
|
53
|
+
df["datetime"] = pd.to_datetime(df["Date"] + " " + df["Time"], errors="coerce")
|
|
54
|
+
else:
|
|
55
|
+
df["datetime"] = pd.to_datetime(df["Date"], errors="coerce")
|
|
56
|
+
df = df.set_index("datetime").sort_index()
|
|
57
|
+
df = df.rename(
|
|
58
|
+
columns={
|
|
59
|
+
"Open": "Open",
|
|
60
|
+
"High": "High",
|
|
61
|
+
"Low": "Low",
|
|
62
|
+
"Close": "Close",
|
|
63
|
+
"Volume": "Volume",
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
for col in ["Open", "High", "Low", "Close"]:
|
|
67
|
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
68
|
+
if "Volume" not in df.columns:
|
|
69
|
+
df["Volume"] = 0.0
|
|
70
|
+
else:
|
|
71
|
+
df["Volume"] = pd.to_numeric(df["Volume"], errors="coerce").fillna(0.0)
|
|
72
|
+
df = df.dropna(subset=["Open", "High", "Low", "Close"])
|
|
73
|
+
|
|
74
|
+
start_dt = pd.to_datetime(start)
|
|
75
|
+
end_dt = pd.to_datetime(end)
|
|
76
|
+
df = df[(df.index >= start_dt) & (df.index <= end_dt)]
|
|
77
|
+
return df[["Open", "High", "Low", "Close", "Volume"]]
|
|
78
|
+
except Exception:
|
|
79
|
+
return pd.DataFrame(columns=["Open", "High", "Low", "Close", "Volume"])
|
|
80
|
+
|
|
81
|
+
def get_latest_price(self, symbol: str) -> Optional[float]:
|
|
82
|
+
try:
|
|
83
|
+
s = self._to_stooq_symbol(symbol)
|
|
84
|
+
# Current quote endpoint
|
|
85
|
+
url = f"https://stooq.com/q/l/?s={s}&f=sd2t2ohlcv&h&e=csv"
|
|
86
|
+
response = requests.get(url, timeout=10)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
df = pd.read_csv(StringIO(response.text))
|
|
89
|
+
if df.empty or "Close" not in df.columns:
|
|
90
|
+
return None
|
|
91
|
+
price = pd.to_numeric(df["Close"].iloc[0], errors="coerce")
|
|
92
|
+
if pd.isna(price):
|
|
93
|
+
return None
|
|
94
|
+
return float(price)
|
|
95
|
+
except Exception:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def get_historical_data(self, symbol: str, period: str = "1d", interval: str = "1h") -> pd.DataFrame:
|
|
99
|
+
now = datetime.now()
|
|
100
|
+
period_map = {
|
|
101
|
+
"1d": timedelta(days=1),
|
|
102
|
+
"5d": timedelta(days=5),
|
|
103
|
+
"1mo": timedelta(days=30),
|
|
104
|
+
"3mo": timedelta(days=90),
|
|
105
|
+
"6mo": timedelta(days=180),
|
|
106
|
+
"1y": timedelta(days=365),
|
|
107
|
+
}
|
|
108
|
+
delta = period_map.get(period, timedelta(days=1))
|
|
109
|
+
start = (now - delta).strftime("%Y-%m-%d")
|
|
110
|
+
end = now.strftime("%Y-%m-%d")
|
|
111
|
+
return self.get_data(symbol, start, end, interval)
|
|
112
|
+
|
|
113
|
+
def is_available(self) -> bool:
|
|
114
|
+
return self._available
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Twelve Data provider implementation for forex market data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from ...core.interfaces import IDataProvider
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TwelveDataProvider(IDataProvider):
|
|
15
|
+
"""Twelve Data provider for historical candles and latest price."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, api_key: str = ""):
|
|
18
|
+
self.api_key = api_key
|
|
19
|
+
self.base_url = "https://api.twelvedata.com"
|
|
20
|
+
self._available = bool(api_key)
|
|
21
|
+
|
|
22
|
+
def _to_twelve_symbol(self, symbol: str) -> str:
|
|
23
|
+
raw = symbol.replace("=X", "").replace("/", "").upper()
|
|
24
|
+
if len(raw) != 6:
|
|
25
|
+
raise ValueError(f"Unsupported forex symbol format: {symbol}")
|
|
26
|
+
return f"{raw[:3]}/{raw[3:]}"
|
|
27
|
+
|
|
28
|
+
def _map_interval(self, interval: str) -> str:
|
|
29
|
+
mapping = {
|
|
30
|
+
"1m": "1min",
|
|
31
|
+
"5m": "5min",
|
|
32
|
+
"15m": "15min",
|
|
33
|
+
"30m": "30min",
|
|
34
|
+
"1h": "1h",
|
|
35
|
+
"4h": "4h",
|
|
36
|
+
"1d": "1day",
|
|
37
|
+
}
|
|
38
|
+
return mapping.get(interval, "1h")
|
|
39
|
+
|
|
40
|
+
def get_data(self, symbol: str, start: str, end: str, interval: str = "1h") -> pd.DataFrame:
|
|
41
|
+
if not self._available:
|
|
42
|
+
return pd.DataFrame(columns=["Open", "High", "Low", "Close", "Volume"])
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
td_symbol = self._to_twelve_symbol(symbol)
|
|
46
|
+
params = {
|
|
47
|
+
"symbol": td_symbol,
|
|
48
|
+
"interval": self._map_interval(interval),
|
|
49
|
+
"start_date": pd.to_datetime(start).strftime("%Y-%m-%d %H:%M:%S"),
|
|
50
|
+
"end_date": pd.to_datetime(end).strftime("%Y-%m-%d %H:%M:%S"),
|
|
51
|
+
"apikey": self.api_key,
|
|
52
|
+
"outputsize": 5000,
|
|
53
|
+
}
|
|
54
|
+
response = requests.get(f"{self.base_url}/time_series", params=params, timeout=20)
|
|
55
|
+
data = response.json()
|
|
56
|
+
|
|
57
|
+
if "values" not in data:
|
|
58
|
+
return pd.DataFrame(columns=["Open", "High", "Low", "Close", "Volume"])
|
|
59
|
+
|
|
60
|
+
rows = data["values"]
|
|
61
|
+
if not rows:
|
|
62
|
+
return pd.DataFrame(columns=["Open", "High", "Low", "Close", "Volume"])
|
|
63
|
+
|
|
64
|
+
df = pd.DataFrame(rows)
|
|
65
|
+
df["datetime"] = pd.to_datetime(df["datetime"])
|
|
66
|
+
df = df.set_index("datetime").sort_index()
|
|
67
|
+
df = df.rename(
|
|
68
|
+
columns={
|
|
69
|
+
"open": "Open",
|
|
70
|
+
"high": "High",
|
|
71
|
+
"low": "Low",
|
|
72
|
+
"close": "Close",
|
|
73
|
+
"volume": "Volume",
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
for col in ["Open", "High", "Low", "Close", "Volume"]:
|
|
77
|
+
if col not in df.columns:
|
|
78
|
+
df[col] = 0.0 if col == "Volume" else pd.NA
|
|
79
|
+
df[["Open", "High", "Low", "Close", "Volume"]] = df[
|
|
80
|
+
["Open", "High", "Low", "Close", "Volume"]
|
|
81
|
+
].apply(pd.to_numeric, errors="coerce")
|
|
82
|
+
df = df.dropna(subset=["Open", "High", "Low", "Close"])
|
|
83
|
+
return df[["Open", "High", "Low", "Close", "Volume"]]
|
|
84
|
+
except Exception:
|
|
85
|
+
return pd.DataFrame(columns=["Open", "High", "Low", "Close", "Volume"])
|
|
86
|
+
|
|
87
|
+
def get_latest_price(self, symbol: str) -> Optional[float]:
|
|
88
|
+
if not self._available:
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
td_symbol = self._to_twelve_symbol(symbol)
|
|
92
|
+
params = {"symbol": td_symbol, "apikey": self.api_key}
|
|
93
|
+
response = requests.get(f"{self.base_url}/price", params=params, timeout=10)
|
|
94
|
+
data = response.json()
|
|
95
|
+
if "price" in data:
|
|
96
|
+
return float(data["price"])
|
|
97
|
+
return None
|
|
98
|
+
except Exception:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def get_historical_data(self, symbol: str, period: str = "1d", interval: str = "1h") -> pd.DataFrame:
|
|
102
|
+
now = datetime.now()
|
|
103
|
+
period_map = {
|
|
104
|
+
"1d": timedelta(days=1),
|
|
105
|
+
"5d": timedelta(days=5),
|
|
106
|
+
"1mo": timedelta(days=30),
|
|
107
|
+
"3mo": timedelta(days=90),
|
|
108
|
+
"6mo": timedelta(days=180),
|
|
109
|
+
"1y": timedelta(days=365),
|
|
110
|
+
}
|
|
111
|
+
delta = period_map.get(period, timedelta(days=1))
|
|
112
|
+
start = (now - delta).strftime("%Y-%m-%d")
|
|
113
|
+
end = now.strftime("%Y-%m-%d")
|
|
114
|
+
return self.get_data(symbol, start, end, interval)
|
|
115
|
+
|
|
116
|
+
def is_available(self) -> bool:
|
|
117
|
+
return self._available
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Yahoo Finance data provider implementation."""
|
|
2
|
+
|
|
3
|
+
import yfinance as yf
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from ...core.interfaces import IDataProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class YFinanceProvider(IDataProvider):
|
|
10
|
+
"""Yahoo Finance data provider."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._available = True
|
|
14
|
+
|
|
15
|
+
def get_data(self, symbol: str, start: str, end: str,
|
|
16
|
+
interval: str = "1h") -> pd.DataFrame:
|
|
17
|
+
"""Get historical data from Yahoo Finance."""
|
|
18
|
+
try:
|
|
19
|
+
# Convert dates to timezone-naive datetime objects
|
|
20
|
+
start_date = pd.to_datetime(start).tz_localize(None) if pd.to_datetime(start).tz is not None else pd.to_datetime(start)
|
|
21
|
+
end_date = pd.to_datetime(end).tz_localize(None) if pd.to_datetime(end).tz is not None else pd.to_datetime(end)
|
|
22
|
+
|
|
23
|
+
# Convert forex symbol to yfinance format if needed
|
|
24
|
+
if not symbol.endswith('=X'):
|
|
25
|
+
symbol = symbol.upper() + "=X"
|
|
26
|
+
|
|
27
|
+
ticker = yf.Ticker(symbol)
|
|
28
|
+
date_range_days = (end_date - start_date).days
|
|
29
|
+
|
|
30
|
+
# Try different approaches to get data
|
|
31
|
+
df = pd.DataFrame()
|
|
32
|
+
|
|
33
|
+
# First try: Get data with the requested interval
|
|
34
|
+
try:
|
|
35
|
+
if date_range_days > 365:
|
|
36
|
+
# For long ranges, use daily data and resample
|
|
37
|
+
df = ticker.history(start=start_date, end=end_date, interval="1d", auto_adjust=True, progress=False)
|
|
38
|
+
else:
|
|
39
|
+
# For shorter ranges, use the requested interval
|
|
40
|
+
df = ticker.history(start=start_date, end=end_date, interval=interval, auto_adjust=True, progress=False)
|
|
41
|
+
|
|
42
|
+
# Convert index to timezone-naive if needed
|
|
43
|
+
if not df.empty and df.index.tz is not None:
|
|
44
|
+
df.index = df.index.tz_localize(None)
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
# Second try: Get maximum available data if first attempt failed
|
|
49
|
+
if df.empty:
|
|
50
|
+
try:
|
|
51
|
+
df = ticker.history(period="max", interval="1d", auto_adjust=True, progress=False)
|
|
52
|
+
if not df.empty:
|
|
53
|
+
# Convert index to timezone-naive for comparison
|
|
54
|
+
df.index = df.index.tz_localize(None) if df.index.tz is not None else df.index
|
|
55
|
+
# Filter to requested date range
|
|
56
|
+
df = df[(df.index >= start_date) & (df.index <= end_date)]
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
# Third try: Get recent data if all else fails
|
|
61
|
+
if df.empty:
|
|
62
|
+
try:
|
|
63
|
+
df = ticker.history(period="5d", interval="1d", auto_adjust=True, progress=False)
|
|
64
|
+
# Convert index to timezone-naive if needed
|
|
65
|
+
if not df.empty and df.index.tz is not None:
|
|
66
|
+
df.index = df.index.tz_localize(None)
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
if df.empty:
|
|
71
|
+
print(f"Warning: No data available for {symbol}")
|
|
72
|
+
return pd.DataFrame(columns=['Open', 'High', 'Low', 'Close', 'Volume'])
|
|
73
|
+
|
|
74
|
+
# Resample if needed and interval is not daily
|
|
75
|
+
if not df.empty and interval != "1d" and len(df) > 1:
|
|
76
|
+
try:
|
|
77
|
+
# Convert interval to pandas frequency format
|
|
78
|
+
freq_map = {'1h': '1h', '4h': '4h', '1m': '1T'}
|
|
79
|
+
freq = freq_map.get(interval, interval)
|
|
80
|
+
df = df.resample(freq).interpolate(method='linear')
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
# Ensure proper column names
|
|
85
|
+
df = df.rename(columns={
|
|
86
|
+
'Open': 'Open',
|
|
87
|
+
'High': 'High',
|
|
88
|
+
'Low': 'Low',
|
|
89
|
+
'Close': 'Close',
|
|
90
|
+
'Volume': 'Volume'
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
# Fill missing volume data
|
|
94
|
+
if 'Volume' not in df.columns or df['Volume'].isna().all():
|
|
95
|
+
df['Volume'] = 1000000 # Default volume for forex
|
|
96
|
+
|
|
97
|
+
# Ensure all required columns exist
|
|
98
|
+
required_columns = ['Open', 'High', 'Low', 'Close', 'Volume']
|
|
99
|
+
for col in required_columns:
|
|
100
|
+
if col not in df.columns:
|
|
101
|
+
if col == 'Volume':
|
|
102
|
+
df[col] = 1000000
|
|
103
|
+
else:
|
|
104
|
+
df[col] = df['Close'] # Use Close price as fallback
|
|
105
|
+
|
|
106
|
+
return df.dropna()
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"Error getting data for {symbol}: {e}")
|
|
110
|
+
return pd.DataFrame(columns=['Open', 'High', 'Low', 'Close', 'Volume'])
|
|
111
|
+
|
|
112
|
+
def _generate_synthetic_data(self, start_date, end_date, interval):
|
|
113
|
+
"""Generate synthetic forex data for testing purposes."""
|
|
114
|
+
import numpy as np
|
|
115
|
+
|
|
116
|
+
# Create date range
|
|
117
|
+
if interval == "1h":
|
|
118
|
+
freq = '1h'
|
|
119
|
+
elif interval == "4h":
|
|
120
|
+
freq = '4h'
|
|
121
|
+
elif interval == "1m":
|
|
122
|
+
freq = '1T'
|
|
123
|
+
else:
|
|
124
|
+
freq = '1d'
|
|
125
|
+
|
|
126
|
+
date_range = pd.date_range(start=start_date, end=end_date, freq=freq)
|
|
127
|
+
|
|
128
|
+
if len(date_range) == 0:
|
|
129
|
+
# Fallback to daily if no data points
|
|
130
|
+
date_range = pd.date_range(start=start_date, end=end_date, freq='1d')
|
|
131
|
+
|
|
132
|
+
# Generate synthetic price data (starting around 1.1000 for EURUSD-like pair)
|
|
133
|
+
base_price = 1.1000
|
|
134
|
+
n_points = len(date_range)
|
|
135
|
+
|
|
136
|
+
# Generate random walk
|
|
137
|
+
returns = np.random.normal(0, 0.001, n_points) # 0.1% daily volatility
|
|
138
|
+
prices = [base_price]
|
|
139
|
+
|
|
140
|
+
for i in range(1, n_points):
|
|
141
|
+
new_price = prices[-1] * (1 + returns[i])
|
|
142
|
+
prices.append(new_price)
|
|
143
|
+
|
|
144
|
+
# Create OHLC data
|
|
145
|
+
data = []
|
|
146
|
+
for i, (date, price) in enumerate(zip(date_range, prices)):
|
|
147
|
+
# Add some intraday variation
|
|
148
|
+
high = price * (1 + abs(np.random.normal(0, 0.0005)))
|
|
149
|
+
low = price * (1 - abs(np.random.normal(0, 0.0005)))
|
|
150
|
+
open_price = price * (1 + np.random.normal(0, 0.0002))
|
|
151
|
+
close_price = price
|
|
152
|
+
|
|
153
|
+
data.append({
|
|
154
|
+
'Open': open_price,
|
|
155
|
+
'High': max(open_price, high, close_price),
|
|
156
|
+
'Low': min(open_price, low, close_price),
|
|
157
|
+
'Close': close_price,
|
|
158
|
+
'Volume': 1000000
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
df = pd.DataFrame(data, index=date_range)
|
|
162
|
+
return df
|
|
163
|
+
|
|
164
|
+
def get_latest_price(self, symbol: str) -> Optional[float]:
|
|
165
|
+
"""Get latest price from Yahoo Finance."""
|
|
166
|
+
try:
|
|
167
|
+
# Convert forex symbol to yfinance format if needed
|
|
168
|
+
if not symbol.endswith('=X'):
|
|
169
|
+
symbol = symbol.upper() + "=X"
|
|
170
|
+
|
|
171
|
+
ticker = yf.Ticker(symbol)
|
|
172
|
+
|
|
173
|
+
# Try to get price from info first
|
|
174
|
+
try:
|
|
175
|
+
info = ticker.info
|
|
176
|
+
if 'regularMarketPrice' in info and info['regularMarketPrice'] is not None:
|
|
177
|
+
return float(info['regularMarketPrice'])
|
|
178
|
+
elif 'currentPrice' in info and info['currentPrice'] is not None:
|
|
179
|
+
return float(info['currentPrice'])
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
# Fallback to recent data
|
|
184
|
+
try:
|
|
185
|
+
df = ticker.history(period="1d", interval="1m", progress=False)
|
|
186
|
+
if not df.empty and 'Close' in df.columns:
|
|
187
|
+
return float(df['Close'].iloc[-1])
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
# Second fallback: try daily data
|
|
192
|
+
try:
|
|
193
|
+
df = ticker.history(period="5d", interval="1d", progress=False)
|
|
194
|
+
if not df.empty and 'Close' in df.columns:
|
|
195
|
+
return float(df['Close'].iloc[-1])
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
print(f"Error getting latest price for {symbol}: {e}")
|
|
201
|
+
|
|
202
|
+
# Return None if all attempts fail
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
def get_historical_data(self, symbol: str, period: str = '1d', interval: str = '1h') -> pd.DataFrame:
|
|
206
|
+
"""Get historical data for a symbol with specified period and interval."""
|
|
207
|
+
try:
|
|
208
|
+
# Convert period to start/end dates
|
|
209
|
+
from datetime import datetime, timedelta
|
|
210
|
+
|
|
211
|
+
# Use timezone-naive datetime objects
|
|
212
|
+
now = datetime.now()
|
|
213
|
+
|
|
214
|
+
if period == '1d':
|
|
215
|
+
end_date = now
|
|
216
|
+
start_date = now - timedelta(days=1)
|
|
217
|
+
elif period == '5d':
|
|
218
|
+
end_date = now
|
|
219
|
+
start_date = now - timedelta(days=5)
|
|
220
|
+
elif period == '1mo':
|
|
221
|
+
end_date = now
|
|
222
|
+
start_date = now - timedelta(days=30)
|
|
223
|
+
elif period == '3mo':
|
|
224
|
+
end_date = now
|
|
225
|
+
start_date = now - timedelta(days=90)
|
|
226
|
+
elif period == '6mo':
|
|
227
|
+
end_date = now
|
|
228
|
+
start_date = now - timedelta(days=180)
|
|
229
|
+
elif period == '1y':
|
|
230
|
+
end_date = now
|
|
231
|
+
start_date = now - timedelta(days=365)
|
|
232
|
+
else:
|
|
233
|
+
# Default to 1 day
|
|
234
|
+
end_date = now
|
|
235
|
+
start_date = now - timedelta(days=1)
|
|
236
|
+
|
|
237
|
+
# Use the existing get_data method
|
|
238
|
+
return self.get_data(symbol, start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d'), interval)
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
print(f"Error getting historical data for {symbol}: {e}")
|
|
242
|
+
return pd.DataFrame(columns=['Open', 'High', 'Low', 'Close', 'Volume'])
|
|
243
|
+
|
|
244
|
+
def is_available(self) -> bool:
|
|
245
|
+
"""Check if Yahoo Finance is available."""
|
|
246
|
+
return self._available
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analytics module for ForexSmartBot v3.2.0
|
|
3
|
+
Provides portfolio analytics, risk analytics, and advanced charting features.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .portfolio_analytics import PortfolioAnalytics
|
|
7
|
+
from .risk_analytics import RiskAnalytics
|
|
8
|
+
from .chart_patterns import ChartPatternRecognizer
|
|
9
|
+
from .performance_attribution import PerformanceAttribution
|
|
10
|
+
from .market_depth import MarketDepthWidget, MarketDepthProvider
|
|
11
|
+
from .correlation_matrix import CorrelationMatrixWidget, CorrelationAnalyzer
|
|
12
|
+
from .economic_calendar import EconomicCalendarWidget, EconomicCalendarProvider
|
|
13
|
+
from .trade_journal import TradeJournalWidget, TradeJournalManager
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
'PortfolioAnalytics',
|
|
17
|
+
'RiskAnalytics',
|
|
18
|
+
'ChartPatternRecognizer',
|
|
19
|
+
'PerformanceAttribution',
|
|
20
|
+
'MarketDepthWidget',
|
|
21
|
+
'MarketDepthProvider',
|
|
22
|
+
'CorrelationMatrixWidget',
|
|
23
|
+
'CorrelationAnalyzer',
|
|
24
|
+
'EconomicCalendarWidget',
|
|
25
|
+
'EconomicCalendarProvider',
|
|
26
|
+
'TradeJournalWidget',
|
|
27
|
+
'TradeJournalManager',
|
|
28
|
+
]
|