121 lines
4.8 KiB
Python
121 lines
4.8 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
|
|
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"])
|
|
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"])
|
|
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):
|
|
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
|
|
)
|
|
|
|
async def _sell(self, pair: str, contract: Contract, quantity: float):
|
|
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)
|
|
|
|
async def on_tick(self):
|
|
pass
|