mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-27 18:57:47 +00:00
Keep main focused on MT5 platform
This commit is contained in:
+21
-44
@@ -1,52 +1,29 @@
|
||||
# 📋 QuantumBotX Development Roadmap
|
||||
# QuantumBotX Roadmap
|
||||
|
||||
**👀 Stay Tuned for Exciting Updates!**
|
||||
QuantumBotX `main` is currently maintained as a Windows-first MetaTrader 5
|
||||
trading platform. Cross-platform broker work is intentionally developed outside
|
||||
`main` until it is mature enough to merge cleanly.
|
||||
|
||||
## 🎯 **What's Coming Next**
|
||||
## Current Focus
|
||||
|
||||
### **Q4 2025: Intelligence Enhancement**
|
||||
- **Advanced AI Features**: Enhanced strategy analysis with machine learning
|
||||
- **Real-time Notifications**: Telegram integration for trade alerts
|
||||
- **Portfolio Analytics**: Advanced performance dashboards
|
||||
- **Enterprise Features**: Multi-account management and compliance logging
|
||||
- Keep MT5 demo/live workflow stable on Windows.
|
||||
- Keep strategy registration, backtesting, and dashboard modules installable on
|
||||
modern Python.
|
||||
- Improve public-safe tests and documentation.
|
||||
- Keep broker/account-specific diagnostics out of the public repository.
|
||||
|
||||
### **Exciting New Project** 🚀
|
||||
We're developing **QuantumBotX API** - a revolutionary cloud-based trading platform that will give users unprecedented freedom:
|
||||
## Near-Term Work
|
||||
|
||||
> **"Trade anywhere, anytime, with any broker - no local installations required!"**
|
||||
- Harden setup for Python 3.12.
|
||||
- Improve MT5 connection diagnostics and clearer user-facing error messages.
|
||||
- Expand regression checks for strategy imports and backtesting.
|
||||
- Review packaging scripts for Windows installer reliability.
|
||||
|
||||
**QuantumBotX API will feature:**
|
||||
- 🌐 **Cloud-native architecture** - Run on any device, anywhere
|
||||
- 🔄 **Direct broker integration** - No intermediaries, pure API trading
|
||||
- 🌏 **Cross-broker support** - IC Markets, Pepperstone, and beyond
|
||||
- ⚡ **Real-time execution** - Ultra-low latency trade processing
|
||||
- 🎓 **Advanced education** - Built-in learning with community support
|
||||
## Deferred Work
|
||||
|
||||
### **Timeline**
|
||||
- **Q4 2025**: Closed beta testing with select users
|
||||
- **Q1 2026**: Public beta launch with premium support
|
||||
- **Q2 2026**: Full global launch with subscription tiers
|
||||
- Cross-platform broker backends on dedicated development branches.
|
||||
- Broker-neutral order and market-data interfaces outside `main`.
|
||||
- Cloud/API trading platform concepts outside `main`.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 **Community & Support**
|
||||
|
||||
**We're building more than software - we're building a trading community!**
|
||||
|
||||
- **Discord Community**: Join our growing trader community
|
||||
- **Educational Content**: Free trading courses and tutorials
|
||||
- **Open Source**: Contribute to the project and shape its future
|
||||
- **Mentorship Program**: One-on-one guidance for serious traders
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ **Our Mission**
|
||||
|
||||
**Empowering traders worldwide with safe, educational, and profitable algorithmic trading solutions.**
|
||||
|
||||
*From local learning platform → Global trading ecosystem!* 🚀💫
|
||||
|
||||
---
|
||||
|
||||
**Roadmap Updated: September 2025**
|
||||
</content>
|
||||
Those items should stay on dedicated development branches until the MT5 platform
|
||||
on `main` remains clean and stable.
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
# core/brokers/base_broker.py
|
||||
"""
|
||||
Universal Broker Interface for Multi-Platform Trading
|
||||
Supports MT5, Binance, and other brokers through unified API
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Optional, Union
|
||||
from enum import Enum
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
|
||||
class OrderType(Enum):
|
||||
MARKET_BUY = "market_buy"
|
||||
MARKET_SELL = "market_sell"
|
||||
LIMIT_BUY = "limit_buy"
|
||||
LIMIT_SELL = "limit_sell"
|
||||
STOP_LOSS = "stop_loss"
|
||||
TAKE_PROFIT = "take_profit"
|
||||
|
||||
class OrderStatus(Enum):
|
||||
PENDING = "pending"
|
||||
FILLED = "filled"
|
||||
CANCELLED = "cancelled"
|
||||
REJECTED = "rejected"
|
||||
|
||||
class Timeframe(Enum):
|
||||
M1 = "1m"
|
||||
M5 = "5m"
|
||||
M15 = "15m"
|
||||
M30 = "30m"
|
||||
H1 = "1h"
|
||||
H4 = "4h"
|
||||
D1 = "1d"
|
||||
|
||||
class Position:
|
||||
def __init__(self, symbol: str, side: str, size: float, entry_price: float,
|
||||
current_price: float, unrealized_pnl: float, realized_pnl: float = 0):
|
||||
self.symbol = symbol
|
||||
self.side = side # 'long' or 'short'
|
||||
self.size = size
|
||||
self.entry_price = entry_price
|
||||
self.current_price = current_price
|
||||
self.unrealized_pnl = unrealized_pnl
|
||||
self.realized_pnl = realized_pnl
|
||||
self.timestamp = datetime.now()
|
||||
|
||||
class Order:
|
||||
def __init__(self, order_id: str, symbol: str, order_type: OrderType,
|
||||
side: str, size: float, price: Optional[float] = None):
|
||||
self.order_id = order_id
|
||||
self.symbol = symbol
|
||||
self.order_type = order_type
|
||||
self.side = side
|
||||
self.size = size
|
||||
self.price = price
|
||||
self.status = OrderStatus.PENDING
|
||||
self.filled_size = 0.0
|
||||
self.avg_fill_price = 0.0
|
||||
self.timestamp = datetime.now()
|
||||
|
||||
class AccountInfo:
|
||||
def __init__(self, balance: float, equity: float, margin: float,
|
||||
free_margin: float, margin_level: float, currency: str = "USD"):
|
||||
self.balance = balance
|
||||
self.equity = equity
|
||||
self.margin = margin
|
||||
self.free_margin = free_margin
|
||||
self.margin_level = margin_level
|
||||
self.currency = currency
|
||||
self.timestamp = datetime.now()
|
||||
|
||||
class BaseBroker(ABC):
|
||||
"""
|
||||
Abstract base class for all broker implementations.
|
||||
Provides unified interface for MT5, Binance, and other brokers.
|
||||
"""
|
||||
|
||||
def __init__(self, broker_name: str):
|
||||
self.broker_name = broker_name
|
||||
self.is_connected = False
|
||||
self.supported_symbols = []
|
||||
|
||||
@abstractmethod
|
||||
def connect(self, credentials: Dict) -> bool:
|
||||
"""Connect to broker with credentials"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def disconnect(self) -> bool:
|
||||
"""Disconnect from broker"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_symbols(self) -> List[str]:
|
||||
"""Get list of available trading symbols"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_market_data(self, symbol: str, timeframe: Timeframe,
|
||||
count: int = 500) -> pd.DataFrame:
|
||||
"""
|
||||
Get OHLCV market data
|
||||
Returns: DataFrame with columns [time, open, high, low, close, volume]
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_price(self, symbol: str) -> Dict[str, float]:
|
||||
"""
|
||||
Get current bid/ask prices
|
||||
Returns: {"bid": price, "ask": price}
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def place_order(self, symbol: str, order_type: OrderType, side: str,
|
||||
size: float, price: Optional[float] = None,
|
||||
stop_loss: Optional[float] = None,
|
||||
take_profit: Optional[float] = None) -> Order:
|
||||
"""Place a trading order"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cancel_order(self, order_id: str) -> bool:
|
||||
"""Cancel an existing order"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_positions(self) -> List[Position]:
|
||||
"""Get all open positions"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_orders(self) -> List[Order]:
|
||||
"""Get all pending orders"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_account_info(self) -> AccountInfo:
|
||||
"""Get account information"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_trade_history(self, days: int = 30) -> List[Dict]:
|
||||
"""Get trade history"""
|
||||
pass
|
||||
|
||||
# Utility methods (implemented in base class)
|
||||
def normalize_symbol(self, symbol: str) -> str:
|
||||
"""Normalize symbol format for the broker"""
|
||||
return symbol.upper().replace("/", "").replace("-", "")
|
||||
|
||||
def calculate_position_size(self, account_balance: float, risk_percent: float,
|
||||
entry_price: float, stop_loss: float) -> float:
|
||||
"""Calculate position size based on risk management"""
|
||||
risk_amount = account_balance * (risk_percent / 100)
|
||||
price_difference = abs(entry_price - stop_loss)
|
||||
|
||||
if price_difference == 0:
|
||||
return 0
|
||||
|
||||
position_size = risk_amount / price_difference
|
||||
return position_size
|
||||
|
||||
def validate_symbol(self, symbol: str) -> bool:
|
||||
"""Check if symbol is supported by broker"""
|
||||
return symbol in self.supported_symbols
|
||||
|
||||
def is_market_open(self) -> bool:
|
||||
"""Check if market is currently open (override for specific markets)"""
|
||||
return True # Crypto markets are always open
|
||||
@@ -1,359 +0,0 @@
|
||||
# core/brokers/binance_broker.py
|
||||
"""
|
||||
Binance Exchange Integration for QuantumBotX
|
||||
Implements crypto trading through Binance API
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
|
||||
from .base_broker import (
|
||||
BaseBroker, OrderType, OrderStatus, Timeframe,
|
||||
Position, Order, AccountInfo
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BinanceBroker(BaseBroker):
|
||||
"""
|
||||
Binance exchange implementation of the universal broker interface.
|
||||
Supports spot and futures trading.
|
||||
"""
|
||||
|
||||
def __init__(self, testnet: bool = True):
|
||||
super().__init__("Binance")
|
||||
self.testnet = testnet
|
||||
self.client = None
|
||||
self.base_url = "https://testnet.binance.vision" if testnet else "https://api.binance.com"
|
||||
|
||||
# Timeframe mapping
|
||||
self.timeframe_map = {
|
||||
Timeframe.M1: "1m",
|
||||
Timeframe.M5: "5m",
|
||||
Timeframe.M15: "15m",
|
||||
Timeframe.M30: "30m",
|
||||
Timeframe.H1: "1h",
|
||||
Timeframe.H4: "4h",
|
||||
Timeframe.D1: "1d"
|
||||
}
|
||||
|
||||
def connect(self, credentials: Dict) -> bool:
|
||||
"""
|
||||
Connect to Binance with API credentials
|
||||
credentials: {"api_key": "...", "secret_key": "..."}
|
||||
"""
|
||||
try:
|
||||
# Import here to avoid dependency issues if not installed
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
|
||||
api_key = credentials.get("api_key")
|
||||
secret_key = credentials.get("secret_key")
|
||||
|
||||
if not api_key or not secret_key:
|
||||
logger.error("Binance API key and secret key are required")
|
||||
return False
|
||||
|
||||
# Initialize Binance client
|
||||
self.client = Client(
|
||||
api_key=api_key,
|
||||
api_secret=secret_key,
|
||||
testnet=self.testnet
|
||||
)
|
||||
|
||||
# Test connection
|
||||
account_info = self.client.get_account()
|
||||
self.is_connected = True
|
||||
|
||||
# Get supported symbols
|
||||
exchange_info = self.client.get_exchange_info()
|
||||
self.supported_symbols = [s['symbol'] for s in exchange_info['symbols']
|
||||
if s['status'] == 'TRADING']
|
||||
|
||||
logger.info(f"Connected to Binance {'Testnet' if self.testnet else 'Mainnet'}")
|
||||
logger.info(f"Account status: {account_info.get('accountType', 'Unknown')}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Binance: {e}")
|
||||
self.is_connected = False
|
||||
return False
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
"""Disconnect from Binance"""
|
||||
self.client = None
|
||||
self.is_connected = False
|
||||
logger.info("Disconnected from Binance")
|
||||
return True
|
||||
|
||||
def get_symbols(self) -> List[str]:
|
||||
"""Get list of available trading symbols"""
|
||||
if not self.is_connected:
|
||||
return []
|
||||
return self.supported_symbols
|
||||
|
||||
def get_market_data(self, symbol: str, timeframe: Timeframe, count: int = 500) -> pd.DataFrame:
|
||||
"""
|
||||
Get OHLCV market data from Binance
|
||||
"""
|
||||
if not self.is_connected:
|
||||
raise Exception("Not connected to Binance")
|
||||
|
||||
try:
|
||||
# Convert timeframe
|
||||
interval = self.timeframe_map[timeframe]
|
||||
|
||||
# Get klines (candlestick data)
|
||||
klines = self.client.get_klines(
|
||||
symbol=symbol,
|
||||
interval=interval,
|
||||
limit=count
|
||||
)
|
||||
|
||||
# Convert to DataFrame
|
||||
df = pd.DataFrame(klines, columns=[
|
||||
'timestamp', 'open', 'high', 'low', 'close', 'volume',
|
||||
'close_time', 'quote_asset_volume', 'number_of_trades',
|
||||
'taker_buy_base_asset_volume', 'taker_buy_quote_asset_volume', 'ignore'
|
||||
])
|
||||
|
||||
# Clean and format data
|
||||
df['time'] = pd.to_datetime(df['timestamp'], unit='ms')
|
||||
df['open'] = pd.to_numeric(df['open'])
|
||||
df['high'] = pd.to_numeric(df['high'])
|
||||
df['low'] = pd.to_numeric(df['low'])
|
||||
df['close'] = pd.to_numeric(df['close'])
|
||||
df['volume'] = pd.to_numeric(df['volume'])
|
||||
|
||||
# Return standardized format
|
||||
return df[['time', 'open', 'high', 'low', 'close', 'volume']].copy()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get market data for {symbol}: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_current_price(self, symbol: str) -> Dict[str, float]:
|
||||
"""Get current bid/ask prices"""
|
||||
if not self.is_connected:
|
||||
raise Exception("Not connected to Binance")
|
||||
|
||||
try:
|
||||
ticker = self.client.get_orderbook_ticker(symbol=symbol)
|
||||
return {
|
||||
"bid": float(ticker['bidPrice']),
|
||||
"ask": float(ticker['askPrice'])
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get current price for {symbol}: {e}")
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
|
||||
def place_order(self, symbol: str, order_type: OrderType, side: str,
|
||||
size: float, price: Optional[float] = None,
|
||||
stop_loss: Optional[float] = None,
|
||||
take_profit: Optional[float] = None) -> Order:
|
||||
"""Place a trading order on Binance"""
|
||||
if not self.is_connected:
|
||||
raise Exception("Not connected to Binance")
|
||||
|
||||
try:
|
||||
# Convert order parameters
|
||||
binance_side = side.upper() # 'BUY' or 'SELL'
|
||||
|
||||
# Determine order type
|
||||
if order_type == OrderType.MARKET_BUY or order_type == OrderType.MARKET_SELL:
|
||||
binance_type = "MARKET"
|
||||
elif order_type == OrderType.LIMIT_BUY or order_type == OrderType.LIMIT_SELL:
|
||||
binance_type = "LIMIT"
|
||||
else:
|
||||
raise ValueError(f"Unsupported order type: {order_type}")
|
||||
|
||||
# Prepare order parameters
|
||||
order_params = {
|
||||
'symbol': symbol,
|
||||
'side': binance_side,
|
||||
'type': binance_type,
|
||||
'quantity': size,
|
||||
}
|
||||
|
||||
if binance_type == "LIMIT":
|
||||
order_params['price'] = price
|
||||
order_params['timeInForce'] = 'GTC' # Good Till Cancelled
|
||||
|
||||
# Place order
|
||||
result = self.client.create_order(**order_params)
|
||||
|
||||
# Create Order object
|
||||
order = Order(
|
||||
order_id=str(result['orderId']),
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side.lower(),
|
||||
size=size,
|
||||
price=price
|
||||
)
|
||||
|
||||
# Update status based on result
|
||||
if result['status'] == 'FILLED':
|
||||
order.status = OrderStatus.FILLED
|
||||
order.filled_size = float(result.get('executedQty', 0))
|
||||
order.avg_fill_price = float(result.get('price', price or 0))
|
||||
elif result['status'] == 'NEW':
|
||||
order.status = OrderStatus.PENDING
|
||||
|
||||
logger.info(f"Order placed: {order.order_id} for {symbol}")
|
||||
return order
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place order: {e}")
|
||||
# Return failed order
|
||||
order = Order(
|
||||
order_id="failed",
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side.lower(),
|
||||
size=size,
|
||||
price=price
|
||||
)
|
||||
order.status = OrderStatus.REJECTED
|
||||
return order
|
||||
|
||||
def cancel_order(self, order_id: str) -> bool:
|
||||
"""Cancel an existing order"""
|
||||
if not self.is_connected:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Note: Need symbol to cancel order in Binance
|
||||
# This is a limitation - may need to store order info
|
||||
logger.warning("Cancel order requires symbol - implement order tracking")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cancel order {order_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_positions(self) -> List[Position]:
|
||||
"""Get all open positions (for futures)"""
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
try:
|
||||
# For spot trading, positions are just balances
|
||||
account = self.client.get_account()
|
||||
positions = []
|
||||
|
||||
for balance in account['balances']:
|
||||
free = float(balance['free'])
|
||||
locked = float(balance['locked'])
|
||||
total = free + locked
|
||||
|
||||
if total > 0:
|
||||
# Create position for non-zero balances
|
||||
position = Position(
|
||||
symbol=balance['asset'],
|
||||
side='long', # Spot is always long
|
||||
size=total,
|
||||
entry_price=0.0, # Not available for spot
|
||||
current_price=0.0, # Would need to fetch
|
||||
unrealized_pnl=0.0 # Not calculated for spot
|
||||
)
|
||||
positions.append(position)
|
||||
|
||||
return positions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get positions: {e}")
|
||||
return []
|
||||
|
||||
def get_orders(self) -> List[Order]:
|
||||
"""Get all pending orders"""
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Get open orders for all symbols (limitation: need symbol)
|
||||
# For now, return empty - would need to track symbols
|
||||
logger.warning("Get orders requires symbol tracking - implement order cache")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get orders: {e}")
|
||||
return []
|
||||
|
||||
def get_account_info(self) -> AccountInfo:
|
||||
"""Get account information"""
|
||||
if not self.is_connected:
|
||||
return AccountInfo(0, 0, 0, 0, 0, "USDT")
|
||||
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
|
||||
# Calculate total balance in USDT
|
||||
total_balance = 0.0
|
||||
|
||||
for balance in account['balances']:
|
||||
free = float(balance['free'])
|
||||
locked = float(balance['locked'])
|
||||
total = free + locked
|
||||
|
||||
if total > 0:
|
||||
asset = balance['asset']
|
||||
if asset == 'USDT':
|
||||
total_balance += total
|
||||
else:
|
||||
# Convert to USDT (simplified - would need price conversion)
|
||||
# For demo purposes, assume small balances
|
||||
if asset in ['BTC', 'ETH']:
|
||||
total_balance += total * 30000 # Rough estimate
|
||||
else:
|
||||
total_balance += total # Assume stablecoin or ignore
|
||||
|
||||
return AccountInfo(
|
||||
balance=total_balance,
|
||||
equity=total_balance, # Same for spot
|
||||
margin=0.0, # Not applicable for spot
|
||||
free_margin=total_balance,
|
||||
margin_level=100.0, # Not applicable for spot
|
||||
currency="USDT"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get account info: {e}")
|
||||
return AccountInfo(0, 0, 0, 0, 0, "USDT")
|
||||
|
||||
def get_trade_history(self, days: int = 30) -> List[Dict]:
|
||||
"""Get trade history"""
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Get trades for major symbols (limitation: need symbol)
|
||||
logger.warning("Trade history requires symbol tracking - implement symbol cache")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get trade history: {e}")
|
||||
return []
|
||||
|
||||
def normalize_symbol(self, symbol: str) -> str:
|
||||
"""Normalize symbol format for Binance"""
|
||||
# Binance uses format like 'BTCUSDT', 'ETHUSDT'
|
||||
symbol = symbol.upper().replace("/", "").replace("-", "")
|
||||
|
||||
# Common conversions
|
||||
if symbol.endswith("USD") and not symbol.endswith("USDT"):
|
||||
symbol = symbol.replace("USD", "USDT")
|
||||
|
||||
return symbol
|
||||
|
||||
def is_market_open(self) -> bool:
|
||||
"""Crypto markets are always open"""
|
||||
return True
|
||||
|
||||
# Convenience function to create Binance broker
|
||||
def create_binance_broker(testnet: bool = True) -> BinanceBroker:
|
||||
"""Create a Binance broker instance"""
|
||||
return BinanceBroker(testnet=testnet)
|
||||
@@ -1,234 +0,0 @@
|
||||
# core/brokers/broker_factory.py
|
||||
"""
|
||||
Broker Factory for QuantumBotX
|
||||
Manages multiple brokers and provides unified interface
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Optional, List
|
||||
from enum import Enum
|
||||
|
||||
from .base_broker import BaseBroker
|
||||
from .binance_broker import BinanceBroker
|
||||
from .ctrader_broker import CTraderBroker
|
||||
from .interactive_brokers import InteractiveBrokersBroker
|
||||
from .tradingview_broker import TradingViewBroker
|
||||
from .indonesian_brokers import (
|
||||
IndopremierBroker, XMIndonesiaBroker,
|
||||
OctaFXIndonesiaBroker, HSBCIndonesiaBroker
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BrokerType(Enum):
|
||||
MT5 = "mt5"
|
||||
BINANCE = "binance"
|
||||
BINANCE_FUTURES = "binance_futures"
|
||||
CTRADER = "ctrader"
|
||||
INTERACTIVE_BROKERS = "interactive_brokers"
|
||||
TRADINGVIEW = "tradingview"
|
||||
# Indonesian brokers
|
||||
INDOPREMIER = "indopremier"
|
||||
XM_INDONESIA = "xm_indonesia"
|
||||
OCTAFX_INDONESIA = "octafx_indonesia"
|
||||
HSBC_INDONESIA = "hsbc_indonesia"
|
||||
|
||||
class BrokerFactory:
|
||||
"""
|
||||
Factory class to create and manage different broker instances
|
||||
"""
|
||||
|
||||
_brokers: Dict[str, BaseBroker] = {}
|
||||
_configs: Dict[str, Dict] = {}
|
||||
|
||||
@classmethod
|
||||
def register_broker_config(cls, broker_id: str, broker_type: BrokerType, config: Dict):
|
||||
"""Register broker configuration"""
|
||||
cls._configs[broker_id] = {
|
||||
'type': broker_type,
|
||||
'config': config
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_broker(cls, broker_id: str) -> Optional[BaseBroker]:
|
||||
"""Create broker instance from registered configuration"""
|
||||
|
||||
if broker_id in cls._brokers:
|
||||
return cls._brokers[broker_id]
|
||||
|
||||
if broker_id not in cls._configs:
|
||||
logger.error(f"No configuration found for broker: {broker_id}")
|
||||
return None
|
||||
|
||||
broker_config = cls._configs[broker_id]
|
||||
broker_type = broker_config['type']
|
||||
config = broker_config['config']
|
||||
|
||||
try:
|
||||
if broker_type == BrokerType.BINANCE:
|
||||
broker = BinanceBroker(testnet=config.get('testnet', True))
|
||||
elif broker_type == BrokerType.BINANCE_FUTURES:
|
||||
# Future implementation
|
||||
broker = BinanceBroker(testnet=config.get('testnet', True))
|
||||
elif broker_type == BrokerType.CTRADER:
|
||||
broker = CTraderBroker(demo=config.get('demo', True))
|
||||
elif broker_type == BrokerType.INTERACTIVE_BROKERS:
|
||||
broker = InteractiveBrokersBroker(paper_trading=config.get('paper_trading', True))
|
||||
elif broker_type == BrokerType.TRADINGVIEW:
|
||||
broker = TradingViewBroker(paper_trading=config.get('paper_trading', True))
|
||||
elif broker_type == BrokerType.INDOPREMIER:
|
||||
broker = IndopremierBroker(demo=config.get('demo', True))
|
||||
elif broker_type == BrokerType.XM_INDONESIA:
|
||||
broker = XMIndonesiaBroker(demo=config.get('demo', True))
|
||||
elif broker_type == BrokerType.OCTAFX_INDONESIA:
|
||||
broker = OctaFXIndonesiaBroker(demo=config.get('demo', True))
|
||||
elif broker_type == BrokerType.HSBC_INDONESIA:
|
||||
broker = HSBCIndonesiaBroker(demo=config.get('demo', True))
|
||||
elif broker_type == BrokerType.MT5:
|
||||
# Import MT5 broker when implemented
|
||||
from .mt5_broker import MT5Broker
|
||||
broker = MT5Broker()
|
||||
else:
|
||||
logger.error(f"Unsupported broker type: {broker_type}")
|
||||
return None
|
||||
|
||||
# Connect broker
|
||||
if broker.connect(config.get('credentials', {})):
|
||||
cls._brokers[broker_id] = broker
|
||||
logger.info(f"Successfully created and connected broker: {broker_id}")
|
||||
return broker
|
||||
else:
|
||||
logger.error(f"Failed to connect broker: {broker_id}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating broker {broker_id}: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_broker(cls, broker_id: str) -> Optional[BaseBroker]:
|
||||
"""Get existing broker instance"""
|
||||
return cls._brokers.get(broker_id)
|
||||
|
||||
@classmethod
|
||||
def disconnect_all(cls):
|
||||
"""Disconnect all brokers"""
|
||||
for broker_id, broker in cls._brokers.items():
|
||||
try:
|
||||
broker.disconnect()
|
||||
logger.info(f"Disconnected broker: {broker_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error disconnecting broker {broker_id}: {e}")
|
||||
|
||||
cls._brokers.clear()
|
||||
|
||||
@classmethod
|
||||
def get_all_brokers(cls) -> Dict[str, BaseBroker]:
|
||||
"""Get all connected brokers"""
|
||||
return cls._brokers.copy()
|
||||
|
||||
@classmethod
|
||||
def get_supported_symbols(cls, broker_id: str) -> List[str]:
|
||||
"""Get supported symbols for a broker"""
|
||||
broker = cls.get_broker(broker_id)
|
||||
if broker:
|
||||
return broker.get_symbols()
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def is_broker_connected(cls, broker_id: str) -> bool:
|
||||
"""Check if broker is connected"""
|
||||
broker = cls.get_broker(broker_id)
|
||||
return broker.is_connected if broker else False
|
||||
|
||||
# Configuration helper functions
|
||||
def setup_demo_brokers():
|
||||
"""Setup demo brokers for testing"""
|
||||
|
||||
# Binance Testnet configuration
|
||||
BrokerFactory.register_broker_config(
|
||||
broker_id="binance_testnet",
|
||||
broker_type=BrokerType.BINANCE,
|
||||
config={
|
||||
'testnet': True,
|
||||
'credentials': {
|
||||
'api_key': '', # Add your testnet API key
|
||||
'secret_key': '' # Add your testnet secret key
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# MT5 Demo configuration
|
||||
BrokerFactory.register_broker_config(
|
||||
broker_id="mt5_demo",
|
||||
broker_type=BrokerType.MT5,
|
||||
config={
|
||||
'credentials': {
|
||||
'login': '', # Add your MT5 demo login
|
||||
'password': '', # Add your MT5 demo password
|
||||
'server': 'MetaQuotes-Demo'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def load_brokers_from_env():
|
||||
"""Load broker configurations from environment variables"""
|
||||
import os
|
||||
|
||||
# Binance configuration
|
||||
binance_api_key = os.getenv('BINANCE_API_KEY')
|
||||
binance_secret = os.getenv('BINANCE_SECRET_KEY')
|
||||
binance_testnet = os.getenv('BINANCE_TESTNET', 'true').lower() == 'true'
|
||||
|
||||
if binance_api_key and binance_secret:
|
||||
BrokerFactory.register_broker_config(
|
||||
broker_id="binance",
|
||||
broker_type=BrokerType.BINANCE,
|
||||
config={
|
||||
'testnet': binance_testnet,
|
||||
'credentials': {
|
||||
'api_key': binance_api_key,
|
||||
'secret_key': binance_secret
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# MT5 configuration
|
||||
mt5_login = os.getenv('MT5_LOGIN')
|
||||
mt5_password = os.getenv('MT5_PASSWORD')
|
||||
mt5_server = os.getenv('MT5_SERVER', 'MetaQuotes-Demo')
|
||||
|
||||
if mt5_login and mt5_password:
|
||||
BrokerFactory.register_broker_config(
|
||||
broker_id="mt5",
|
||||
broker_type=BrokerType.MT5,
|
||||
config={
|
||||
'credentials': {
|
||||
'login': mt5_login,
|
||||
'password': mt5_password,
|
||||
'server': mt5_server
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Load configurations
|
||||
load_brokers_from_env()
|
||||
|
||||
# Create brokers
|
||||
binance_broker = BrokerFactory.create_broker("binance")
|
||||
mt5_broker = BrokerFactory.create_broker("mt5")
|
||||
|
||||
if binance_broker:
|
||||
print(f"Binance connected: {binance_broker.is_connected}")
|
||||
symbols = binance_broker.get_symbols()[:10] # First 10 symbols
|
||||
print(f"Binance symbols: {symbols}")
|
||||
|
||||
if mt5_broker:
|
||||
print(f"MT5 connected: {mt5_broker.is_connected}")
|
||||
account_info = mt5_broker.get_account_info()
|
||||
print(f"MT5 balance: {account_info.balance}")
|
||||
|
||||
# Cleanup
|
||||
BrokerFactory.disconnect_all()
|
||||
@@ -1,415 +0,0 @@
|
||||
# core/brokers/ctrader_broker.py
|
||||
"""
|
||||
cTrader Broker Integration for QuantumBotX
|
||||
Modern forex/CFD platform with excellent API
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
|
||||
from .base_broker import (
|
||||
BaseBroker, OrderType, OrderStatus, Timeframe,
|
||||
Position, Order, AccountInfo
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CTraderBroker(BaseBroker):
|
||||
"""
|
||||
cTrader (cTID) implementation of the universal broker interface.
|
||||
Uses cTrader REST API for modern forex trading.
|
||||
"""
|
||||
|
||||
def __init__(self, demo: bool = True):
|
||||
super().__init__("cTrader")
|
||||
self.demo = demo
|
||||
self.client_id = None
|
||||
self.client_secret = None
|
||||
self.access_token = None
|
||||
self.account_id = None
|
||||
self.base_url = "https://demo-api.ctraderapi.com" if demo else "https://api.ctraderapi.com"
|
||||
|
||||
# Timeframe mapping
|
||||
self.timeframe_map = {
|
||||
Timeframe.M1: "M1",
|
||||
Timeframe.M5: "M5",
|
||||
Timeframe.M15: "M15",
|
||||
Timeframe.M30: "M30",
|
||||
Timeframe.H1: "H1",
|
||||
Timeframe.H4: "H4",
|
||||
Timeframe.D1: "D1"
|
||||
}
|
||||
|
||||
def connect(self, credentials: Dict) -> bool:
|
||||
"""
|
||||
Connect to cTrader with OAuth credentials
|
||||
credentials: {"client_id": "...", "client_secret": "...", "account_id": "..."}
|
||||
"""
|
||||
try:
|
||||
self.client_id = credentials.get("client_id")
|
||||
self.client_secret = credentials.get("client_secret")
|
||||
self.account_id = credentials.get("account_id")
|
||||
|
||||
if not all([self.client_id, self.client_secret, self.account_id]):
|
||||
logger.error("cTrader client_id, client_secret, and account_id are required")
|
||||
return False
|
||||
|
||||
# OAuth token request
|
||||
token_url = f"{self.base_url}/oauth/v2/token"
|
||||
token_data = {
|
||||
'grant_type': 'client_credentials',
|
||||
'client_id': self.client_id,
|
||||
'client_secret': self.client_secret,
|
||||
'scope': 'trading'
|
||||
}
|
||||
|
||||
response = requests.post(token_url, data=token_data)
|
||||
|
||||
if response.status_code == 200:
|
||||
token_info = response.json()
|
||||
self.access_token = token_info['access_token']
|
||||
self.is_connected = True
|
||||
|
||||
# Get supported symbols
|
||||
self._load_symbols()
|
||||
|
||||
logger.info(f"Connected to cTrader {'Demo' if self.demo else 'Live'}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"cTrader authentication failed: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to cTrader: {e}")
|
||||
self.is_connected = False
|
||||
return False
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
"""Disconnect from cTrader"""
|
||||
self.access_token = None
|
||||
self.is_connected = False
|
||||
logger.info("Disconnected from cTrader")
|
||||
return True
|
||||
|
||||
def _make_request(self, endpoint: str, method: str = "GET", data: Dict = None) -> Dict:
|
||||
"""Make authenticated request to cTrader API"""
|
||||
if not self.access_token:
|
||||
raise Exception("Not authenticated with cTrader")
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.access_token}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
if method == "GET":
|
||||
response = requests.get(url, headers=headers, params=data)
|
||||
elif method == "POST":
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
elif method == "PUT":
|
||||
response = requests.put(url, headers=headers, json=data)
|
||||
elif method == "DELETE":
|
||||
response = requests.delete(url, headers=headers)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception(f"cTrader API error: {response.status_code} - {response.text}")
|
||||
|
||||
def _load_symbols(self):
|
||||
"""Load available symbols from cTrader"""
|
||||
try:
|
||||
symbols_data = self._make_request("/v2/symbols")
|
||||
self.supported_symbols = [s['symbolName'] for s in symbols_data.get('symbols', [])]
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cTrader symbols: {e}")
|
||||
# Common forex symbols as fallback
|
||||
self.supported_symbols = [
|
||||
'EURUSD', 'GBPUSD', 'USDJPY', 'USDCHF', 'AUDUSD', 'USDCAD',
|
||||
'NZDUSD', 'EURGBP', 'EURJPY', 'GBPJPY', 'XAUUSD', 'XAGUSD'
|
||||
]
|
||||
|
||||
def get_symbols(self) -> List[str]:
|
||||
"""Get list of available trading symbols"""
|
||||
return self.supported_symbols
|
||||
|
||||
def get_market_data(self, symbol: str, timeframe: Timeframe, count: int = 500) -> pd.DataFrame:
|
||||
"""Get OHLCV market data from cTrader"""
|
||||
if not self.is_connected:
|
||||
raise Exception("Not connected to cTrader")
|
||||
|
||||
try:
|
||||
# Convert timeframe
|
||||
ct_timeframe = self.timeframe_map[timeframe]
|
||||
|
||||
# Calculate from time (count bars back)
|
||||
now = datetime.utcnow()
|
||||
# Estimate time per bar
|
||||
minutes_per_bar = {
|
||||
'M1': 1, 'M5': 5, 'M15': 15, 'M30': 30,
|
||||
'H1': 60, 'H4': 240, 'D1': 1440
|
||||
}
|
||||
|
||||
minutes_back = count * minutes_per_bar.get(ct_timeframe, 60)
|
||||
from_time = now - timedelta(minutes=minutes_back)
|
||||
|
||||
# Request historical data
|
||||
params = {
|
||||
'symbolName': symbol,
|
||||
'periodName': ct_timeframe,
|
||||
'fromTimestamp': int(from_time.timestamp() * 1000),
|
||||
'toTimestamp': int(now.timestamp() * 1000),
|
||||
'count': count
|
||||
}
|
||||
|
||||
data = self._make_request("/v2/bars", params=params)
|
||||
bars = data.get('bars', [])
|
||||
|
||||
if not bars:
|
||||
return pd.DataFrame()
|
||||
|
||||
# Convert to DataFrame
|
||||
df_data = []
|
||||
for bar in bars:
|
||||
df_data.append({
|
||||
'time': datetime.fromtimestamp(bar['timestamp'] / 1000),
|
||||
'open': bar['open'],
|
||||
'high': bar['high'],
|
||||
'low': bar['low'],
|
||||
'close': bar['close'],
|
||||
'volume': bar.get('volume', 0)
|
||||
})
|
||||
|
||||
return pd.DataFrame(df_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get market data for {symbol}: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_current_price(self, symbol: str) -> Dict[str, float]:
|
||||
"""Get current bid/ask prices"""
|
||||
if not self.is_connected:
|
||||
raise Exception("Not connected to cTrader")
|
||||
|
||||
try:
|
||||
data = self._make_request(f"/v2/symbols/{symbol}/tick")
|
||||
return {
|
||||
"bid": data['bid'],
|
||||
"ask": data['ask']
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get current price for {symbol}: {e}")
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
|
||||
def place_order(self, symbol: str, order_type: OrderType, side: str,
|
||||
size: float, price: Optional[float] = None,
|
||||
stop_loss: Optional[float] = None,
|
||||
take_profit: Optional[float] = None) -> Order:
|
||||
"""Place a trading order on cTrader"""
|
||||
if not self.is_connected:
|
||||
raise Exception("Not connected to cTrader")
|
||||
|
||||
try:
|
||||
# Convert order parameters
|
||||
ct_side = "BUY" if side.lower() == "buy" else "SELL"
|
||||
|
||||
# Convert volume to lots (cTrader uses volume in units)
|
||||
volume = int(size * 100000) # Convert lots to units
|
||||
|
||||
# Determine order type
|
||||
if order_type in [OrderType.MARKET_BUY, OrderType.MARKET_SELL]:
|
||||
ct_type = "MARKET"
|
||||
elif order_type in [OrderType.LIMIT_BUY, OrderType.LIMIT_SELL]:
|
||||
ct_type = "LIMIT"
|
||||
else:
|
||||
raise ValueError(f"Unsupported order type: {order_type}")
|
||||
|
||||
# Prepare order data
|
||||
order_data = {
|
||||
'accountId': self.account_id,
|
||||
'symbolName': symbol,
|
||||
'orderType': ct_type,
|
||||
'tradeSide': ct_side,
|
||||
'volume': volume,
|
||||
}
|
||||
|
||||
if ct_type == "LIMIT" and price:
|
||||
order_data['limitPrice'] = price
|
||||
|
||||
if stop_loss:
|
||||
order_data['stopLoss'] = stop_loss
|
||||
if take_profit:
|
||||
order_data['takeProfit'] = take_profit
|
||||
|
||||
# Place order
|
||||
result = self._make_request("/v2/orders", method="POST", data=order_data)
|
||||
|
||||
# Create Order object
|
||||
order = Order(
|
||||
order_id=str(result.get('orderId', 'unknown')),
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side.lower(),
|
||||
size=size,
|
||||
price=price
|
||||
)
|
||||
|
||||
order.status = OrderStatus.PENDING
|
||||
if result.get('executionType') == 'TRADE':
|
||||
order.status = OrderStatus.FILLED
|
||||
|
||||
logger.info(f"cTrader order placed: {order.order_id} for {symbol}")
|
||||
return order
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place cTrader order: {e}")
|
||||
order = Order(
|
||||
order_id="failed",
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side.lower(),
|
||||
size=size,
|
||||
price=price
|
||||
)
|
||||
order.status = OrderStatus.REJECTED
|
||||
return order
|
||||
|
||||
def cancel_order(self, order_id: str) -> bool:
|
||||
"""Cancel an existing order"""
|
||||
if not self.is_connected:
|
||||
return False
|
||||
|
||||
try:
|
||||
self._make_request(f"/v2/orders/{order_id}", method="DELETE")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cancel cTrader order {order_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_positions(self) -> List[Position]:
|
||||
"""Get all open positions"""
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
try:
|
||||
data = self._make_request(f"/v2/accounts/{self.account_id}/positions")
|
||||
positions = []
|
||||
|
||||
for pos_data in data.get('positions', []):
|
||||
position = Position(
|
||||
symbol=pos_data['symbolName'],
|
||||
side='long' if pos_data['tradeSide'] == 'BUY' else 'short',
|
||||
size=pos_data['volume'] / 100000, # Convert units to lots
|
||||
entry_price=pos_data['entryPrice'],
|
||||
current_price=pos_data['currentPrice'],
|
||||
unrealized_pnl=pos_data['unrealizedGrossProfit']
|
||||
)
|
||||
positions.append(position)
|
||||
|
||||
return positions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get cTrader positions: {e}")
|
||||
return []
|
||||
|
||||
def get_orders(self) -> List[Order]:
|
||||
"""Get all pending orders"""
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
try:
|
||||
data = self._make_request(f"/v2/accounts/{self.account_id}/orders")
|
||||
orders = []
|
||||
|
||||
for order_data in data.get('orders', []):
|
||||
order = Order(
|
||||
order_id=str(order_data['orderId']),
|
||||
symbol=order_data['symbolName'],
|
||||
order_type=OrderType.LIMIT_BUY, # Simplified
|
||||
side=order_data['tradeSide'].lower(),
|
||||
size=order_data['volume'] / 100000,
|
||||
price=order_data.get('limitPrice')
|
||||
)
|
||||
order.status = OrderStatus.PENDING
|
||||
orders.append(order)
|
||||
|
||||
return orders
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get cTrader orders: {e}")
|
||||
return []
|
||||
|
||||
def get_account_info(self) -> AccountInfo:
|
||||
"""Get account information"""
|
||||
if not self.is_connected:
|
||||
return AccountInfo(0, 0, 0, 0, 0, "USD")
|
||||
|
||||
try:
|
||||
data = self._make_request(f"/v2/accounts/{self.account_id}")
|
||||
|
||||
balance = data.get('balance', 0)
|
||||
equity = data.get('equity', balance)
|
||||
margin = data.get('margin', 0)
|
||||
free_margin = data.get('freeMargin', balance)
|
||||
margin_level = data.get('marginLevel', 100)
|
||||
currency = data.get('currency', 'USD')
|
||||
|
||||
return AccountInfo(
|
||||
balance=balance,
|
||||
equity=equity,
|
||||
margin=margin,
|
||||
free_margin=free_margin,
|
||||
margin_level=margin_level,
|
||||
currency=currency
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get cTrader account info: {e}")
|
||||
return AccountInfo(0, 0, 0, 0, 0, "USD")
|
||||
|
||||
def get_trade_history(self, days: int = 30) -> List[Dict]:
|
||||
"""Get trade history"""
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
try:
|
||||
from_time = datetime.now() - timedelta(days=days)
|
||||
params = {
|
||||
'fromTimestamp': int(from_time.timestamp() * 1000),
|
||||
'toTimestamp': int(datetime.now().timestamp() * 1000)
|
||||
}
|
||||
|
||||
data = self._make_request(f"/v2/accounts/{self.account_id}/deals", params=params)
|
||||
return data.get('deals', [])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get cTrader trade history: {e}")
|
||||
return []
|
||||
|
||||
def normalize_symbol(self, symbol: str) -> str:
|
||||
"""Normalize symbol format for cTrader"""
|
||||
# cTrader typically uses format like 'EURUSD', 'GBPUSD'
|
||||
return symbol.upper().replace("/", "").replace("-", "")
|
||||
|
||||
def is_market_open(self) -> bool:
|
||||
"""Check if forex market is open"""
|
||||
now = datetime.utcnow()
|
||||
# Forex market is open from Sunday 22:00 UTC to Friday 22:00 UTC
|
||||
if now.weekday() == 5: # Saturday
|
||||
return False
|
||||
if now.weekday() == 6 and now.hour < 22: # Sunday before 22:00 UTC
|
||||
return False
|
||||
if now.weekday() == 4 and now.hour >= 22: # Friday after 22:00 UTC
|
||||
return False
|
||||
return True
|
||||
|
||||
# Convenience function
|
||||
def create_ctrader_broker(demo: bool = True) -> CTraderBroker:
|
||||
"""Create a cTrader broker instance"""
|
||||
return CTraderBroker(demo=demo)
|
||||
@@ -1,546 +0,0 @@
|
||||
# core/brokers/indonesian_brokers.py
|
||||
"""
|
||||
Indonesian Market Brokers Integration for QuantumBotX
|
||||
Supporting local Indonesian brokers and international brokers popular in Indonesia
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
import numpy as np
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
|
||||
from .base_broker import (
|
||||
BaseBroker, OrderType, OrderStatus, Timeframe,
|
||||
Position, Order, AccountInfo
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class IndopremierBroker(BaseBroker):
|
||||
"""
|
||||
Indopremier Securities (IPOT) - Popular Indonesian broker
|
||||
Known for good demo accounts and local market access
|
||||
"""
|
||||
|
||||
def __init__(self, demo: bool = True):
|
||||
super().__init__("Indopremier")
|
||||
self.demo = demo
|
||||
self.base_url = "https://demo-api.indopremier.com" if demo else "https://api.indopremier.com"
|
||||
self.session = requests.Session()
|
||||
|
||||
# Indonesian market symbols
|
||||
self.supported_symbols = [
|
||||
# IDX (Indonesian Stock Exchange) - Blue chips
|
||||
'BBCA.JK', # Bank Central Asia
|
||||
'BBRI.JK', # Bank Rakyat Indonesia
|
||||
'BMRI.JK', # Bank Mandiri
|
||||
'TLKM.JK', # Telkom Indonesia
|
||||
'ASII.JK', # Astra International
|
||||
'UNVR.JK', # Unilever Indonesia
|
||||
'ICBP.JK', # Indofood CBP
|
||||
'INDF.JK', # Indofood Sukses Makmur
|
||||
'GGRM.JK', # Gudang Garam
|
||||
'HMSP.JK', # HM Sampoerna
|
||||
|
||||
# IDX ETFs and Indices
|
||||
'LQ45.JK', # LQ45 Index
|
||||
'IHSG.JK', # Jakarta Composite Index
|
||||
|
||||
# International through Indopremier
|
||||
'USDID', # USD/IDR
|
||||
'USDIDR', # USD/IDR alternative
|
||||
'XAUIDR', # Gold in IDR
|
||||
]
|
||||
|
||||
def connect(self, credentials: Dict) -> bool:
|
||||
"""Connect to Indopremier"""
|
||||
try:
|
||||
username = credentials.get("username")
|
||||
password = credentials.get("password")
|
||||
|
||||
if not all([username, password]):
|
||||
logger.error("Indopremier username and password required")
|
||||
return False
|
||||
|
||||
# Simulate authentication for demo
|
||||
if self.demo:
|
||||
self.is_connected = True
|
||||
logger.info("Connected to Indopremier Demo")
|
||||
return True
|
||||
|
||||
# Real implementation would use actual API
|
||||
auth_data = {
|
||||
'username': username,
|
||||
'password': password
|
||||
}
|
||||
|
||||
# This would be actual API call
|
||||
self.is_connected = True
|
||||
logger.info("Connected to Indopremier Live")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Indopremier: {e}")
|
||||
return False
|
||||
|
||||
def get_market_data(self, symbol: str, timeframe: Timeframe, count: int = 500) -> pd.DataFrame:
|
||||
"""Get Indonesian market data"""
|
||||
try:
|
||||
# For demo, generate realistic Indonesian stock data
|
||||
dates = pd.date_range(end=datetime.now(), periods=count, freq='1h')
|
||||
|
||||
# Realistic prices for Indonesian stocks
|
||||
base_prices = {
|
||||
'BBCA.JK': 9000, # BCA around 9,000 IDR
|
||||
'BBRI.JK': 4500, # BRI around 4,500 IDR
|
||||
'BMRI.JK': 8500, # Mandiri around 8,500 IDR
|
||||
'TLKM.JK': 3200, # Telkom around 3,200 IDR
|
||||
'ASII.JK': 6800, # Astra around 6,800 IDR
|
||||
'UNVR.JK': 7200, # Unilever around 7,200 IDR
|
||||
'USDID': 15400, # USD/IDR around 15,400
|
||||
'XAUIDR': 1000000, # Gold around 1M IDR per oz
|
||||
}
|
||||
|
||||
base_price = base_prices.get(symbol, 5000)
|
||||
|
||||
# Indonesian market volatility (generally lower than crypto)
|
||||
volatility = 0.015 if '.JK' in symbol else 0.008 # 1.5% for stocks, 0.8% for forex
|
||||
|
||||
# Generate price movements
|
||||
returns = np.random.randn(count) * volatility
|
||||
prices = base_price * (1 + returns).cumprod()
|
||||
|
||||
df = pd.DataFrame({
|
||||
'time': dates,
|
||||
'open': prices,
|
||||
'high': prices * (1 + np.random.uniform(0, 0.01, count)),
|
||||
'low': prices * (1 - np.random.uniform(0, 0.01, count)),
|
||||
'close': prices,
|
||||
'volume': np.random.randint(100000, 1000000, count) # Indonesian market volumes
|
||||
})
|
||||
|
||||
# Ensure OHLC integrity
|
||||
df['high'] = df[['high', 'close', 'open']].max(axis=1)
|
||||
df['low'] = df[['low', 'close', 'open']].min(axis=1)
|
||||
|
||||
# Adjust for Indonesian market hours (09:00-16:00 WIB, Mon-Fri)
|
||||
# Filter out weekend data for stock symbols
|
||||
if '.JK' in symbol:
|
||||
df = df[df['time'].dt.weekday < 5] # Monday=0, Sunday=6
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get Indopremier market data for {symbol}: {e}")
|
||||
return pd.DataFrame()
|
||||
def disconnect(self) -> bool:
|
||||
"""Disconnect from Indopremier"""
|
||||
self.is_connected = False
|
||||
logger.info("Disconnected from Indopremier")
|
||||
return True
|
||||
|
||||
def get_symbols(self) -> List[str]:
|
||||
"""Get list of available trading symbols"""
|
||||
return self.supported_symbols
|
||||
|
||||
def get_current_price(self, symbol: str) -> Dict[str, float]:
|
||||
"""Get current bid/ask prices"""
|
||||
try:
|
||||
# For demo, use last price from market data
|
||||
df = self.get_market_data(symbol, Timeframe.M1, 1)
|
||||
if not df.empty:
|
||||
last_price = df.iloc[-1]['close']
|
||||
spread = last_price * 0.001 # 0.1% spread for Indonesian stocks
|
||||
return {
|
||||
"bid": last_price - spread/2,
|
||||
"ask": last_price + spread/2
|
||||
}
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get Indopremier current price for {symbol}: {e}")
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
|
||||
def place_order(self, symbol: str, order_type: OrderType, side: str,
|
||||
size: float, price: Optional[float] = None,
|
||||
stop_loss: Optional[float] = None,
|
||||
take_profit: Optional[float] = None) -> Order:
|
||||
"""Place order (simulated for demo)"""
|
||||
try:
|
||||
order_id = str(int(time.time()))
|
||||
|
||||
# For Indonesian stocks, size is in lots (100 shares)
|
||||
if '.JK' in symbol:
|
||||
size = max(1, int(size)) # Minimum 1 lot
|
||||
|
||||
order = Order(
|
||||
order_id=order_id,
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side.lower(),
|
||||
size=size,
|
||||
price=price
|
||||
)
|
||||
|
||||
# Simulate immediate execution for demo
|
||||
order.status = OrderStatus.FILLED
|
||||
order.filled_size = size
|
||||
|
||||
current_price = self.get_current_price(symbol)
|
||||
order.avg_fill_price = current_price['ask'] if side.lower() == 'buy' else current_price['bid']
|
||||
|
||||
logger.info(f"Indopremier demo order: {side} {size} {symbol} at {order.avg_fill_price}")
|
||||
return order
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place Indopremier order: {e}")
|
||||
order = Order(
|
||||
order_id="failed",
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side.lower(),
|
||||
size=size,
|
||||
price=price
|
||||
)
|
||||
order.status = OrderStatus.REJECTED
|
||||
return order
|
||||
|
||||
def cancel_order(self, order_id: str) -> bool:
|
||||
"""Cancel an existing order"""
|
||||
logger.info(f"Indopremier demo: Order {order_id} cancelled")
|
||||
return True
|
||||
|
||||
def get_positions(self) -> List[Position]:
|
||||
"""Get all open positions"""
|
||||
# For demo, return empty list
|
||||
return []
|
||||
|
||||
def get_orders(self) -> List[Order]:
|
||||
"""Get all pending orders"""
|
||||
# For demo, return empty list
|
||||
return []
|
||||
|
||||
def get_account_info(self) -> AccountInfo:
|
||||
"""Get account information"""
|
||||
try:
|
||||
return AccountInfo(
|
||||
balance=1000000000, # 1 billion IDR demo balance
|
||||
equity=1000000000,
|
||||
margin=0.0,
|
||||
free_margin=1000000000,
|
||||
margin_level=100.0,
|
||||
currency="IDR"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get Indopremier account info: {e}")
|
||||
return AccountInfo(0, 0, 0, 0, 0, "IDR")
|
||||
|
||||
def get_trade_history(self, days: int = 30) -> List[Dict]:
|
||||
"""Get trade history"""
|
||||
# For demo, return empty list
|
||||
return []
|
||||
|
||||
class XMIndonesiaBroker(BaseBroker):
|
||||
"""
|
||||
XM Indonesia - Popular international broker in Indonesia
|
||||
Offers forex, commodities, and indices with good demo accounts
|
||||
"""
|
||||
|
||||
def __init__(self, demo: bool = True):
|
||||
super().__init__("XM Indonesia")
|
||||
self.demo = demo
|
||||
|
||||
# XM Indonesia popular symbols
|
||||
self.supported_symbols = [
|
||||
# Major Forex pairs
|
||||
'EURUSD', 'GBPUSD', 'USDJPY', 'USDCHF', 'AUDUSD', 'USDCAD',
|
||||
'NZDUSD', 'EURGBP', 'EURJPY', 'GBPJPY',
|
||||
|
||||
# IDR pairs (if available)
|
||||
'USDIDR', 'EURIDR', 'GBPIDR', 'JPYIDR',
|
||||
|
||||
# Commodities popular in Indonesia
|
||||
'XAUUSD', 'XAGUSD', 'USOIL', 'UKOIL', 'NGAS',
|
||||
|
||||
# Indices
|
||||
'US30', 'SPX500', 'NAS100', 'UK100', 'GER30', 'FRA40',
|
||||
'AUS200', 'JPN225', 'HK50',
|
||||
|
||||
# Cryptocurrency CFDs
|
||||
'BTCUSD', 'ETHUSD', 'LTCUSD', 'XRPUSD'
|
||||
]
|
||||
|
||||
def connect(self, credentials: Dict) -> bool:
|
||||
"""Connect to XM Indonesia"""
|
||||
try:
|
||||
login = credentials.get("login")
|
||||
password = credentials.get("password")
|
||||
server = credentials.get("server", "XM-Demo" if self.demo else "XM-Real")
|
||||
|
||||
if not all([login, password]):
|
||||
logger.error("XM Indonesia login and password required")
|
||||
return False
|
||||
|
||||
# XM uses MT4/MT5 platform, so similar to existing MT5 integration
|
||||
self.is_connected = True
|
||||
|
||||
logger.info(f"Connected to XM Indonesia {'Demo' if self.demo else 'Live'}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to XM Indonesia: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
self.is_connected = False
|
||||
return True
|
||||
|
||||
def get_symbols(self) -> List[str]:
|
||||
return self.supported_symbols
|
||||
|
||||
def get_market_data(self, symbol: str, timeframe: Timeframe, count: int = 500) -> pd.DataFrame:
|
||||
# Generate simulated forex data
|
||||
dates = pd.date_range(end=datetime.now(), periods=count, freq='1h')
|
||||
base_prices = {'EURUSD': 1.0850, 'USDIDR': 15400, 'XAUUSD': 2020}
|
||||
base_price = base_prices.get(symbol, 1.0)
|
||||
|
||||
returns = np.random.randn(count) * 0.01
|
||||
prices = base_price * (1 + returns).cumprod()
|
||||
|
||||
return pd.DataFrame({
|
||||
'time': dates, 'open': prices, 'high': prices * 1.002,
|
||||
'low': prices * 0.998, 'close': prices, 'volume': np.random.randint(1000, 10000, count)
|
||||
})
|
||||
|
||||
def get_current_price(self, symbol: str) -> Dict[str, float]:
|
||||
df = self.get_market_data(symbol, Timeframe.M1, 1)
|
||||
if not df.empty:
|
||||
price = df.iloc[-1]['close']
|
||||
return {"bid": price - 0.0001, "ask": price + 0.0001}
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
|
||||
def place_order(self, symbol: str, order_type: OrderType, side: str, size: float,
|
||||
price: Optional[float] = None, stop_loss: Optional[float] = None,
|
||||
take_profit: Optional[float] = None) -> Order:
|
||||
order = Order(str(int(time.time())), symbol, order_type, side.lower(), size, price)
|
||||
order.status = OrderStatus.FILLED
|
||||
return order
|
||||
|
||||
def cancel_order(self, order_id: str) -> bool:
|
||||
return True
|
||||
|
||||
def get_positions(self) -> List[Position]:
|
||||
return []
|
||||
|
||||
def get_orders(self) -> List[Order]:
|
||||
return []
|
||||
|
||||
def get_account_info(self) -> AccountInfo:
|
||||
return AccountInfo(10000, 10000, 0, 10000, 100, "USD")
|
||||
|
||||
def get_trade_history(self, days: int = 30) -> List[Dict]:
|
||||
return []
|
||||
|
||||
class OctaFXIndonesiaBroker(BaseBroker):
|
||||
"""
|
||||
OctaFX Indonesia - Another popular international broker
|
||||
Known for good spreads and demo accounts
|
||||
"""
|
||||
|
||||
def __init__(self, demo: bool = True):
|
||||
super().__init__("OctaFX Indonesia")
|
||||
self.demo = demo
|
||||
|
||||
self.supported_symbols = [
|
||||
# Forex majors and minors
|
||||
'EURUSD', 'GBPUSD', 'USDJPY', 'USDCHF', 'AUDUSD', 'USDCAD',
|
||||
'NZDUSD', 'EURGBP', 'EURJPY', 'GBPJPY', 'AUDJPY', 'NZDJPY',
|
||||
'EURCHF', 'GBPCHF', 'AUDCHF', 'NZDCHF', 'CADCHF', 'CHFJPY',
|
||||
|
||||
# Exotic pairs including IDR
|
||||
'USDIDR', 'USDSGD', 'USDTHB', 'USDMYR',
|
||||
|
||||
# Metals
|
||||
'XAUUSD', 'XAGUSD', 'XPDUSD', 'XPTUSD',
|
||||
|
||||
# Energies
|
||||
'USOIL', 'UKOIL', 'NGAS',
|
||||
|
||||
# Indices
|
||||
'SPX500', 'NAS100', 'US30', 'UK100', 'GER30', 'FRA40', 'ESP35',
|
||||
'ITA40', 'AUS200', 'JPN225', 'HK50'
|
||||
]
|
||||
|
||||
def connect(self, credentials: Dict) -> bool:
|
||||
self.is_connected = True
|
||||
return True
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
self.is_connected = False
|
||||
return True
|
||||
|
||||
def get_symbols(self) -> List[str]:
|
||||
return self.supported_symbols
|
||||
|
||||
def get_market_data(self, symbol: str, timeframe: Timeframe, count: int = 500) -> pd.DataFrame:
|
||||
dates = pd.date_range(end=datetime.now(), periods=count, freq='1h')
|
||||
base_price = 1.0850 if 'EUR' in symbol else 15400 if 'IDR' in symbol else 100
|
||||
returns = np.random.randn(count) * 0.01
|
||||
prices = base_price * (1 + returns).cumprod()
|
||||
return pd.DataFrame({
|
||||
'time': dates, 'open': prices, 'high': prices * 1.001,
|
||||
'low': prices * 0.999, 'close': prices, 'volume': np.random.randint(1000, 5000, count)
|
||||
})
|
||||
|
||||
def get_current_price(self, symbol: str) -> Dict[str, float]:
|
||||
df = self.get_market_data(symbol, Timeframe.M1, 1)
|
||||
if not df.empty:
|
||||
price = df.iloc[-1]['close']
|
||||
return {"bid": price - 0.0001, "ask": price + 0.0001}
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
|
||||
def place_order(self, symbol: str, order_type: OrderType, side: str, size: float,
|
||||
price: Optional[float] = None, stop_loss: Optional[float] = None,
|
||||
take_profit: Optional[float] = None) -> Order:
|
||||
order = Order(str(int(time.time())), symbol, order_type, side.lower(), size, price)
|
||||
order.status = OrderStatus.FILLED
|
||||
return order
|
||||
|
||||
def cancel_order(self, order_id: str) -> bool:
|
||||
return True
|
||||
|
||||
def get_positions(self) -> List[Position]:
|
||||
return []
|
||||
|
||||
def get_orders(self) -> List[Order]:
|
||||
return []
|
||||
|
||||
def get_account_info(self) -> AccountInfo:
|
||||
return AccountInfo(10000, 10000, 0, 10000, 100, "USD")
|
||||
|
||||
def get_trade_history(self, days: int = 30) -> List[Dict]:
|
||||
return []
|
||||
|
||||
class HSBCIndonesiaBroker(BaseBroker):
|
||||
"""
|
||||
HSBC Indonesia - International bank with trading platform
|
||||
Good for forex and international markets
|
||||
"""
|
||||
|
||||
def __init__(self, demo: bool = True):
|
||||
super().__init__("HSBC Indonesia")
|
||||
self.demo = demo
|
||||
|
||||
self.supported_symbols = [
|
||||
# Major currencies
|
||||
'EURUSD', 'GBPUSD', 'USDJPY', 'USDCHF', 'AUDUSD', 'USDCAD',
|
||||
|
||||
# Asian currencies (HSBC specialty)
|
||||
'USDIDR', 'USDSGD', 'USDHKD', 'USDKRW', 'USDCNY', 'USDTHB',
|
||||
'USDMYR', 'USDPHP', 'USDVND',
|
||||
|
||||
# Cross currencies
|
||||
'EURIDR', 'GBPIDR', 'AUDIDR', 'JPYIDR', 'SGDIDR',
|
||||
|
||||
# Precious metals
|
||||
'XAUUSD', 'XAGUSD'
|
||||
]
|
||||
|
||||
def connect(self, credentials: Dict) -> bool:
|
||||
self.is_connected = True
|
||||
return True
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
self.is_connected = False
|
||||
return True
|
||||
|
||||
def get_symbols(self) -> List[str]:
|
||||
return self.supported_symbols
|
||||
|
||||
def get_market_data(self, symbol: str, timeframe: Timeframe, count: int = 500) -> pd.DataFrame:
|
||||
dates = pd.date_range(end=datetime.now(), periods=count, freq='1h')
|
||||
base_price = 15400 if 'IDR' in symbol else 1.0850 if 'EUR' in symbol else 100
|
||||
returns = np.random.randn(count) * 0.008
|
||||
prices = base_price * (1 + returns).cumprod()
|
||||
return pd.DataFrame({
|
||||
'time': dates, 'open': prices, 'high': prices * 1.001,
|
||||
'low': prices * 0.999, 'close': prices, 'volume': np.random.randint(500, 2000, count)
|
||||
})
|
||||
|
||||
def get_current_price(self, symbol: str) -> Dict[str, float]:
|
||||
df = self.get_market_data(symbol, Timeframe.M1, 1)
|
||||
if not df.empty:
|
||||
price = df.iloc[-1]['close']
|
||||
return {"bid": price - 0.0002, "ask": price + 0.0002}
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
|
||||
def place_order(self, symbol: str, order_type: OrderType, side: str, size: float,
|
||||
price: Optional[float] = None, stop_loss: Optional[float] = None,
|
||||
take_profit: Optional[float] = None) -> Order:
|
||||
order = Order(str(int(time.time())), symbol, order_type, side.lower(), size, price)
|
||||
order.status = OrderStatus.FILLED
|
||||
return order
|
||||
|
||||
def cancel_order(self, order_id: str) -> bool:
|
||||
return True
|
||||
|
||||
def get_positions(self) -> List[Position]:
|
||||
return []
|
||||
|
||||
def get_orders(self) -> List[Order]:
|
||||
return []
|
||||
|
||||
def get_account_info(self) -> AccountInfo:
|
||||
return AccountInfo(10000, 10000, 0, 10000, 100, "USD")
|
||||
|
||||
def get_trade_history(self, days: int = 30) -> List[Dict]:
|
||||
return []
|
||||
|
||||
# Factory function for Indonesian brokers
|
||||
def create_indonesian_broker(broker_name: str, demo: bool = True) -> BaseBroker:
|
||||
"""Create Indonesian broker instance"""
|
||||
brokers = {
|
||||
'indopremier': IndopremierBroker,
|
||||
'xm_indonesia': XMIndonesiaBroker,
|
||||
'octafx_indonesia': OctaFXIndonesiaBroker,
|
||||
'hsbc_indonesia': HSBCIndonesiaBroker
|
||||
}
|
||||
|
||||
broker_class = brokers.get(broker_name.lower())
|
||||
if broker_class:
|
||||
return broker_class(demo=demo)
|
||||
else:
|
||||
raise ValueError(f"Unknown Indonesian broker: {broker_name}")
|
||||
|
||||
# Indonesian market information
|
||||
INDONESIAN_MARKET_INFO = {
|
||||
'market_hours': {
|
||||
'idx_stocks': 'Monday-Friday 09:00-16:00 WIB (GMT+7)',
|
||||
'forex_local': '24/5 (follows global forex)',
|
||||
'commodities': '24/5 (follows global commodities)'
|
||||
},
|
||||
'popular_stocks': {
|
||||
'BBCA.JK': 'Bank Central Asia - Largest private bank',
|
||||
'BBRI.JK': 'Bank Rakyat Indonesia - State-owned bank',
|
||||
'BMRI.JK': 'Bank Mandiri - Largest bank by assets',
|
||||
'TLKM.JK': 'Telkom Indonesia - Telecom giant',
|
||||
'ASII.JK': 'Astra International - Automotive conglomerate',
|
||||
'UNVR.JK': 'Unilever Indonesia - Consumer goods',
|
||||
'ICBP.JK': 'Indofood CBP - Food and beverages',
|
||||
'GGRM.JK': 'Gudang Garam - Cigarette manufacturer',
|
||||
'HMSP.JK': 'HM Sampoerna - Tobacco company'
|
||||
},
|
||||
'currency_info': {
|
||||
'base_currency': 'IDR (Indonesian Rupiah)',
|
||||
'typical_usd_idr': '15,000-16,000 IDR per USD',
|
||||
'volatility': 'Moderate, influenced by commodity prices'
|
||||
},
|
||||
'regulatory_info': {
|
||||
'regulator': 'OJK (Otoritas Jasa Keuangan)',
|
||||
'stock_exchange': 'IDX (Indonesia Stock Exchange)',
|
||||
'trading_lot': '100 shares minimum for most stocks'
|
||||
}
|
||||
}
|
||||
@@ -1,491 +0,0 @@
|
||||
# core/brokers/interactive_brokers.py
|
||||
"""
|
||||
Interactive Brokers Integration for QuantumBotX
|
||||
Professional-grade multi-asset trading platform
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from .base_broker import (
|
||||
BaseBroker, OrderType, OrderStatus, Timeframe,
|
||||
Position, Order, AccountInfo
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class InteractiveBrokersBroker(BaseBroker):
|
||||
"""
|
||||
Interactive Brokers (IBKR) implementation using TWS API.
|
||||
Supports stocks, forex, futures, options, and more.
|
||||
"""
|
||||
|
||||
def __init__(self, paper_trading: bool = True):
|
||||
super().__init__("Interactive Brokers")
|
||||
self.paper_trading = paper_trading
|
||||
self.ib_app = None
|
||||
self.client_id = 1 # Unique client ID
|
||||
self.port = 7497 if paper_trading else 7496 # Paper vs Live port
|
||||
self.host = "127.0.0.1"
|
||||
self.is_connected_flag = False
|
||||
|
||||
# Data storage
|
||||
self.positions_data = {}
|
||||
self.orders_data = {}
|
||||
self.account_data = {}
|
||||
self.market_data_cache = {}
|
||||
|
||||
# Timeframe mapping (IB uses specific duration/bar size combinations)
|
||||
self.timeframe_map = {
|
||||
Timeframe.M1: ("1 D", "1 min"), # 1 day of 1-minute bars
|
||||
Timeframe.M5: ("5 D", "5 mins"), # 5 days of 5-minute bars
|
||||
Timeframe.M15: ("10 D", "15 mins"), # 10 days of 15-minute bars
|
||||
Timeframe.M30: ("1 M", "30 mins"), # 1 month of 30-minute bars
|
||||
Timeframe.H1: ("1 M", "1 hour"), # 1 month of 1-hour bars
|
||||
Timeframe.H4: ("3 M", "4 hours"), # 3 months of 4-hour bars
|
||||
Timeframe.D1: ("1 Y", "1 day"), # 1 year of daily bars
|
||||
}
|
||||
|
||||
def connect(self, credentials: Dict) -> bool:
|
||||
"""
|
||||
Connect to Interactive Brokers TWS/Gateway
|
||||
credentials: {"host": "127.0.0.1", "port": 7497, "client_id": 1}
|
||||
"""
|
||||
try:
|
||||
# Import here to avoid dependency issues if not installed
|
||||
from ibapi.client import EClient
|
||||
from ibapi.wrapper import EWrapper
|
||||
from ibapi.contract import Contract
|
||||
|
||||
# Override connection parameters if provided
|
||||
self.host = credentials.get("host", self.host)
|
||||
self.port = credentials.get("port", self.port)
|
||||
self.client_id = credentials.get("client_id", self.client_id)
|
||||
|
||||
# Create IB App class that combines EClient and EWrapper
|
||||
class IBApp(EWrapper, EClient):
|
||||
def __init__(self, broker_instance):
|
||||
EClient.__init__(self, self)
|
||||
self.broker = broker_instance
|
||||
self.next_order_id = None
|
||||
|
||||
def nextValidId(self, orderId: int):
|
||||
"""Callback when connection is established"""
|
||||
self.next_order_id = orderId
|
||||
self.broker.is_connected_flag = True
|
||||
logger.info(f"IB connection established. Next order ID: {orderId}")
|
||||
|
||||
def accountSummary(self, reqId: int, account: str, tag: str, value: str, currency: str):
|
||||
"""Account summary callback"""
|
||||
if account not in self.broker.account_data:
|
||||
self.broker.account_data[account] = {}
|
||||
self.broker.account_data[account][tag] = {
|
||||
'value': value,
|
||||
'currency': currency
|
||||
}
|
||||
|
||||
def position(self, account: str, contract, position: float, avgCost: float):
|
||||
"""Position callback"""
|
||||
symbol = contract.symbol
|
||||
self.broker.positions_data[symbol] = {
|
||||
'account': account,
|
||||
'symbol': symbol,
|
||||
'position': position,
|
||||
'avg_cost': avgCost,
|
||||
'contract': contract
|
||||
}
|
||||
|
||||
def openOrder(self, orderId, contract, order, orderState):
|
||||
"""Open order callback"""
|
||||
self.broker.orders_data[orderId] = {
|
||||
'order_id': orderId,
|
||||
'contract': contract,
|
||||
'order': order,
|
||||
'state': orderState
|
||||
}
|
||||
|
||||
def historicalData(self, reqId, bar):
|
||||
"""Historical data callback"""
|
||||
if reqId not in self.broker.market_data_cache:
|
||||
self.broker.market_data_cache[reqId] = []
|
||||
|
||||
self.broker.market_data_cache[reqId].append({
|
||||
'date': bar.date,
|
||||
'open': bar.open,
|
||||
'high': bar.high,
|
||||
'low': bar.low,
|
||||
'close': bar.close,
|
||||
'volume': bar.volume
|
||||
})
|
||||
|
||||
def error(self, reqId, errorCode, errorString, advancedOrderRejectJson=""):
|
||||
"""Error callback"""
|
||||
logger.error(f"IB Error {errorCode}: {errorString}")
|
||||
|
||||
# Create and connect IB app
|
||||
self.ib_app = IBApp(self)
|
||||
self.ib_app.connect(self.host, self.port, self.client_id)
|
||||
|
||||
# Start message processing in separate thread
|
||||
def run_loop():
|
||||
self.ib_app.run()
|
||||
|
||||
api_thread = threading.Thread(target=run_loop, daemon=True)
|
||||
api_thread.start()
|
||||
|
||||
# Wait for connection
|
||||
timeout = 10 # 10 seconds timeout
|
||||
for _ in range(timeout * 10): # Check every 0.1 seconds
|
||||
if self.is_connected_flag:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
if self.is_connected_flag:
|
||||
self.is_connected = True
|
||||
|
||||
# Request account summary
|
||||
self.ib_app.reqAccountSummary(1, "All", "$LEDGER")
|
||||
time.sleep(2) # Wait for data
|
||||
|
||||
# Load supported symbols (simplified list)
|
||||
self.supported_symbols = [
|
||||
# Forex
|
||||
'EUR.USD', 'GBP.USD', 'USD.JPY', 'USD.CHF', 'AUD.USD', 'USD.CAD',
|
||||
# Stocks
|
||||
'AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN', 'META',
|
||||
# Futures
|
||||
'ES', 'NQ', 'YM', 'RTY', # Stock index futures
|
||||
'GC', 'SI', 'CL', # Commodity futures
|
||||
]
|
||||
|
||||
logger.info(f"Connected to Interactive Brokers {'Paper' if self.paper_trading else 'Live'}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to establish IB connection within timeout")
|
||||
return False
|
||||
|
||||
except ImportError:
|
||||
logger.error("ibapi package not installed. Install with: pip install ibapi")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Interactive Brokers: {e}")
|
||||
self.is_connected = False
|
||||
return False
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
"""Disconnect from Interactive Brokers"""
|
||||
if self.ib_app:
|
||||
self.ib_app.disconnect()
|
||||
self.is_connected = False
|
||||
self.is_connected_flag = False
|
||||
logger.info("Disconnected from Interactive Brokers")
|
||||
return True
|
||||
|
||||
def get_symbols(self) -> List[str]:
|
||||
"""Get list of available trading symbols"""
|
||||
return self.supported_symbols
|
||||
|
||||
def _create_contract(self, symbol: str) -> 'Contract':
|
||||
"""Create IB Contract object for symbol"""
|
||||
from ibapi.contract import Contract
|
||||
|
||||
contract = Contract()
|
||||
|
||||
# Determine contract type based on symbol format
|
||||
if '.' in symbol: # Forex (EUR.USD format)
|
||||
base, quote = symbol.split('.')
|
||||
contract.symbol = base
|
||||
contract.secType = "CASH"
|
||||
contract.currency = quote
|
||||
contract.exchange = "IDEALPRO"
|
||||
elif symbol in ['ES', 'NQ', 'YM', 'RTY', 'GC', 'SI', 'CL']: # Futures
|
||||
contract.symbol = symbol
|
||||
contract.secType = "FUT"
|
||||
contract.exchange = "CME" # Simplified
|
||||
contract.lastTradeDateOrContractMonth = "202412" # Would need dynamic
|
||||
else: # Stocks
|
||||
contract.symbol = symbol
|
||||
contract.secType = "STK"
|
||||
contract.currency = "USD"
|
||||
contract.exchange = "SMART"
|
||||
|
||||
return contract
|
||||
|
||||
def get_market_data(self, symbol: str, timeframe: Timeframe, count: int = 500) -> pd.DataFrame:
|
||||
"""Get OHLCV market data from Interactive Brokers"""
|
||||
if not self.is_connected:
|
||||
raise Exception("Not connected to Interactive Brokers")
|
||||
|
||||
try:
|
||||
contract = self._create_contract(symbol)
|
||||
duration, bar_size = self.timeframe_map[timeframe]
|
||||
|
||||
# Request historical data
|
||||
req_id = int(time.time()) # Unique request ID
|
||||
self.market_data_cache[req_id] = []
|
||||
|
||||
self.ib_app.reqHistoricalData(
|
||||
req_id, contract, "", duration, bar_size, "TRADES", 1, 1, False, []
|
||||
)
|
||||
|
||||
# Wait for data
|
||||
timeout = 10
|
||||
for _ in range(timeout * 10):
|
||||
if req_id in self.market_data_cache and len(self.market_data_cache[req_id]) > 0:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
# Convert to DataFrame
|
||||
data = self.market_data_cache.get(req_id, [])
|
||||
if not data:
|
||||
return pd.DataFrame()
|
||||
|
||||
df_data = []
|
||||
for bar in data:
|
||||
# Parse IB date format
|
||||
try:
|
||||
if len(bar['date']) == 8: # Daily format: 20231201
|
||||
date_obj = datetime.strptime(bar['date'], '%Y%m%d')
|
||||
else: # Intraday format: 20231201 10:30:00
|
||||
date_obj = datetime.strptime(bar['date'], '%Y%m%d %H:%M:%S')
|
||||
except:
|
||||
date_obj = datetime.now()
|
||||
|
||||
df_data.append({
|
||||
'time': date_obj,
|
||||
'open': bar['open'],
|
||||
'high': bar['high'],
|
||||
'low': bar['low'],
|
||||
'close': bar['close'],
|
||||
'volume': bar['volume']
|
||||
})
|
||||
|
||||
# Clean up cache
|
||||
del self.market_data_cache[req_id]
|
||||
|
||||
return pd.DataFrame(df_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get IB market data for {symbol}: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_current_price(self, symbol: str) -> Dict[str, float]:
|
||||
"""Get current bid/ask prices"""
|
||||
if not self.is_connected:
|
||||
raise Exception("Not connected to Interactive Brokers")
|
||||
|
||||
try:
|
||||
# IB requires market data subscription for real-time prices
|
||||
# For demo purposes, return last close price as both bid/ask
|
||||
# In real implementation, would use reqMktData
|
||||
df = self.get_market_data(symbol, Timeframe.M1, 1)
|
||||
if not df.empty:
|
||||
last_price = df.iloc[-1]['close']
|
||||
return {"bid": last_price - 0.0001, "ask": last_price + 0.0001}
|
||||
else:
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get IB current price for {symbol}: {e}")
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
|
||||
def place_order(self, symbol: str, order_type: OrderType, side: str,
|
||||
size: float, price: Optional[float] = None,
|
||||
stop_loss: Optional[float] = None,
|
||||
take_profit: Optional[float] = None) -> Order:
|
||||
"""Place a trading order on Interactive Brokers"""
|
||||
if not self.is_connected:
|
||||
raise Exception("Not connected to Interactive Brokers")
|
||||
|
||||
try:
|
||||
from ibapi.order import Order as IBOrder
|
||||
|
||||
contract = self._create_contract(symbol)
|
||||
|
||||
# Create IB order
|
||||
ib_order = IBOrder()
|
||||
ib_order.action = "BUY" if side.lower() == "buy" else "SELL"
|
||||
ib_order.totalQuantity = size
|
||||
|
||||
# Set order type
|
||||
if order_type in [OrderType.MARKET_BUY, OrderType.MARKET_SELL]:
|
||||
ib_order.orderType = "MKT"
|
||||
elif order_type in [OrderType.LIMIT_BUY, OrderType.LIMIT_SELL]:
|
||||
ib_order.orderType = "LMT"
|
||||
ib_order.lmtPrice = price
|
||||
|
||||
# Get next order ID
|
||||
if not self.ib_app.next_order_id:
|
||||
logger.error("No valid order ID available")
|
||||
raise Exception("No valid order ID")
|
||||
|
||||
order_id = self.ib_app.next_order_id
|
||||
self.ib_app.next_order_id += 1
|
||||
|
||||
# Place order
|
||||
self.ib_app.placeOrder(order_id, contract, ib_order)
|
||||
|
||||
# Create Order object
|
||||
order = Order(
|
||||
order_id=str(order_id),
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side.lower(),
|
||||
size=size,
|
||||
price=price
|
||||
)
|
||||
|
||||
order.status = OrderStatus.PENDING
|
||||
logger.info(f"IB order placed: {order_id} for {symbol}")
|
||||
return order
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place IB order: {e}")
|
||||
order = Order(
|
||||
order_id="failed",
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side.lower(),
|
||||
size=size,
|
||||
price=price
|
||||
)
|
||||
order.status = OrderStatus.REJECTED
|
||||
return order
|
||||
|
||||
def cancel_order(self, order_id: str) -> bool:
|
||||
"""Cancel an existing order"""
|
||||
if not self.is_connected:
|
||||
return False
|
||||
|
||||
try:
|
||||
self.ib_app.cancelOrder(int(order_id))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cancel IB order {order_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_positions(self) -> List[Position]:
|
||||
"""Get all open positions"""
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Request positions
|
||||
self.ib_app.reqPositions()
|
||||
time.sleep(2) # Wait for data
|
||||
|
||||
positions = []
|
||||
for symbol, pos_data in self.positions_data.items():
|
||||
if pos_data['position'] != 0: # Only non-zero positions
|
||||
position = Position(
|
||||
symbol=symbol,
|
||||
side='long' if pos_data['position'] > 0 else 'short',
|
||||
size=abs(pos_data['position']),
|
||||
entry_price=pos_data['avg_cost'],
|
||||
current_price=pos_data['avg_cost'], # Would need market price
|
||||
unrealized_pnl=0.0 # Would need calculation
|
||||
)
|
||||
positions.append(position)
|
||||
|
||||
return positions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get IB positions: {e}")
|
||||
return []
|
||||
|
||||
def get_orders(self) -> List[Order]:
|
||||
"""Get all pending orders"""
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Request open orders
|
||||
self.ib_app.reqOpenOrders()
|
||||
time.sleep(2) # Wait for data
|
||||
|
||||
orders = []
|
||||
for order_id, order_data in self.orders_data.items():
|
||||
order = Order(
|
||||
order_id=str(order_id),
|
||||
symbol=order_data['contract'].symbol,
|
||||
order_type=OrderType.LIMIT_BUY, # Simplified
|
||||
side=order_data['order'].action.lower(),
|
||||
size=order_data['order'].totalQuantity,
|
||||
price=getattr(order_data['order'], 'lmtPrice', None)
|
||||
)
|
||||
order.status = OrderStatus.PENDING
|
||||
orders.append(order)
|
||||
|
||||
return orders
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get IB orders: {e}")
|
||||
return []
|
||||
|
||||
def get_account_info(self) -> AccountInfo:
|
||||
"""Get account information"""
|
||||
if not self.is_connected:
|
||||
return AccountInfo(0, 0, 0, 0, 0, "USD")
|
||||
|
||||
try:
|
||||
# Use cached account data
|
||||
account_data = list(self.account_data.values())[0] if self.account_data else {}
|
||||
|
||||
net_liquidation = float(account_data.get('NetLiquidation', {}).get('value', 0))
|
||||
total_cash = float(account_data.get('TotalCashValue', {}).get('value', 0))
|
||||
buying_power = float(account_data.get('BuyingPower', {}).get('value', 0))
|
||||
|
||||
return AccountInfo(
|
||||
balance=total_cash,
|
||||
equity=net_liquidation,
|
||||
margin=0.0, # Would need calculation
|
||||
free_margin=buying_power,
|
||||
margin_level=100.0, # Would need calculation
|
||||
currency="USD"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get IB account info: {e}")
|
||||
return AccountInfo(0, 0, 0, 0, 0, "USD")
|
||||
|
||||
def get_trade_history(self, days: int = 30) -> List[Dict]:
|
||||
"""Get trade history"""
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
try:
|
||||
# IB trade history would require execution reports
|
||||
# For now, return empty list
|
||||
logger.warning("IB trade history not implemented - requires execution report handling")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get IB trade history: {e}")
|
||||
return []
|
||||
|
||||
def normalize_symbol(self, symbol: str) -> str:
|
||||
"""Normalize symbol format for Interactive Brokers"""
|
||||
# Convert common formats to IB format
|
||||
symbol = symbol.upper()
|
||||
|
||||
# Forex: EURUSD -> EUR.USD
|
||||
forex_pairs = ['EURUSD', 'GBPUSD', 'USDJPY', 'USDCHF', 'AUDUSD', 'USDCAD']
|
||||
for pair in forex_pairs:
|
||||
if symbol == pair:
|
||||
return f"{pair[:3]}.{pair[3:]}"
|
||||
|
||||
return symbol
|
||||
|
||||
def is_market_open(self) -> bool:
|
||||
"""Check if markets are open (simplified)"""
|
||||
now = datetime.now()
|
||||
# US market hours: weekdays, roughly 9:30 AM - 4:00 PM ET
|
||||
return now.weekday() < 5 # Simplified
|
||||
|
||||
# Convenience function
|
||||
def create_ib_broker(paper_trading: bool = True) -> InteractiveBrokersBroker:
|
||||
"""Create an Interactive Brokers broker instance"""
|
||||
return InteractiveBrokersBroker(paper_trading=paper_trading)
|
||||
@@ -1,449 +0,0 @@
|
||||
# core/brokers/tradingview_broker.py
|
||||
"""
|
||||
TradingView Integration for QuantumBotX
|
||||
Social trading platform with Pine Script integration
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
import websocket
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from .base_broker import (
|
||||
BaseBroker, OrderType, OrderStatus, Timeframe,
|
||||
Position, Order, AccountInfo
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TradingViewBroker(BaseBroker):
|
||||
"""
|
||||
TradingView integration for QuantumBotX.
|
||||
|
||||
Note: This is a conceptual implementation as TradingView doesn't have
|
||||
a traditional trading API. In practice, this would work through:
|
||||
1. Webhook signals from TradingView alerts
|
||||
2. Screen scraping (not recommended)
|
||||
3. Third-party integrations
|
||||
|
||||
This implementation shows how it would work architecturally.
|
||||
"""
|
||||
|
||||
def __init__(self, paper_trading: bool = True):
|
||||
super().__init__("TradingView")
|
||||
self.paper_trading = paper_trading
|
||||
self.session = requests.Session()
|
||||
self.websocket = None
|
||||
self.webhook_server = None
|
||||
|
||||
# TradingView doesn't provide direct API access
|
||||
# This would work through webhook alerts
|
||||
self.base_url = "https://www.tradingview.com"
|
||||
|
||||
# Simulated data for demo purposes
|
||||
self.portfolio = {}
|
||||
self.pending_orders = {}
|
||||
self.trade_history = []
|
||||
self.current_capital = 10000.0
|
||||
|
||||
# Timeframe mapping
|
||||
self.timeframe_map = {
|
||||
Timeframe.M1: "1",
|
||||
Timeframe.M5: "5",
|
||||
Timeframe.M15: "15",
|
||||
Timeframe.M30: "30",
|
||||
Timeframe.H1: "60",
|
||||
Timeframe.H4: "240",
|
||||
Timeframe.D1: "1D"
|
||||
}
|
||||
|
||||
def connect(self, credentials: Dict) -> bool:
|
||||
"""
|
||||
Connect to TradingView (conceptual)
|
||||
credentials: {"username": "...", "password": "...", "webhook_secret": "..."}
|
||||
"""
|
||||
try:
|
||||
username = credentials.get("username")
|
||||
password = credentials.get("password")
|
||||
webhook_secret = credentials.get("webhook_secret")
|
||||
|
||||
if not all([username, webhook_secret]):
|
||||
logger.error("TradingView username and webhook_secret are required")
|
||||
return False
|
||||
|
||||
# In real implementation, would set up webhook server
|
||||
self._setup_webhook_server(webhook_secret)
|
||||
|
||||
self.is_connected = True
|
||||
|
||||
# Popular tradingview symbols
|
||||
self.supported_symbols = [
|
||||
# Forex
|
||||
'EURUSD', 'GBPUSD', 'USDJPY', 'USDCHF', 'AUDUSD', 'USDCAD',
|
||||
'NZDUSD', 'EURGBP', 'EURJPY', 'GBPJPY',
|
||||
# Crypto
|
||||
'BTCUSD', 'ETHUSD', 'ADAUSD', 'SOLUSD', 'DOGEUSD',
|
||||
# Stocks
|
||||
'AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN', 'META', 'NVDA',
|
||||
# Commodities
|
||||
'XAUUSD', 'XAGUSD', 'USOIL', 'UKOIL',
|
||||
# Indices
|
||||
'SPX', 'DJI', 'NDX', 'RUT'
|
||||
]
|
||||
|
||||
logger.info(f"Connected to TradingView {'Paper' if self.paper_trading else 'Live'}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to TradingView: {e}")
|
||||
self.is_connected = False
|
||||
return False
|
||||
|
||||
def _setup_webhook_server(self, webhook_secret: str):
|
||||
"""Setup webhook server to receive TradingView alerts"""
|
||||
try:
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
webhook_app = Flask(__name__)
|
||||
|
||||
@webhook_app.route('/tradingview-webhook', methods=['POST'])
|
||||
def handle_webhook():
|
||||
try:
|
||||
# Verify webhook secret
|
||||
received_secret = request.headers.get('X-Webhook-Secret')
|
||||
if received_secret != webhook_secret:
|
||||
return jsonify({'error': 'Invalid webhook secret'}), 401
|
||||
|
||||
# Parse alert data
|
||||
alert_data = request.get_json()
|
||||
self._process_tradingview_alert(alert_data)
|
||||
|
||||
return jsonify({'status': 'success'}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Webhook error: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# Run webhook server in background thread
|
||||
def run_webhook():
|
||||
webhook_app.run(host='0.0.0.0', port=5001, debug=False)
|
||||
|
||||
webhook_thread = threading.Thread(target=run_webhook, daemon=True)
|
||||
webhook_thread.start()
|
||||
|
||||
logger.info("TradingView webhook server started on port 5001")
|
||||
|
||||
except ImportError:
|
||||
logger.warning("Flask not available for webhook server")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to setup webhook server: {e}")
|
||||
|
||||
def _process_tradingview_alert(self, alert_data: Dict):
|
||||
"""Process incoming TradingView alert"""
|
||||
try:
|
||||
# Expected alert format:
|
||||
# {
|
||||
# "symbol": "EURUSD",
|
||||
# "action": "buy" or "sell",
|
||||
# "price": 1.0850,
|
||||
# "stop_loss": 1.0800,
|
||||
# "take_profit": 1.0900,
|
||||
# "quantity": 1.0,
|
||||
# "strategy": "My Strategy"
|
||||
# }
|
||||
|
||||
symbol = alert_data.get('symbol')
|
||||
action = alert_data.get('action', '').lower()
|
||||
price = float(alert_data.get('price', 0))
|
||||
quantity = float(alert_data.get('quantity', 1.0))
|
||||
|
||||
if action in ['buy', 'sell'] and symbol and price > 0:
|
||||
# Execute the trade
|
||||
order_type = OrderType.MARKET_BUY if action == 'buy' else OrderType.MARKET_SELL
|
||||
|
||||
order = self.place_order(
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=action,
|
||||
size=quantity,
|
||||
price=price,
|
||||
stop_loss=alert_data.get('stop_loss'),
|
||||
take_profit=alert_data.get('take_profit')
|
||||
)
|
||||
|
||||
logger.info(f"TradingView alert processed: {action} {quantity} {symbol} at {price}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process TradingView alert: {e}")
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
"""Disconnect from TradingView"""
|
||||
self.is_connected = False
|
||||
logger.info("Disconnected from TradingView")
|
||||
return True
|
||||
|
||||
def get_symbols(self) -> List[str]:
|
||||
"""Get list of available trading symbols"""
|
||||
return self.supported_symbols
|
||||
|
||||
def get_market_data(self, symbol: str, timeframe: Timeframe, count: int = 500) -> pd.DataFrame:
|
||||
"""
|
||||
Get market data from TradingView
|
||||
Note: This would require web scraping or third-party API
|
||||
"""
|
||||
try:
|
||||
# For demo purposes, generate simulated data
|
||||
# In real implementation, would scrape TradingView charts or use third-party API
|
||||
|
||||
logger.warning("TradingView market data: Using simulated data (real implementation would require scraping)")
|
||||
|
||||
# Generate simulated price data
|
||||
dates = pd.date_range(end=datetime.now(), periods=count, freq='1h')
|
||||
|
||||
# Base prices for different symbols
|
||||
base_prices = {
|
||||
'EURUSD': 1.0850, 'GBPUSD': 1.2650, 'USDJPY': 148.50,
|
||||
'BTCUSD': 42000, 'ETHUSD': 2500, 'AAPL': 190.0,
|
||||
'XAUUSD': 2020.0, 'SPX': 4500.0
|
||||
}
|
||||
|
||||
base_price = base_prices.get(symbol, 100.0)
|
||||
|
||||
# Generate price movements
|
||||
returns = np.random.randn(count) * 0.01 # 1% volatility
|
||||
prices = base_price * (1 + returns).cumprod()
|
||||
|
||||
df = pd.DataFrame({
|
||||
'time': dates,
|
||||
'open': prices,
|
||||
'high': prices * (1 + np.random.uniform(0, 0.005, count)),
|
||||
'low': prices * (1 - np.random.uniform(0, 0.005, count)),
|
||||
'close': prices,
|
||||
'volume': np.random.randint(1000, 10000, count)
|
||||
})
|
||||
|
||||
# Ensure OHLC integrity
|
||||
df['high'] = df[['high', 'close', 'open']].max(axis=1)
|
||||
df['low'] = df[['low', 'close', 'open']].min(axis=1)
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get TradingView market data for {symbol}: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_current_price(self, symbol: str) -> Dict[str, float]:
|
||||
"""Get current bid/ask prices"""
|
||||
try:
|
||||
# In real implementation, would scrape TradingView or use websocket
|
||||
df = self.get_market_data(symbol, Timeframe.M1, 1)
|
||||
if not df.empty:
|
||||
last_price = df.iloc[-1]['close']
|
||||
spread = last_price * 0.0001 # Typical spread
|
||||
return {
|
||||
"bid": last_price - spread/2,
|
||||
"ask": last_price + spread/2
|
||||
}
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get TradingView current price for {symbol}: {e}")
|
||||
return {"bid": 0.0, "ask": 0.0}
|
||||
|
||||
def place_order(self, symbol: str, order_type: OrderType, side: str,
|
||||
size: float, price: Optional[float] = None,
|
||||
stop_loss: Optional[float] = None,
|
||||
take_profit: Optional[float] = None) -> Order:
|
||||
"""
|
||||
Place order (simulated for TradingView)
|
||||
In practice, this would trigger through connected broker
|
||||
"""
|
||||
try:
|
||||
order_id = str(int(time.time()))
|
||||
|
||||
# Simulate order execution
|
||||
if order_type in [OrderType.MARKET_BUY, OrderType.MARKET_SELL]:
|
||||
current_price = self.get_current_price(symbol)
|
||||
execution_price = current_price['ask'] if side.lower() == 'buy' else current_price['bid']
|
||||
else:
|
||||
execution_price = price
|
||||
|
||||
# Create order
|
||||
order = Order(
|
||||
order_id=order_id,
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side.lower(),
|
||||
size=size,
|
||||
price=execution_price
|
||||
)
|
||||
|
||||
# Simulate immediate execution for market orders
|
||||
if order_type in [OrderType.MARKET_BUY, OrderType.MARKET_SELL]:
|
||||
order.status = OrderStatus.FILLED
|
||||
order.filled_size = size
|
||||
order.avg_fill_price = execution_price
|
||||
|
||||
# Update portfolio
|
||||
if symbol not in self.portfolio:
|
||||
self.portfolio[symbol] = {'long': 0, 'short': 0, 'avg_price': 0}
|
||||
|
||||
if side.lower() == 'buy':
|
||||
self.portfolio[symbol]['long'] += size
|
||||
else:
|
||||
self.portfolio[symbol]['short'] += size
|
||||
|
||||
# Add to trade history
|
||||
self.trade_history.append({
|
||||
'time': datetime.now(),
|
||||
'symbol': symbol,
|
||||
'side': side.lower(),
|
||||
'size': size,
|
||||
'price': execution_price,
|
||||
'order_id': order_id
|
||||
})
|
||||
|
||||
logger.info(f"TradingView simulated order executed: {side} {size} {symbol} at {execution_price}")
|
||||
else:
|
||||
order.status = OrderStatus.PENDING
|
||||
self.pending_orders[order_id] = order
|
||||
|
||||
return order
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place TradingView order: {e}")
|
||||
order = Order(
|
||||
order_id="failed",
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side.lower(),
|
||||
size=size,
|
||||
price=price
|
||||
)
|
||||
order.status = OrderStatus.REJECTED
|
||||
return order
|
||||
|
||||
def cancel_order(self, order_id: str) -> bool:
|
||||
"""Cancel an existing order"""
|
||||
try:
|
||||
if order_id in self.pending_orders:
|
||||
del self.pending_orders[order_id]
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cancel TradingView order {order_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_positions(self) -> List[Position]:
|
||||
"""Get all open positions"""
|
||||
try:
|
||||
positions = []
|
||||
|
||||
for symbol, pos_data in self.portfolio.items():
|
||||
long_size = pos_data['long']
|
||||
short_size = pos_data['short']
|
||||
net_size = long_size - short_size
|
||||
|
||||
if net_size != 0:
|
||||
current_price_data = self.get_current_price(symbol)
|
||||
current_price = current_price_data['bid'] if net_size > 0 else current_price_data['ask']
|
||||
|
||||
position = Position(
|
||||
symbol=symbol,
|
||||
side='long' if net_size > 0 else 'short',
|
||||
size=abs(net_size),
|
||||
entry_price=pos_data.get('avg_price', current_price),
|
||||
current_price=current_price,
|
||||
unrealized_pnl=0.0 # Would calculate based on entry vs current
|
||||
)
|
||||
positions.append(position)
|
||||
|
||||
return positions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get TradingView positions: {e}")
|
||||
return []
|
||||
|
||||
def get_orders(self) -> List[Order]:
|
||||
"""Get all pending orders"""
|
||||
return list(self.pending_orders.values())
|
||||
|
||||
def get_account_info(self) -> AccountInfo:
|
||||
"""Get account information"""
|
||||
try:
|
||||
# Simulate account info
|
||||
return AccountInfo(
|
||||
balance=self.current_capital,
|
||||
equity=self.current_capital, # Simplified
|
||||
margin=0.0,
|
||||
free_margin=self.current_capital,
|
||||
margin_level=100.0,
|
||||
currency="USD"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get TradingView account info: {e}")
|
||||
return AccountInfo(0, 0, 0, 0, 0, "USD")
|
||||
|
||||
def get_trade_history(self, days: int = 30) -> List[Dict]:
|
||||
"""Get trade history"""
|
||||
try:
|
||||
cutoff_date = datetime.now() - timedelta(days=days)
|
||||
recent_trades = [
|
||||
trade for trade in self.trade_history
|
||||
if trade['time'] >= cutoff_date
|
||||
]
|
||||
return recent_trades
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get TradingView trade history: {e}")
|
||||
return []
|
||||
|
||||
def normalize_symbol(self, symbol: str) -> str:
|
||||
"""Normalize symbol format for TradingView"""
|
||||
# TradingView uses various symbol formats
|
||||
symbol = symbol.upper()
|
||||
|
||||
# Convert some common formats
|
||||
if symbol == 'XAUUSD':
|
||||
return 'GOLD'
|
||||
elif symbol == 'XAGUSD':
|
||||
return 'SILVER'
|
||||
elif symbol.endswith('USDT'):
|
||||
return symbol.replace('USDT', 'USD')
|
||||
|
||||
return symbol
|
||||
|
||||
def is_market_open(self) -> bool:
|
||||
"""TradingView shows global markets - always something open"""
|
||||
return True
|
||||
|
||||
def create_pine_script_strategy(self, strategy_code: str) -> str:
|
||||
"""
|
||||
Create a Pine Script strategy (conceptual)
|
||||
Returns strategy ID for webhook alerts
|
||||
"""
|
||||
try:
|
||||
# In real implementation, would create TradingView strategy
|
||||
# and set up webhook alerts
|
||||
|
||||
strategy_id = f"strategy_{int(time.time())}"
|
||||
|
||||
logger.info(f"Pine Script strategy created (simulated): {strategy_id}")
|
||||
logger.info("Set up TradingView alerts with webhook URL: http://your-server.com:5001/tradingview-webhook")
|
||||
|
||||
return strategy_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create Pine Script strategy: {e}")
|
||||
return ""
|
||||
|
||||
# Convenience function
|
||||
def create_tradingview_broker(paper_trading: bool = True) -> TradingViewBroker:
|
||||
"""Create a TradingView broker instance"""
|
||||
return TradingViewBroker(paper_trading=paper_trading)
|
||||
@@ -1 +0,0 @@
|
||||
# Init file for core/interfaces
|
||||
@@ -1,353 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Indonesian Market Trading Demo for QuantumBotX
|
||||
Showcasing opportunities in Indonesian financial markets
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Add the project root to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def demo_indonesian_market_overview():
|
||||
"""Overview of Indonesian trading opportunities"""
|
||||
print("🇮🇩 Indonesian Market Trading Opportunities")
|
||||
print("=" * 60)
|
||||
print("Welcome to the Indonesian Financial Markets!")
|
||||
print("=" * 60)
|
||||
|
||||
market_segments = {
|
||||
'IDX Stocks (Jakarta Stock Exchange)': {
|
||||
'description': 'Local Indonesian companies',
|
||||
'examples': ['BBCA.JK (BCA)', 'BBRI.JK (BRI)', 'TLKM.JK (Telkom)'],
|
||||
'trading_hours': '09:00-16:00 WIB (GMT+7)',
|
||||
'currency': 'IDR (Indonesian Rupiah)',
|
||||
'min_lot': '100 shares',
|
||||
'opportunities': ['Banking sector growth', 'Infrastructure development', 'Consumer goods expansion']
|
||||
},
|
||||
'USD/IDR Forex': {
|
||||
'description': 'Indonesian Rupiah currency trading',
|
||||
'examples': ['USDIDR', 'EURIDR', 'JPYIDR'],
|
||||
'trading_hours': '24/5 (Global forex hours)',
|
||||
'currency': 'IDR pairs',
|
||||
'min_lot': 'Varies by broker',
|
||||
'opportunities': ['Commodity-driven moves', 'Central bank policy', 'Tourism recovery']
|
||||
},
|
||||
'International Markets via Indonesian Brokers': {
|
||||
'description': 'Global markets through local brokers',
|
||||
'examples': ['XAUUSD', 'US stocks', 'Major forex pairs'],
|
||||
'trading_hours': 'Varies by market',
|
||||
'currency': 'USD typically',
|
||||
'min_lot': 'Standard international',
|
||||
'opportunities': ['Global diversification', 'USD income', 'Hedge against IDR']
|
||||
}
|
||||
}
|
||||
|
||||
print("\\n📊 Indonesian Market Segments:")
|
||||
for i, (segment, details) in enumerate(market_segments.items(), 1):
|
||||
print(f"\\n{i}. {segment}")
|
||||
print(f" 📝 Description: {details['description']}")
|
||||
print(f" 📈 Examples: {', '.join(details['examples'])}")
|
||||
print(f" ⏰ Hours: {details['trading_hours']}")
|
||||
print(f" 💰 Currency: {details['currency']}")
|
||||
print(f" 🎯 Opportunities: {', '.join(details['opportunities'][:2])}")
|
||||
|
||||
def demo_indonesian_brokers():
|
||||
"""Showcase Indonesian brokers with demo accounts"""
|
||||
print("\\n🏢 Indonesian Brokers with Demo Accounts")
|
||||
print("=" * 60)
|
||||
|
||||
brokers = [
|
||||
{
|
||||
'name': 'Indopremier Securities (IPOT)',
|
||||
'type': 'Local Indonesian Broker',
|
||||
'specialties': ['IDX Stocks', 'Local bonds', 'Indonesian mutual funds'],
|
||||
'demo_account': 'Yes - Full IDX access',
|
||||
'advantages': ['Local market expertise', 'IDR-based trading', 'Indonesian customer service'],
|
||||
'website': 'https://www.indopremier.com/',
|
||||
'best_for': 'Indonesian stock market and local investments'
|
||||
},
|
||||
{
|
||||
'name': 'XM Indonesia',
|
||||
'type': 'International Broker (Indonesia Office)',
|
||||
'specialties': ['Forex', 'CFDs', 'Commodities', 'Crypto CFDs'],
|
||||
'demo_account': 'Yes - $10,000 virtual',
|
||||
'advantages': ['Global markets', 'MT4/MT5 platform', 'Indonesian support'],
|
||||
'website': 'https://www.xm.com/id/',
|
||||
'best_for': 'Forex and international markets'
|
||||
},
|
||||
{
|
||||
'name': 'OctaFX Indonesia',
|
||||
'type': 'International Broker (Popular in Indonesia)',
|
||||
'specialties': ['Forex', 'Metals', 'Indices', 'Energies'],
|
||||
'demo_account': 'Yes - Unlimited time',
|
||||
'advantages': ['Tight spreads', 'Fast execution', 'Indonesian community'],
|
||||
'website': 'https://www.octafx.com/id/',
|
||||
'best_for': 'Professional forex trading'
|
||||
},
|
||||
{
|
||||
'name': 'HSBC Indonesia',
|
||||
'type': 'International Bank',
|
||||
'specialties': ['Forex', 'Asian currencies', 'Trade finance'],
|
||||
'demo_account': 'Available for qualified clients',
|
||||
'advantages': ['Banking integration', 'Asian market focus', 'Multi-currency'],
|
||||
'website': 'Contact local HSBC branch',
|
||||
'best_for': 'Currency hedging and international business'
|
||||
}
|
||||
]
|
||||
|
||||
print("\\n🎯 Recommended Brokers for Indonesian Traders:")
|
||||
for i, broker in enumerate(brokers, 1):
|
||||
print(f"\\n{i}. {broker['name']}")
|
||||
print(f" 🏢 Type: {broker['type']}")
|
||||
print(f" 📈 Specialties: {', '.join(broker['specialties'][:3])}")
|
||||
print(f" 🧪 Demo Account: {broker['demo_account']}")
|
||||
print(f" ⭐ Best For: {broker['best_for']}")
|
||||
print(f" 🌐 Website: {broker['website']}")
|
||||
|
||||
def demo_idx_stocks_trading():
|
||||
"""Demo trading Indonesian stocks"""
|
||||
print("\\n📈 IDX Stock Trading Simulation")
|
||||
print("=" * 60)
|
||||
|
||||
# Simulate some popular Indonesian stocks
|
||||
idx_stocks = [
|
||||
{'symbol': 'BBCA.JK', 'name': 'Bank Central Asia', 'price': 9150, 'sector': 'Banking'},
|
||||
{'symbol': 'BBRI.JK', 'name': 'Bank Rakyat Indonesia', 'price': 4520, 'sector': 'Banking'},
|
||||
{'symbol': 'TLKM.JK', 'name': 'Telkom Indonesia', 'price': 3280, 'sector': 'Telecommunications'},
|
||||
{'symbol': 'ASII.JK', 'name': 'Astra International', 'price': 6750, 'sector': 'Automotive'},
|
||||
{'symbol': 'UNVR.JK', 'name': 'Unilever Indonesia', 'price': 7100, 'sector': 'Consumer Goods'},
|
||||
]
|
||||
|
||||
print("\\n🏦 Popular IDX Stocks (Simulated Prices):")
|
||||
print("Symbol | Company | Price (IDR) | Sector")
|
||||
print("-" * 70)
|
||||
|
||||
total_portfolio_value = 0
|
||||
|
||||
for stock in idx_stocks:
|
||||
# Simulate small price movements
|
||||
current_price = stock['price'] * (1 + np.random.uniform(-0.02, 0.02))
|
||||
change_pct = ((current_price - stock['price']) / stock['price']) * 100
|
||||
|
||||
# Simulate trading with 1000 IDR capital per stock
|
||||
shares_affordable = int(100000 / current_price) # 100k IDR investment
|
||||
position_value = shares_affordable * current_price
|
||||
total_portfolio_value += position_value
|
||||
|
||||
color = "📈" if change_pct > 0 else "📉" if change_pct < 0 else "➡️"
|
||||
|
||||
print(f"{stock['symbol']:10} | {stock['name']:25} | {current_price:8.0f} {color} | {stock['sector']}")
|
||||
|
||||
print(f"\\n💼 Simulated Portfolio Value: {total_portfolio_value:,.0f} IDR")
|
||||
print(f"💰 Equivalent in USD: ${total_portfolio_value/15400:.2f} (assuming 1 USD = 15,400 IDR)")
|
||||
|
||||
def demo_usd_idr_trading():
|
||||
"""Demo USD/IDR forex trading"""
|
||||
print("\\n💱 USD/IDR Forex Trading Simulation")
|
||||
print("=" * 60)
|
||||
|
||||
# Current USD/IDR around 15,400
|
||||
base_rate = 15400
|
||||
|
||||
# Simulate daily USD/IDR movements
|
||||
days = 30
|
||||
dates = pd.date_range(end=datetime.now(), periods=days, freq='D')
|
||||
|
||||
# IDR volatility (typically 0.5-1% daily)
|
||||
daily_changes = np.random.randn(days) * 0.008 # 0.8% daily volatility
|
||||
rates = base_rate * (1 + daily_changes).cumprod()
|
||||
|
||||
print(f"\\n📊 USD/IDR Rate Simulation (Last {days} days):")
|
||||
print(f"Starting Rate: {base_rate:,.0f} IDR per USD")
|
||||
print(f"Ending Rate: {rates[-1]:,.0f} IDR per USD")
|
||||
print(f"Total Change: {((rates[-1] - base_rate) / base_rate) * 100:+.2f}%")
|
||||
|
||||
# Trading simulation
|
||||
position_size = 10000 # $10,000 USD position
|
||||
entry_rate = rates[0]
|
||||
exit_rate = rates[-1]
|
||||
|
||||
if rates[-1] > rates[0]: # USD strengthened
|
||||
pnl_usd = position_size * ((exit_rate - entry_rate) / entry_rate)
|
||||
direction = "USD strengthened"
|
||||
else: # USD weakened
|
||||
pnl_usd = position_size * ((exit_rate - entry_rate) / entry_rate)
|
||||
direction = "USD weakened"
|
||||
|
||||
pnl_idr = pnl_usd * exit_rate
|
||||
|
||||
print(f"\\n💹 Trading Simulation:")
|
||||
print(f"Position: Long ${position_size:,} USD vs IDR")
|
||||
print(f"Entry Rate: {entry_rate:,.0f} IDR/USD")
|
||||
print(f"Exit Rate: {exit_rate:,.0f} IDR/USD")
|
||||
print(f"Market Move: {direction}")
|
||||
print(f"P&L: ${pnl_usd:+,.2f} USD (or {pnl_idr:+,.0f} IDR)")
|
||||
|
||||
def demo_strategy_performance_indonesia():
|
||||
"""Test strategies on Indonesian markets"""
|
||||
print("\\n🤖 Strategy Performance on Indonesian Markets")
|
||||
print("=" * 60)
|
||||
|
||||
from core.brokers.indonesian_brokers import IndopremierBroker
|
||||
|
||||
# Create Indonesian broker instance
|
||||
broker = IndopremierBroker(demo=True)
|
||||
|
||||
# Test symbols
|
||||
test_symbols = [
|
||||
('BBCA.JK', 'Bank Central Asia'),
|
||||
('USDIDR', 'USD/IDR Forex'),
|
||||
('XAUIDR', 'Gold in IDR')
|
||||
]
|
||||
|
||||
print("\\n📈 Testing QuantumBotX Strategies on Indonesian Markets:")
|
||||
|
||||
for symbol, name in test_symbols:
|
||||
try:
|
||||
# Get simulated market data
|
||||
df = broker.get_market_data(symbol, broker.timeframe_map[broker.Timeframe.H1] if hasattr(broker, 'timeframe_map') else 'H1', 500)
|
||||
|
||||
if not df.empty:
|
||||
# Calculate basic metrics
|
||||
volatility = (df['close'].std() / df['close'].mean()) * 100
|
||||
price_range = f"{df['close'].min():.0f} - {df['close'].max():.0f}"
|
||||
|
||||
# Assess suitability for different strategies
|
||||
if volatility < 2:
|
||||
strategy_rec = "Bollinger Reversion (Low volatility)"
|
||||
elif volatility > 5:
|
||||
strategy_rec = "Conservative MA Crossover (High volatility)"
|
||||
else:
|
||||
strategy_rec = "QuantumBotX Hybrid (Moderate volatility)"
|
||||
|
||||
print(f"\\n📊 {symbol} ({name}):")
|
||||
print(f" Price Range: {price_range}")
|
||||
print(f" Volatility: {volatility:.1f}%")
|
||||
print(f" Recommended Strategy: {strategy_rec}")
|
||||
print(f" Data Points: {len(df)} bars")
|
||||
else:
|
||||
print(f"\\n❌ {symbol}: No data available")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\\n❌ {symbol}: Error - {e}")
|
||||
|
||||
def demo_regulatory_compliance():
|
||||
"""Indonesian regulatory information"""
|
||||
print("\\n⚖️ Indonesian Regulatory Compliance")
|
||||
print("=" * 60)
|
||||
|
||||
regulatory_info = {
|
||||
'Primary Regulator': {
|
||||
'name': 'OJK (Otoritas Jasa Keuangan)',
|
||||
'role': 'Financial Services Authority',
|
||||
'website': 'https://www.ojk.go.id/',
|
||||
'oversight': 'Banks, capital markets, insurance, pension funds'
|
||||
},
|
||||
'Stock Exchange': {
|
||||
'name': 'IDX (Indonesia Stock Exchange)',
|
||||
'location': 'Jakarta',
|
||||
'website': 'https://www.idx.co.id/',
|
||||
'trading_currency': 'Indonesian Rupiah (IDR)'
|
||||
},
|
||||
'Key Regulations': [
|
||||
'Foreign investment limits in certain sectors',
|
||||
'Tax obligations for trading profits',
|
||||
'Anti-money laundering (AML) requirements',
|
||||
'Know Your Customer (KYC) procedures'
|
||||
],
|
||||
'Tax Considerations': [
|
||||
'Capital gains tax on stock trading',
|
||||
'Forex trading taxation rules',
|
||||
'Withholding tax on foreign investments',
|
||||
'Professional trader vs investor classification'
|
||||
]
|
||||
}
|
||||
|
||||
print("\\n🏛️ Regulatory Framework:")
|
||||
print(f"Primary Regulator: {regulatory_info['Primary Regulator']['name']}")
|
||||
print(f"Stock Exchange: {regulatory_info['Stock Exchange']['name']}")
|
||||
|
||||
print("\\n⚠️ Important Considerations:")
|
||||
for consideration in regulatory_info['Key Regulations'][:3]:
|
||||
print(f" • {consideration}")
|
||||
|
||||
print("\\n💰 Tax Implications:")
|
||||
for tax_item in regulatory_info['Tax Considerations'][:3]:
|
||||
print(f" • {tax_item}")
|
||||
|
||||
print("\\n📝 Recommendation:")
|
||||
print(" • Consult with Indonesian tax advisor")
|
||||
print(" • Understand local broker regulations")
|
||||
print(" • Keep detailed trading records")
|
||||
print(" • Consider professional trader registration if applicable")
|
||||
|
||||
def main():
|
||||
"""Main Indonesian market demo"""
|
||||
print("🇮🇩 SELAMAT DATANG! Welcome to Indonesian Market Trading!")
|
||||
print("Your QuantumBotX system now supports Indonesian markets!")
|
||||
print()
|
||||
|
||||
# Run all demos
|
||||
demo_indonesian_market_overview()
|
||||
demo_indonesian_brokers()
|
||||
demo_idx_stocks_trading()
|
||||
demo_usd_idr_trading()
|
||||
demo_strategy_performance_indonesia()
|
||||
demo_regulatory_compliance()
|
||||
|
||||
print("\\n" + "=" * 60)
|
||||
print("🎯 NEXT STEPS FOR INDONESIAN TRADING")
|
||||
print("=" * 60)
|
||||
|
||||
next_steps = [
|
||||
{
|
||||
'step': '1. Choose Your Indonesian Broker',
|
||||
'recommendation': 'Start with XM Indonesia demo (easiest setup)',
|
||||
'action': 'Sign up for demo account at xm.com/id/'
|
||||
},
|
||||
{
|
||||
'step': '2. Add Indonesian Configuration',
|
||||
'recommendation': 'Update .env file with Indonesian broker credentials',
|
||||
'action': 'Add XM_INDONESIA_LOGIN and XM_INDONESIA_PASSWORD'
|
||||
},
|
||||
{
|
||||
'step': '3. Test IDX Stocks Strategy',
|
||||
'recommendation': 'Start with banking stocks (BBCA, BBRI, BMRI)',
|
||||
'action': 'Run backtests on Indonesian blue-chip stocks'
|
||||
},
|
||||
{
|
||||
'step': '4. Explore USD/IDR Trading',
|
||||
'recommendation': 'Great for Indonesian traders to earn USD',
|
||||
'action': 'Test forex strategies on USD/IDR pair'
|
||||
},
|
||||
{
|
||||
'step': '5. Regulatory Compliance',
|
||||
'recommendation': 'Understand Indonesian tax obligations',
|
||||
'action': 'Consult with local financial advisor'
|
||||
}
|
||||
]
|
||||
|
||||
for step_info in next_steps:
|
||||
print(f"\\n{step_info['step']}")
|
||||
print(f" 💡 Recommendation: {step_info['recommendation']}")
|
||||
print(f" 🎯 Action: {step_info['action']}")
|
||||
|
||||
print("\\n🎉 AMAZING OPPORTUNITY!")
|
||||
print("=" * 60)
|
||||
print("You're now building a trading system that covers:")
|
||||
print("✅ Global Forex (MT5, cTrader, XM)")
|
||||
print("✅ Cryptocurrency (Binance)")
|
||||
print("✅ US Stocks (Interactive Brokers)")
|
||||
print("✅ Social Trading (TradingView)")
|
||||
print("✅ Indonesian Markets (Local brokers)")
|
||||
print()
|
||||
print("🌏 FROM INDONESIA TO THE WORLD!")
|
||||
print("Your trading system now spans the entire globe! 🚀")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,310 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Broker Universe Demo for QuantumBotX
|
||||
Shows how to trade across all major platforms simultaneously
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
|
||||
# Add the project root to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def demo_all_brokers():
|
||||
"""Demonstrate all broker integrations"""
|
||||
print("🌍 QuantumBotX Multi-Broker Universe Demo")
|
||||
print("=" * 60)
|
||||
print("Your trading system now supports ALL major platforms!")
|
||||
print("=" * 60)
|
||||
|
||||
brokers_info = [
|
||||
{
|
||||
'name': 'MetaTrader 5',
|
||||
'type': 'Forex/CFD Platform',
|
||||
'assets': ['EURUSD', 'GBPUSD', 'XAUUSD', 'US30', 'AAPL'],
|
||||
'advantages': ['Most forex brokers', 'Expert Advisors', 'Built-in indicators'],
|
||||
'best_for': 'Forex and traditional CFD trading'
|
||||
},
|
||||
{
|
||||
'name': 'Binance',
|
||||
'type': 'Crypto Exchange',
|
||||
'assets': ['BTCUSDT', 'ETHUSDT', 'ADAUSDT', 'SOLUSDT', 'DOGEUSDT'],
|
||||
'advantages': ['24/7 trading', 'High liquidity', 'Low fees'],
|
||||
'best_for': 'Cryptocurrency trading and DeFi'
|
||||
},
|
||||
{
|
||||
'name': 'cTrader',
|
||||
'type': 'Modern Forex Platform',
|
||||
'assets': ['EURUSD', 'GBPUSD', 'USDJPY', 'XAUUSD', 'USOIL'],
|
||||
'advantages': ['Advanced charting', 'Level II pricing', 'Fast execution'],
|
||||
'best_for': 'Professional forex trading'
|
||||
},
|
||||
{
|
||||
'name': 'Interactive Brokers',
|
||||
'type': 'Multi-Asset Broker',
|
||||
'assets': ['AAPL', 'ES', 'EURUSD', 'GC', 'Options'],
|
||||
'advantages': ['Global markets', 'Low commissions', 'Advanced tools'],
|
||||
'best_for': 'Stocks, futures, and options'
|
||||
},
|
||||
{
|
||||
'name': 'TradingView',
|
||||
'type': 'Social Trading Platform',
|
||||
'assets': ['All markets', 'Pine Script', 'Social signals'],
|
||||
'advantages': ['Community strategies', 'Advanced charts', 'Alerts'],
|
||||
'best_for': 'Strategy development and social trading'
|
||||
}
|
||||
]
|
||||
|
||||
print("\\n🏢 Broker Overview:")
|
||||
print("=" * 60)
|
||||
|
||||
for i, broker in enumerate(brokers_info, 1):
|
||||
print(f"\\n{i}. {broker['name']} ({broker['type']})")
|
||||
print(f" 📈 Assets: {', '.join(broker['assets'][:3])}{'...' if len(broker['assets']) > 3 else ''}")
|
||||
print(f" ⭐ Best For: {broker['best_for']}")
|
||||
print(f" 🎯 Key Advantages: {', '.join(broker['advantages'][:2])}")
|
||||
|
||||
return brokers_info
|
||||
|
||||
def demo_unified_portfolio():
|
||||
"""Show how to create a unified portfolio across all brokers"""
|
||||
print("\\n💼 Unified Portfolio Management")
|
||||
print("=" * 60)
|
||||
|
||||
portfolio_allocation = {
|
||||
'MT5 (Forex)': {
|
||||
'allocation': '30%',
|
||||
'symbols': ['EURUSD', 'GBPUSD', 'USDJPY'],
|
||||
'strategy': 'QuantumBotX Hybrid',
|
||||
'capital': '$3,000'
|
||||
},
|
||||
'Binance (Crypto)': {
|
||||
'allocation': '25%',
|
||||
'symbols': ['BTCUSDT', 'ETHUSDT', 'ADAUSDT'],
|
||||
'strategy': 'MA Crossover (Crypto-tuned)',
|
||||
'capital': '$2,500'
|
||||
},
|
||||
'cTrader (Forex Pro)': {
|
||||
'allocation': '20%',
|
||||
'symbols': ['XAUUSD', 'USOIL'],
|
||||
'strategy': 'Bollinger Reversion',
|
||||
'capital': '$2,000'
|
||||
},
|
||||
'Interactive Brokers (Stocks)': {
|
||||
'allocation': '20%',
|
||||
'symbols': ['AAPL', 'MSFT', 'TSLA'],
|
||||
'strategy': 'Quantum Velocity',
|
||||
'capital': '$2,000'
|
||||
},
|
||||
'TradingView (Signals)': {
|
||||
'allocation': '5%',
|
||||
'symbols': ['Community strategies'],
|
||||
'strategy': 'Pine Script alerts',
|
||||
'capital': '$500'
|
||||
}
|
||||
}
|
||||
|
||||
print("\\n📊 Portfolio Distribution ($10,000 total):")
|
||||
print("-" * 60)
|
||||
|
||||
total_expected_return = 0
|
||||
|
||||
for broker, details in portfolio_allocation.items():
|
||||
print(f"\\n{broker}")
|
||||
print(f" 💰 Capital: {details['capital']} ({details['allocation']})")
|
||||
print(f" 📈 Assets: {', '.join(details['symbols'][:3])}")
|
||||
print(f" 🤖 Strategy: {details['strategy']}")
|
||||
|
||||
# Simulate expected returns
|
||||
expected_monthly = np.random.uniform(2, 8) # 2-8% monthly return
|
||||
total_expected_return += expected_monthly * float(details['allocation'].strip('%')) / 100
|
||||
print(f" 📊 Expected Monthly Return: {expected_monthly:.1f}%")
|
||||
|
||||
print(f"\\n🎯 Portfolio Expected Monthly Return: {total_expected_return:.1f}%")
|
||||
print(f"🎯 Portfolio Expected Annual Return: {total_expected_return * 12:.1f}%")
|
||||
|
||||
def demo_risk_management():
|
||||
"""Show unified risk management across all brokers"""
|
||||
print("\\n🛡️ Unified Risk Management System")
|
||||
print("=" * 60)
|
||||
|
||||
risk_rules = [
|
||||
{
|
||||
'rule': 'Maximum Portfolio Risk',
|
||||
'value': '15% of total capital',
|
||||
'implementation': 'Sum of all open positions across all brokers'
|
||||
},
|
||||
{
|
||||
'rule': 'Per-Broker Risk Limit',
|
||||
'value': '5% per broker maximum',
|
||||
'implementation': 'Individual broker position sizing limits'
|
||||
},
|
||||
{
|
||||
'rule': 'Correlation Protection',
|
||||
'value': 'Max 3 correlated positions',
|
||||
'implementation': 'Cross-broker correlation monitoring'
|
||||
},
|
||||
{
|
||||
'rule': 'Volatility Scaling',
|
||||
'value': 'Dynamic position sizing',
|
||||
'implementation': 'ATR-based sizing per asset class'
|
||||
},
|
||||
{
|
||||
'rule': 'Emergency Brake',
|
||||
'value': 'Auto-stop at 10% daily loss',
|
||||
'implementation': 'Real-time P&L monitoring across all accounts'
|
||||
}
|
||||
]
|
||||
|
||||
print("\\n🔒 Global Risk Rules:")
|
||||
for i, rule in enumerate(risk_rules, 1):
|
||||
print(f"\\n{i}. {rule['rule']}: {rule['value']}")
|
||||
print(f" Implementation: {rule['implementation']}")
|
||||
|
||||
def demo_24_7_opportunities():
|
||||
"""Show 24/7 trading opportunities"""
|
||||
print("\\n⏰ 24/7 Global Trading Opportunities")
|
||||
print("=" * 60)
|
||||
|
||||
trading_schedule = [
|
||||
{'time': '00:00-08:00 UTC', 'active': ['Crypto (Binance)', 'Forex (Asian session)'], 'opportunity': 'Crypto volatility + Asian forex'},
|
||||
{'time': '08:00-16:00 UTC', 'active': ['All Forex', 'European Stocks', 'Crypto'], 'opportunity': 'European session overlap'},
|
||||
{'time': '13:00-17:00 UTC', 'active': ['US Stocks (IB)', 'US/EU Forex overlap', 'Crypto'], 'opportunity': 'Maximum liquidity window'},
|
||||
{'time': '17:00-00:00 UTC', 'active': ['Crypto (Binance)', 'Asian prep', 'After-hours'], 'opportunity': 'Crypto focus + overnight gaps'}
|
||||
]
|
||||
|
||||
print("\\n🌍 Global Trading Sessions:")
|
||||
for session in trading_schedule:
|
||||
print(f"\\n⏰ {session['time']}")
|
||||
print(f" 🎯 Active: {', '.join(session['active'])}")
|
||||
print(f" 💡 Opportunity: {session['opportunity']}")
|
||||
|
||||
print("\\n🔥 Never Miss a Move:")
|
||||
print(" • Forex: 24/5 traditional markets")
|
||||
print(" • Crypto: 24/7/365 never stops")
|
||||
print(" • Stocks: Pre/post market + global exchanges")
|
||||
print(" • Commodities: Global futures markets")
|
||||
|
||||
def demo_integration_benefits():
|
||||
"""Show the benefits of integrated multi-broker system"""
|
||||
print("\\n🚀 Integration Benefits")
|
||||
print("=" * 60)
|
||||
|
||||
benefits = [
|
||||
{
|
||||
'category': 'Market Coverage',
|
||||
'benefits': [
|
||||
'Trade forex, crypto, stocks, and commodities',
|
||||
'Access to global markets 24/7',
|
||||
'Never limited by single broker restrictions'
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Risk Diversification',
|
||||
'benefits': [
|
||||
'Spread risk across multiple platforms',
|
||||
'Reduce broker-specific risks',
|
||||
'Currency and asset class diversification'
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Strategy Optimization',
|
||||
'benefits': [
|
||||
'Different strategies for different markets',
|
||||
'Platform-specific advantages utilization',
|
||||
'Cross-market arbitrage opportunities'
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Operational Excellence',
|
||||
'benefits': [
|
||||
'Single dashboard for all trading',
|
||||
'Unified risk management',
|
||||
'Consolidated reporting and analytics'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
for benefit_group in benefits:
|
||||
print(f"\\n📈 {benefit_group['category']}:")
|
||||
for benefit in benefit_group['benefits']:
|
||||
print(f" ✅ {benefit}")
|
||||
|
||||
def main():
|
||||
"""Main demo function"""
|
||||
print("🎉 Welcome to the Financial Universe!")
|
||||
print("Your QuantumBotX system now connects to EVERYTHING!")
|
||||
print()
|
||||
|
||||
# Demo all components
|
||||
brokers_info = demo_all_brokers()
|
||||
demo_unified_portfolio()
|
||||
demo_risk_management()
|
||||
demo_24_7_opportunities()
|
||||
demo_integration_benefits()
|
||||
|
||||
print("\\n" + "=" * 60)
|
||||
print("🎯 IMPLEMENTATION ROADMAP")
|
||||
print("=" * 60)
|
||||
|
||||
roadmap = [
|
||||
{
|
||||
'phase': 'Week 1: Crypto Integration',
|
||||
'tasks': ['Set up Binance testnet', 'Test crypto strategies', 'Validate risk management'],
|
||||
'impact': 'Add 24/7 trading capability'
|
||||
},
|
||||
{
|
||||
'phase': 'Week 2: cTrader Setup',
|
||||
'tasks': ['Create cTrader demo account', 'Test modern forex features', 'Compare with MT5'],
|
||||
'impact': 'Enhanced forex trading experience'
|
||||
},
|
||||
{
|
||||
'phase': 'Week 3: Interactive Brokers',
|
||||
'tasks': ['Set up TWS paper trading', 'Test stock strategies', 'Explore futures'],
|
||||
'impact': 'Access to US stocks and global markets'
|
||||
},
|
||||
{
|
||||
'phase': 'Week 4: TradingView Integration',
|
||||
'tasks': ['Set up webhook alerts', 'Create Pine Script strategies', 'Social trading'],
|
||||
'impact': 'Community-driven strategy development'
|
||||
},
|
||||
{
|
||||
'phase': 'Month 2: Unified Platform',
|
||||
'tasks': ['Portfolio manager', 'Cross-broker risk management', 'Performance analytics'],
|
||||
'impact': 'Complete multi-broker trading ecosystem'
|
||||
}
|
||||
]
|
||||
|
||||
for i, phase in enumerate(roadmap, 1):
|
||||
print(f"\\n{i}. {phase['phase']}")
|
||||
print(f" 📋 Tasks: {', '.join(phase['tasks'][:2])}...")
|
||||
print(f" 🎯 Impact: {phase['impact']}")
|
||||
|
||||
print("\\n" + "=" * 60)
|
||||
print("🏆 THE BIG PICTURE")
|
||||
print("=" * 60)
|
||||
print("\\n🌟 What You're Building:")
|
||||
print(" • Universal Trading Platform - One system, all markets")
|
||||
print(" • Risk-Managed Portfolio - Diversified across asset classes")
|
||||
print(" • 24/7 Profit Machine - Never miss opportunities")
|
||||
print(" • Future-Proof Architecture - Ready for any new broker")
|
||||
|
||||
print("\\n💰 Potential Impact:")
|
||||
current_profit = 4649.94
|
||||
projected_increase = 2.5 # Conservative 2.5x increase
|
||||
projected_profit = current_profit * projected_increase
|
||||
|
||||
print(f" Current Demo Profit: ${current_profit:,.2f}")
|
||||
print(f" With Multi-Broker: ${projected_profit:,.2f} (estimated)")
|
||||
print(f" Improvement Factor: {projected_increase}x")
|
||||
|
||||
print("\\n🎉 Congratulations!")
|
||||
print("You've just designed a trading system that rivals")
|
||||
print("what hedge funds and prop trading firms use!")
|
||||
print("\\nFrom learning to trade → Building a financial empire! 🚀")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,59 +0,0 @@
|
||||
# testing/test_ctrader_broker.py
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from datetime import datetime
|
||||
from core.brokers.ctrader_broker import CTraderBroker
|
||||
|
||||
class TestCTraderBroker(unittest.TestCase):
|
||||
"""
|
||||
Test cases for the cTrader broker implementation, focusing on market hours.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up a CTraderBroker instance for testing."""
|
||||
self.broker = CTraderBroker(demo=True)
|
||||
|
||||
@patch('core.brokers.ctrader_broker.datetime')
|
||||
def test_is_market_open_weekday(self, mock_datetime):
|
||||
"""Test that the market is open on a standard weekday."""
|
||||
# Wednesday, 12:00 UTC
|
||||
mock_datetime.utcnow.return_value = datetime(2023, 1, 4, 12, 0, 0)
|
||||
self.assertTrue(self.broker.is_market_open())
|
||||
|
||||
@patch('core.brokers.ctrader_broker.datetime')
|
||||
def test_is_market_closed_saturday(self, mock_datetime):
|
||||
"""Test that the market is closed on Saturday."""
|
||||
# Saturday, 12:00 UTC
|
||||
mock_datetime.utcnow.return_value = datetime(2023, 1, 7, 12, 0, 0)
|
||||
self.assertFalse(self.broker.is_market_open())
|
||||
|
||||
@patch('core.brokers.ctrader_broker.datetime')
|
||||
def test_is_market_opens_sunday_evening(self, mock_datetime):
|
||||
"""Test that the market opens on Sunday evening."""
|
||||
# Sunday, 22:01 UTC (market is open)
|
||||
mock_datetime.utcnow.return_value = datetime(2023, 1, 8, 22, 1, 0)
|
||||
self.assertTrue(self.broker.is_market_open())
|
||||
|
||||
@patch('core.brokers.ctrader_broker.datetime')
|
||||
def test_is_market_closed_sunday_morning(self, mock_datetime):
|
||||
"""Test that the market is closed on Sunday morning."""
|
||||
# Sunday, 10:00 UTC (market is closed)
|
||||
mock_datetime.utcnow.return_value = datetime(2023, 1, 8, 10, 0, 0)
|
||||
self.assertFalse(self.broker.is_market_open())
|
||||
|
||||
@patch('core.brokers.ctrader_broker.datetime')
|
||||
def test_is_market_closes_friday_evening(self, mock_datetime):
|
||||
"""Test that the market closes on Friday evening."""
|
||||
# Friday, 22:01 UTC (market is closed)
|
||||
mock_datetime.utcnow.return_value = datetime(2023, 1, 6, 22, 1, 0)
|
||||
self.assertFalse(self.broker.is_market_open())
|
||||
|
||||
@patch('core.brokers.ctrader_broker.datetime')
|
||||
def test_is_market_open_friday_morning(self, mock_datetime):
|
||||
"""Test that the market is open on Friday morning."""
|
||||
# Friday, 10:00 UTC (market is open)
|
||||
mock_datetime.utcnow.return_value = datetime(2023, 1, 6, 10, 0, 0)
|
||||
self.assertTrue(self.broker.is_market_open())
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user