BotDeepSeek/strategies/forex.py
2026-08-12 01:46:19 -07:00

122 lines
5.0 KiB
Python

import logging
import pandas as pd
from ib_insync import Contract, IB
from bars import BarManager, is_market_active, parse_bar_size_seconds, to_completed_df
from config import config
from orders import execute_market_order, trade_commission
from state import PositionTracker
from strategies.base import BaseStrategy
logger = logging.getLogger(__name__)
class ForexMAStrategy(BaseStrategy):
"""Forex MA cross strategy (disabled by default).
Exits: hard stop loss at -stop_loss_pct, or fast MA crossing below slow MA.
"""
def __init__(self, ib: IB, bar_manager: BarManager, tracker: PositionTracker):
super().__init__(ib, bar_manager, tracker)
self.cfg = config.forex
self.fast_period = self.cfg.fast_ma_period
self.slow_period = self.cfg.slow_ma_period
self.units = self.cfg.trade_units
self.bar_seconds = parse_bar_size_seconds(self.cfg.bar_size)
self.contracts: dict[str, Contract] = {}
async def on_start(self):
logger.info(
"Forex MA strategy started: pairs=%s, fast=%d, slow=%d, units=%d, stop_loss=%.1f%%",
self.cfg.pairs, self.fast_period, self.slow_period, self.units, self.cfg.stop_loss_pct,
)
for pair in self.cfg.pairs:
base, quote = pair.split(".")
contract = Contract(secType="CASH", symbol=base, currency=quote, exchange=self.cfg.exchange)
await self.ib.qualifyContractsAsync(contract)
if not contract.conId:
logger.error("Forex %s: failed to qualify contract, skipped", pair)
continue
self.contracts[pair] = contract
await self.bar_manager.subscribe(
contract, self.cfg.bar_size, f"{self.cfg.lookback_days} D", "MIDPOINT"
)
logger.info("Forex %s: subscribed, owned=%s", pair, self.tracker.get(self.name, pair))
async def _get_df(self, contract: Contract) -> pd.DataFrame:
bars = self.bar_manager.get(contract, self.cfg.bar_size, "MIDPOINT")
if bars is None:
bars = await self.bar_manager.subscribe(
contract, self.cfg.bar_size, f"{self.cfg.lookback_days} D", "MIDPOINT"
)
return to_completed_df(bars, self.bar_seconds)
async def on_bar(self):
for pair, contract in self.contracts.items():
try:
await self._process_pair(pair, contract)
except Exception as e:
logger.exception("Forex %s: error: %s", pair, e)
async def _process_pair(self, pair: str, contract: Contract):
df = await self._get_df(contract)
if len(df) < self.slow_period + 2:
return
if not is_market_active(df, self.bar_seconds):
return
df = df.copy()
df["fast_ma"] = df["close"].rolling(self.fast_period).mean()
df["slow_ma"] = df["close"].rolling(self.slow_period).mean()
last = df.iloc[-1]
prev = df.iloc[-2]
fast_above = last["fast_ma"] > last["slow_ma"]
prev_fast_above = prev["fast_ma"] > prev["slow_ma"]
cross_up = fast_above and not prev_fast_above
cross_down = not fast_above and prev_fast_above
owned = self.tracker.get(self.name, pair)
if owned:
entry = owned["entry_price"]
if last["close"] <= entry * (1 - self.cfg.stop_loss_pct / 100):
logger.info(
"FOREX STOP-LOSS SELL: %s close=%.5f entry=%.5f",
pair, last["close"], entry,
)
await self._sell(pair, contract, owned["quantity"], "SoftStop")
elif cross_down:
logger.info(
"FOREX SELL %s (fast MA %.5f < slow MA %.5f)",
pair, last["fast_ma"], last["slow_ma"],
)
await self._sell(pair, contract, owned["quantity"], "CrossDown")
else:
if cross_up:
logger.info(
"FOREX BUY %s (fast MA %.5f crossed above slow MA %.5f)",
pair, last["fast_ma"], last["slow_ma"],
)
await self._buy(pair, contract)
async def _buy(self, pair: str, contract: Contract, reason: str = "MACross"):
ref = f"{self.name}:{pair}"
trade = await execute_market_order(self.ib, contract, "BUY", self.units, ref)
if trade and trade.orderStatus.filled > 0:
self.tracker.record_buy(
self.name, pair, trade.orderStatus.filled, trade.orderStatus.avgFillPrice,
reason=reason, commission=trade_commission(trade),
)
async def _sell(self, pair: str, contract: Contract, quantity: float, reason: str = ""):
ref = f"{self.name}:{pair}"
trade = await execute_market_order(self.ib, contract, "SELL", quantity, ref)
if trade and trade.orderStatus.filled > 0:
self.tracker.record_sell(self.name, pair, trade.orderStatus.filled, reason=reason)
async def on_tick(self):
pass