Architecture Docker (8 services), FastAPI, TimescaleDB, Redis, Streamlit. Stratégies : scalping, intraday, swing. MLEngine + RegimeDetector (HMM). BacktestEngine + WalkForwardAnalyzer + Optuna optimizer. Routes API complètes dont /optimize async. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
720 lines
22 KiB
Markdown
720 lines
22 KiB
Markdown
# 🔌 Guide d'Intégration IG Markets - Trading AI Secure
|
|
|
|
## 📋 Table des Matières
|
|
1. [Vue d'ensemble IG Markets](#vue-densemble-ig-markets)
|
|
2. [Configuration Compte](#configuration-compte)
|
|
3. [API REST](#api-rest)
|
|
4. [Streaming Lightstreamer](#streaming-lightstreamer)
|
|
5. [Gestion des Ordres](#gestion-des-ordres)
|
|
6. [Risk Management IG](#risk-management-ig)
|
|
7. [Migration Progressive](#migration-progressive)
|
|
8. [Implémentation](#implémentation)
|
|
|
|
---
|
|
|
|
## 🎯 Vue d'ensemble IG Markets
|
|
|
|
### Pourquoi IG Markets ?
|
|
|
|
- ✅ **API complète** : REST + Streaming temps réel
|
|
- ✅ **Compte démo gratuit** : Test sans risque
|
|
- ✅ **Large gamme d'instruments** : Forex, Indices, Actions, Crypto
|
|
- ✅ **CFD et DMA** : Flexibilité trading
|
|
- ✅ **Guaranteed stops** : Protection slippage
|
|
- ✅ **Documentation** : API bien documentée
|
|
|
|
### Architecture IG
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────┐
|
|
│ IG MARKETS API │
|
|
├──────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ REST API STREAMING API │
|
|
│ ├─ Authentication ├─ Lightstreamer │
|
|
│ ├─ Account Info ├─ Prix temps réel │
|
|
│ ├─ Market Data ├─ Positions updates │
|
|
│ ├─ Orders (CRUD) └─ Account updates │
|
|
│ ├─ Positions │
|
|
│ └─ Historical Data │
|
|
│ │
|
|
└──────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 🔐 Configuration Compte
|
|
|
|
### Étape 1 : Créer Compte Démo
|
|
|
|
1. **S'inscrire** : https://www.ig.com/uk/demo-account
|
|
2. **Activer compte démo** : Capital virtuel £10,000
|
|
3. **Accéder API** : Dashboard → API → Generate API Key
|
|
|
|
### Étape 2 : Obtenir Credentials
|
|
|
|
```yaml
|
|
# config/ig_config.yaml (EXEMPLE - NE PAS COMMITER)
|
|
ig_credentials:
|
|
# Démo
|
|
demo:
|
|
api_key: "YOUR_DEMO_API_KEY"
|
|
username: "YOUR_DEMO_USERNAME"
|
|
password: "YOUR_DEMO_PASSWORD"
|
|
account_id: "YOUR_DEMO_ACCOUNT_ID"
|
|
api_url: "https://demo-api.ig.com/gateway/deal"
|
|
|
|
# Live (À ACTIVER APRÈS VALIDATION)
|
|
live:
|
|
api_key: "YOUR_LIVE_API_KEY"
|
|
username: "YOUR_LIVE_USERNAME"
|
|
password: "YOUR_LIVE_PASSWORD"
|
|
account_id: "YOUR_LIVE_ACCOUNT_ID"
|
|
api_url: "https://api.ig.com/gateway/deal"
|
|
|
|
# Lightstreamer
|
|
lightstreamer:
|
|
demo_url: "https://demo-apd.marketdatasys.com"
|
|
live_url: "https://apd.marketdatasys.com"
|
|
```
|
|
|
|
### Étape 3 : Sécuriser Credentials
|
|
|
|
```python
|
|
# src/core/config_manager.py
|
|
|
|
import os
|
|
from cryptography.fernet import Fernet
|
|
import yaml
|
|
|
|
class SecureConfigManager:
|
|
"""
|
|
Gestion sécurisée des credentials IG
|
|
"""
|
|
|
|
def __init__(self):
|
|
# Générer clé encryption (à stocker en variable d'environnement)
|
|
self.key = os.getenv('ENCRYPTION_KEY') or Fernet.generate_key()
|
|
self.cipher = Fernet(self.key)
|
|
|
|
def load_ig_credentials(self, environment='demo'):
|
|
"""
|
|
Charge credentials IG de manière sécurisée
|
|
"""
|
|
with open('config/ig_config.yaml', 'r') as f:
|
|
config = yaml.safe_load(f)
|
|
|
|
credentials = config['ig_credentials'][environment]
|
|
|
|
# Décrypter password si encrypté
|
|
if credentials['password'].startswith('encrypted:'):
|
|
encrypted_password = credentials['password'].replace('encrypted:', '')
|
|
credentials['password'] = self.cipher.decrypt(
|
|
encrypted_password.encode()
|
|
).decode()
|
|
|
|
return credentials
|
|
|
|
def encrypt_password(self, password: str) -> str:
|
|
"""Encrypte password"""
|
|
return 'encrypted:' + self.cipher.encrypt(password.encode()).decode()
|
|
```
|
|
|
|
---
|
|
|
|
## 🌐 API REST
|
|
|
|
### Authentification
|
|
|
|
```python
|
|
# src/data/ig_connector.py
|
|
|
|
import requests
|
|
from typing import Dict, Optional
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class IGRestAPI:
|
|
"""
|
|
Connecteur IG REST API
|
|
"""
|
|
|
|
def __init__(self, credentials: Dict):
|
|
self.api_key = credentials['api_key']
|
|
self.username = credentials['username']
|
|
self.password = credentials['password']
|
|
self.account_id = credentials['account_id']
|
|
self.base_url = credentials['api_url']
|
|
|
|
self.session = requests.Session()
|
|
self.access_token = None
|
|
self.cst_token = None
|
|
self.security_token = None
|
|
|
|
def authenticate(self) -> bool:
|
|
"""
|
|
Authentification IG API
|
|
|
|
Returns:
|
|
True si succès, False sinon
|
|
"""
|
|
url = f"{self.base_url}/session"
|
|
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-IG-API-KEY': self.api_key,
|
|
'Version': '2'
|
|
}
|
|
|
|
payload = {
|
|
'identifier': self.username,
|
|
'password': self.password
|
|
}
|
|
|
|
try:
|
|
response = self.session.post(url, json=payload, headers=headers)
|
|
response.raise_for_status()
|
|
|
|
# Extraire tokens
|
|
self.cst_token = response.headers.get('CST')
|
|
self.security_token = response.headers.get('X-SECURITY-TOKEN')
|
|
|
|
# Sélectionner compte
|
|
self._select_account(self.account_id)
|
|
|
|
logger.info("IG API authentication successful")
|
|
return True
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"IG API authentication failed: {e}")
|
|
return False
|
|
|
|
def _select_account(self, account_id: str):
|
|
"""Sélectionne compte de trading"""
|
|
url = f"{self.base_url}/session"
|
|
|
|
headers = self._get_headers()
|
|
headers['Version'] = '1'
|
|
headers['_method'] = 'PUT'
|
|
|
|
payload = {
|
|
'accountId': account_id,
|
|
'defaultAccount': True
|
|
}
|
|
|
|
response = self.session.put(url, json=payload, headers=headers)
|
|
response.raise_for_status()
|
|
|
|
def _get_headers(self) -> Dict:
|
|
"""Headers pour requêtes authentifiées"""
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-IG-API-KEY': self.api_key,
|
|
'CST': self.cst_token,
|
|
'X-SECURITY-TOKEN': self.security_token
|
|
}
|
|
|
|
def get_account_info(self) -> Dict:
|
|
"""Récupère informations compte"""
|
|
url = f"{self.base_url}/accounts"
|
|
|
|
response = self.session.get(url, headers=self._get_headers())
|
|
response.raise_for_status()
|
|
|
|
return response.json()
|
|
|
|
def get_market_details(self, epic: str) -> Dict:
|
|
"""
|
|
Récupère détails d'un marché
|
|
|
|
Args:
|
|
epic: Identifiant marché IG (ex: 'CS.D.EURUSD.MINI.IP')
|
|
"""
|
|
url = f"{self.base_url}/markets/{epic}"
|
|
|
|
headers = self._get_headers()
|
|
headers['Version'] = '3'
|
|
|
|
response = self.session.get(url, headers=headers)
|
|
response.raise_for_status()
|
|
|
|
return response.json()
|
|
|
|
def get_positions(self) -> Dict:
|
|
"""Récupère positions ouvertes"""
|
|
url = f"{self.base_url}/positions"
|
|
|
|
headers = self._get_headers()
|
|
headers['Version'] = '2'
|
|
|
|
response = self.session.get(url, headers=headers)
|
|
response.raise_for_status()
|
|
|
|
return response.json()
|
|
|
|
def place_order(
|
|
self,
|
|
epic: str,
|
|
direction: str,
|
|
size: float,
|
|
stop_loss: Optional[float] = None,
|
|
take_profit: Optional[float] = None,
|
|
guaranteed_stop: bool = True
|
|
) -> Dict:
|
|
"""
|
|
Place ordre de trading
|
|
|
|
Args:
|
|
epic: Identifiant marché
|
|
direction: 'BUY' ou 'SELL'
|
|
size: Taille position
|
|
stop_loss: Niveau stop-loss
|
|
take_profit: Niveau take-profit
|
|
guaranteed_stop: Utiliser guaranteed stop (recommandé)
|
|
"""
|
|
url = f"{self.base_url}/positions/otc"
|
|
|
|
headers = self._get_headers()
|
|
headers['Version'] = '2'
|
|
|
|
payload = {
|
|
'epic': epic,
|
|
'direction': direction,
|
|
'size': size,
|
|
'orderType': 'MARKET',
|
|
'timeInForce': 'FILL_OR_KILL',
|
|
'guaranteedStop': guaranteed_stop,
|
|
'currencyCode': 'GBP'
|
|
}
|
|
|
|
# Ajouter stop-loss
|
|
if stop_loss:
|
|
payload['stopLevel'] = stop_loss
|
|
payload['stopDistance'] = None # Calculé automatiquement
|
|
|
|
# Ajouter take-profit
|
|
if take_profit:
|
|
payload['limitLevel'] = take_profit
|
|
payload['limitDistance'] = None
|
|
|
|
try:
|
|
response = self.session.post(url, json=payload, headers=headers)
|
|
response.raise_for_status()
|
|
|
|
result = response.json()
|
|
deal_reference = result.get('dealReference')
|
|
|
|
# Confirmer ordre
|
|
return self._confirm_order(deal_reference)
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"Order placement failed: {e}")
|
|
raise
|
|
|
|
def _confirm_order(self, deal_reference: str) -> Dict:
|
|
"""Confirme exécution ordre"""
|
|
url = f"{self.base_url}/confirms/{deal_reference}"
|
|
|
|
headers = self._get_headers()
|
|
headers['Version'] = '1'
|
|
|
|
response = self.session.get(url, headers=headers)
|
|
response.raise_for_status()
|
|
|
|
return response.json()
|
|
|
|
def close_position(self, deal_id: str) -> Dict:
|
|
"""Ferme position"""
|
|
# Récupérer détails position
|
|
position = self._get_position_details(deal_id)
|
|
|
|
url = f"{self.base_url}/positions/otc"
|
|
|
|
headers = self._get_headers()
|
|
headers['Version'] = '1'
|
|
headers['_method'] = 'DELETE'
|
|
|
|
payload = {
|
|
'dealId': deal_id,
|
|
'direction': 'SELL' if position['direction'] == 'BUY' else 'BUY',
|
|
'size': position['size'],
|
|
'orderType': 'MARKET'
|
|
}
|
|
|
|
response = self.session.delete(url, json=payload, headers=headers)
|
|
response.raise_for_status()
|
|
|
|
return response.json()
|
|
|
|
def get_historical_prices(
|
|
self,
|
|
epic: str,
|
|
resolution: str = 'MINUTE',
|
|
num_points: int = 100
|
|
) -> Dict:
|
|
"""
|
|
Récupère prix historiques
|
|
|
|
Args:
|
|
epic: Identifiant marché
|
|
resolution: 'SECOND', 'MINUTE', 'MINUTE_5', 'HOUR', 'DAY'
|
|
num_points: Nombre de points (max 1000)
|
|
"""
|
|
url = f"{self.base_url}/prices/{epic}"
|
|
|
|
headers = self._get_headers()
|
|
headers['Version'] = '3'
|
|
|
|
params = {
|
|
'resolution': resolution,
|
|
'max': min(num_points, 1000)
|
|
}
|
|
|
|
response = self.session.get(url, headers=headers, params=params)
|
|
response.raise_for_status()
|
|
|
|
return response.json()
|
|
```
|
|
|
|
---
|
|
|
|
## 📡 Streaming Lightstreamer
|
|
|
|
### Configuration
|
|
|
|
```python
|
|
# src/data/ig_streaming.py
|
|
|
|
from lightstreamer.client import LightstreamerClient, Subscription
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class IGStreamingAPI:
|
|
"""
|
|
Connecteur IG Streaming (Lightstreamer)
|
|
|
|
Permet de recevoir:
|
|
- Prix en temps réel
|
|
- Updates positions
|
|
- Updates compte
|
|
"""
|
|
|
|
def __init__(self, credentials: Dict, cst_token: str, security_token: str):
|
|
self.lightstreamer_url = credentials.get('lightstreamer_url')
|
|
self.cst_token = cst_token
|
|
self.security_token = security_token
|
|
self.account_id = credentials['account_id']
|
|
|
|
self.client = None
|
|
self.subscriptions = {}
|
|
|
|
def connect(self):
|
|
"""Connexion Lightstreamer"""
|
|
self.client = LightstreamerClient(self.lightstreamer_url, "DEFAULT")
|
|
|
|
# Configuration connexion
|
|
self.client.connectionDetails.setUser(self.account_id)
|
|
self.client.connectionDetails.setPassword(f"CST-{self.cst_token}|XST-{self.security_token}")
|
|
|
|
# Listeners
|
|
self.client.addListener(ConnectionListener())
|
|
|
|
# Connecter
|
|
self.client.connect()
|
|
|
|
logger.info("Lightstreamer connection established")
|
|
|
|
def subscribe_market(self, epic: str, callback):
|
|
"""
|
|
Subscribe à prix temps réel d'un marché
|
|
|
|
Args:
|
|
epic: Identifiant marché
|
|
callback: Fonction appelée à chaque update
|
|
"""
|
|
# Items à subscribe
|
|
items = [f"MARKET:{epic}"]
|
|
|
|
# Fields à recevoir
|
|
fields = [
|
|
"BID", "OFFER", "HIGH", "LOW",
|
|
"MID_OPEN", "CHANGE", "CHANGE_PCT",
|
|
"UPDATE_TIME", "MARKET_STATE"
|
|
]
|
|
|
|
# Créer subscription
|
|
subscription = Subscription(
|
|
mode="MERGE",
|
|
items=items,
|
|
fields=fields
|
|
)
|
|
|
|
# Ajouter listener
|
|
subscription.addListener(MarketDataListener(callback))
|
|
|
|
# Subscribe
|
|
self.client.subscribe(subscription)
|
|
self.subscriptions[epic] = subscription
|
|
|
|
logger.info(f"Subscribed to market: {epic}")
|
|
|
|
def subscribe_account(self, callback):
|
|
"""Subscribe à updates compte"""
|
|
items = [f"ACCOUNT:{self.account_id}"]
|
|
|
|
fields = [
|
|
"AVAILABLE_CASH", "PNL", "DEPOSIT",
|
|
"USED_MARGIN", "AVAILABLE_TO_DEAL", "EQUITY"
|
|
]
|
|
|
|
subscription = Subscription(
|
|
mode="MERGE",
|
|
items=items,
|
|
fields=fields
|
|
)
|
|
|
|
subscription.addListener(AccountListener(callback))
|
|
|
|
self.client.subscribe(subscription)
|
|
self.subscriptions['account'] = subscription
|
|
|
|
def unsubscribe(self, key: str):
|
|
"""Unsubscribe d'un stream"""
|
|
if key in self.subscriptions:
|
|
self.client.unsubscribe(self.subscriptions[key])
|
|
del self.subscriptions[key]
|
|
|
|
def disconnect(self):
|
|
"""Déconnexion Lightstreamer"""
|
|
if self.client:
|
|
self.client.disconnect()
|
|
logger.info("Lightstreamer disconnected")
|
|
|
|
class MarketDataListener:
|
|
"""Listener pour données marché"""
|
|
|
|
def __init__(self, callback):
|
|
self.callback = callback
|
|
|
|
def onItemUpdate(self, update):
|
|
"""Appelé à chaque update prix"""
|
|
data = {
|
|
'bid': update.getValue('BID'),
|
|
'offer': update.getValue('OFFER'),
|
|
'high': update.getValue('HIGH'),
|
|
'low': update.getValue('LOW'),
|
|
'change': update.getValue('CHANGE'),
|
|
'change_pct': update.getValue('CHANGE_PCT'),
|
|
'update_time': update.getValue('UPDATE_TIME'),
|
|
'market_state': update.getValue('MARKET_STATE')
|
|
}
|
|
|
|
self.callback(data)
|
|
|
|
class AccountListener:
|
|
"""Listener pour updates compte"""
|
|
|
|
def __init__(self, callback):
|
|
self.callback = callback
|
|
|
|
def onItemUpdate(self, update):
|
|
"""Appelé à chaque update compte"""
|
|
data = {
|
|
'available_cash': update.getValue('AVAILABLE_CASH'),
|
|
'pnl': update.getValue('PNL'),
|
|
'deposit': update.getValue('DEPOSIT'),
|
|
'used_margin': update.getValue('USED_MARGIN'),
|
|
'equity': update.getValue('EQUITY')
|
|
}
|
|
|
|
self.callback(data)
|
|
|
|
class ConnectionListener:
|
|
"""Listener pour état connexion"""
|
|
|
|
def onStatusChange(self, status):
|
|
logger.info(f"Lightstreamer status: {status}")
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Gestion des Ordres
|
|
|
|
### Wrapper Unifié
|
|
|
|
```python
|
|
# src/core/order_manager.py
|
|
|
|
class IGOrderManager:
|
|
"""
|
|
Gestionnaire d'ordres IG avec validation
|
|
"""
|
|
|
|
def __init__(self, ig_api: IGRestAPI, risk_manager: RiskManager):
|
|
self.ig_api = ig_api
|
|
self.risk_manager = risk_manager
|
|
|
|
def execute_signal(self, signal: Signal) -> Optional[str]:
|
|
"""
|
|
Exécute signal de trading
|
|
|
|
Returns:
|
|
Deal ID si succès, None sinon
|
|
"""
|
|
# 1. Valider avec risk manager
|
|
is_valid, error = self.risk_manager.validate_trade(
|
|
symbol=signal.symbol,
|
|
quantity=signal.quantity,
|
|
price=signal.entry_price,
|
|
stop_loss=signal.stop_loss,
|
|
take_profit=signal.take_profit,
|
|
strategy=signal.strategy
|
|
)
|
|
|
|
if not is_valid:
|
|
logger.warning(f"Trade rejected: {error}")
|
|
return None
|
|
|
|
# 2. Convertir symbol en EPIC IG
|
|
epic = self._symbol_to_epic(signal.symbol)
|
|
|
|
# 3. Placer ordre
|
|
try:
|
|
result = self.ig_api.place_order(
|
|
epic=epic,
|
|
direction='BUY' if signal.direction == 'LONG' else 'SELL',
|
|
size=signal.quantity,
|
|
stop_loss=signal.stop_loss,
|
|
take_profit=signal.take_profit,
|
|
guaranteed_stop=True # Toujours utiliser guaranteed stop
|
|
)
|
|
|
|
deal_id = result.get('dealId')
|
|
|
|
# 4. Enregistrer position dans risk manager
|
|
if deal_id:
|
|
self.risk_manager.add_position(Position(
|
|
symbol=signal.symbol,
|
|
quantity=signal.quantity,
|
|
entry_price=signal.entry_price,
|
|
current_price=signal.entry_price,
|
|
stop_loss=signal.stop_loss,
|
|
take_profit=signal.take_profit,
|
|
strategy=signal.strategy,
|
|
entry_time=datetime.now(),
|
|
unrealized_pnl=0.0,
|
|
risk_amount=abs(signal.entry_price - signal.stop_loss) * signal.quantity
|
|
))
|
|
|
|
return deal_id
|
|
|
|
except Exception as e:
|
|
logger.error(f"Order execution failed: {e}")
|
|
return None
|
|
|
|
def _symbol_to_epic(self, symbol: str) -> str:
|
|
"""
|
|
Convertit symbol standard en EPIC IG
|
|
|
|
Exemples:
|
|
- EURUSD → CS.D.EURUSD.MINI.IP
|
|
- GBPUSD → CS.D.GBPUSD.MINI.IP
|
|
- US500 → IX.D.SPTRD.IFE.IP
|
|
"""
|
|
# Mapping symbol → EPIC
|
|
EPIC_MAP = {
|
|
'EURUSD': 'CS.D.EURUSD.MINI.IP',
|
|
'GBPUSD': 'CS.D.GBPUSD.MINI.IP',
|
|
'USDJPY': 'CS.D.USDJPY.MINI.IP',
|
|
'US500': 'IX.D.SPTRD.IFE.IP',
|
|
'US30': 'IX.D.DOW.IFE.IP',
|
|
'GER40': 'IX.D.DAX.IFE.IP',
|
|
}
|
|
|
|
return EPIC_MAP.get(symbol, symbol)
|
|
```
|
|
|
|
---
|
|
|
|
## 🛡️ Risk Management IG
|
|
|
|
### Spécificités IG
|
|
|
|
```python
|
|
class IGRiskCalculator:
|
|
"""
|
|
Calculs risk spécifiques IG
|
|
"""
|
|
|
|
@staticmethod
|
|
def calculate_margin_required(
|
|
epic: str,
|
|
size: float,
|
|
market_details: Dict
|
|
) -> float:
|
|
"""
|
|
Calcule margin requis pour position
|
|
|
|
IG utilise margin factor variable selon instrument
|
|
"""
|
|
margin_factor = market_details['dealingRules']['minDealSize']['value']
|
|
current_price = market_details['snapshot']['offer']
|
|
|
|
margin_required = size * current_price * margin_factor
|
|
|
|
return margin_required
|
|
|
|
@staticmethod
|
|
def calculate_guaranteed_stop_premium(
|
|
stop_distance: float,
|
|
market_details: Dict
|
|
) -> float:
|
|
"""
|
|
Calcule coût du guaranteed stop
|
|
|
|
IG facture premium pour guaranteed stops
|
|
"""
|
|
premium_factor = market_details['dealingRules']['marketOrderPreference']
|
|
|
|
# Premium généralement 0.3-0.5% du stop distance
|
|
premium = stop_distance * 0.003 # 0.3%
|
|
|
|
return premium
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 Migration Progressive
|
|
|
|
### Plan de Migration
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────┐
|
|
│ MIGRATION VERS IG MARKETS │
|
|
├──────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ PHASE 1: Développement (Semaines 1-8) │
|
|
│ └─ Sources gratuites (Yahoo, Alpha Vantage) │
|
|
│ │
|
|
│ PHASE 2: Tests IG Démo (Semaines 9-10) │
|
|
│ ├─ Connexion API IG démo │
|
|
│ ├─ Paper trading 30 jours │
|
|
│ └─ Validation performance │
|
|
│ │
|
|
│ PHASE 3: Live Progressif (Semaine 11+) │
|
|
│ ├─ Capital initial minimal (£500) │
|
|
│ ├─ 1 stratégie uniquement │
|
|
│ ├─ Monitoring 24/7 │
|
|
│ └─ Scale progressif si succès │
|
|
│ │
|
|
└──────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
**Documentation IG Markets complète !**
|