| Overall Statistics |
|
Total Orders 5716 Average Win 0.35% Average Loss -0.40% Compounding Annual Return 11.578% Drawdown 9.100% Expectancy 0.253 Start Equity 100000 End Equity 196000.01 Net Profit 96.000% Sharpe Ratio 0.667 Sortino Ratio 0.507 Probabilistic Sharpe Ratio 56.186% Loss Rate 33% Win Rate 67% Profit-Loss Ratio 0.88 Alpha 0.04 Beta 0.112 Annual Standard Deviation 0.074 Annual Variance 0.006 Information Ratio -0.202 Tracking Error 0.168 Treynor Ratio 0.442 Total Fees $2200.64 Estimated Strategy Capacity $66000000.00 Lowest Capacity Asset VIAC XA367FHRKACL Portfolio Turnover 6.26% Drawdown Recovery 622 |
from AlgorithmImports import *
from collections import defaultdict
import pandas as pd
class Nasdaq100MarketCapIbs(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2023, 1, 1)
self.set_cash(100000)
self.universe_settings.resolution = Resolution.DAILY
self._universe = self.add_universe(self.universe.etf("QQQ", self._etf_filter))
self._entry_dates = {}
self._market_caps = {}
self._pending_orders = {}
# Warm-up 210 dní pro momentum 200D
self.set_warmup(210, Resolution.DAILY)
self.schedule.on(
self.date_rules.every_day("QQQ"),
self.time_rules.after_market_close("QQQ", 2),
self._evaluate_signals
)
self.schedule.on(
self.date_rules.every(DayOfWeek.MONDAY),
self.time_rules.after_market_open("QQQ", 30),
self._refresh_market_caps
)
def _etf_filter(self, constituents: List[ETFConstituentUniverse]) -> List[Symbol]:
return [c.symbol for c in constituents]
def _refresh_market_caps(self) -> None:
if self.is_warming_up:
return
selected = list(self._universe.selected)
if not selected:
return
for symbol in selected:
try:
cap = self.securities[symbol].fundamentals.market_cap
if cap and cap > 0:
self._market_caps[symbol] = cap
except Exception:
pass
self.log(f"Market caps refreshed: {len(self._market_caps)} symbols")
def on_securities_changed(self, changes: SecurityChanges) -> None:
for added in changes.added_securities:
try:
cap = added.fundamentals.market_cap
if cap and cap > 0:
self._market_caps[added.symbol] = cap
except Exception:
pass
for removed in changes.removed_securities:
if removed.symbol in self._entry_dates:
del self._entry_dates[removed.symbol]
if removed.symbol in self._market_caps:
del self._market_caps[removed.symbol]
if removed.symbol in self._pending_orders:
ticket = self._pending_orders.pop(removed.symbol)
if ticket.status not in [OrderStatus.FILLED, OrderStatus.CANCELED]:
ticket.cancel()
def _get_history(self, symbol: Symbol, periods: int) -> Optional[pd.DataFrame]:
"""Načte daily historii pro symbol."""
try:
hist = self.history(symbol, periods, Resolution.DAILY)
if hist.empty or len(hist) < periods:
return None
return hist
except Exception:
return None
def _compute_ibs(self, symbol: Symbol) -> Optional[float]:
"""IBS z posledního daily baru."""
hist = self._get_history(symbol, 2)
if hist is None:
return None
last = hist.iloc[-1]
high, low, close = float(last["high"]), float(last["low"]), float(last["close"])
if high == low:
return 0.0
return (close - low) / (high - low)
def _compute_momentum_200(self, symbol: Symbol) -> Optional[float]:
"""200denní momentum."""
hist = self._get_history(symbol, 201)
if hist is None:
return None
past = float(hist.iloc[0]["close"])
current = float(hist.iloc[-1]["close"])
if past <= 0:
return None
return current / past - 1.0
def _compute_atr(self, symbol: Symbol, period: int = 10) -> Optional[float]:
"""Wilder ATR z daily barů."""
hist = self._get_history(symbol, period * 3)
if hist is None or len(hist) < period + 2:
return None
df = hist.copy().reset_index(drop=True)
prev_close = df["close"].shift(1)
tr = pd.concat([
df["high"] - df["low"],
(df["high"] - prev_close).abs(),
(df["low"] - prev_close).abs()
], axis=1).max(axis=1)
atr = tr.ewm(alpha=1.0 / period, adjust=False).mean()
val = atr.iloc[-1]
return None if pd.isna(val) else float(val)
def _compute_adx(self, symbol: Symbol, period: int = 10) -> Optional[float]:
"""Wilder ADX z daily barů."""
hist = self._get_history(symbol, period * 3)
if hist is None or len(hist) < period * 3:
return None
df = hist.copy().reset_index(drop=True)
high = df["high"]
low = df["low"]
close = df["close"]
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
dm_plus = (high - high.shift(1)).clip(lower=0)
dm_minus = (low.shift(1) - low).clip(lower=0)
mask = dm_plus >= dm_minus
dm_plus = dm_plus.where(mask, 0)
dm_minus = dm_minus.where(~mask, 0)
alpha = 1.0 / period
atr = tr.ewm(alpha=alpha, adjust=False).mean()
di_plus = 100 * dm_plus.ewm(alpha=alpha, adjust=False).mean() / atr.replace(0, float("nan"))
di_minus = 100 * dm_minus.ewm(alpha=alpha, adjust=False).mean() / atr.replace(0, float("nan"))
dx = 100 * (di_plus - di_minus).abs() / (di_plus + di_minus).replace(0, float("nan"))
adx = dx.ewm(alpha=alpha, adjust=False).mean()
val = adx.iloc[-1]
return None if pd.isna(val) else float(val)
def _evaluate_signals(self) -> None:
if self.is_warming_up:
return
selected = list(self._universe.selected)
if not selected:
return
today = self.time.date()
# Zrušit nevyplněné pending ordery z předchozího dne
for symbol in list(self._pending_orders.keys()):
ticket = self._pending_orders[symbol]
if ticket.status not in [OrderStatus.FILLED, OrderStatus.CANCELED, OrderStatus.INVALID]:
ticket.cancel()
self.log(f"Cancelled pending limit order for {symbol}")
if symbol in self._entry_dates:
del self._entry_dates[symbol]
del self._pending_orders[symbol]
# Sort dle market cap desc
sorted_symbols = sorted(
selected,
key=lambda s: self._market_caps.get(s, 0),
reverse=True
)
exit_targets = []
current_positions = sum(1 for s in selected if self.portfolio[s].invested)
new_order_count = 0
max_positions = 10
position_size = 0.10
max_new_positions = max(0, max_positions - current_positions - len(self._pending_orders))
for symbol in sorted_symbols:
# Exit existující pozice
if self.portfolio[symbol].invested:
ibs = self._compute_ibs(symbol)
momentum = self._compute_momentum_200(symbol)
days_held = (today - self._entry_dates[symbol]).days if symbol in self._entry_dates else 999
if (
(ibs is not None and ibs > 0.8) or
days_held >= 10 or
(momentum is not None and momentum <= 0)
):
exit_targets.append(PortfolioTarget(symbol, 0))
if symbol in self._entry_dates:
del self._entry_dates[symbol]
ibs_str = f"{ibs:.2f}" if ibs is not None else "N/A"
momentum_str = f"{momentum:.2%}" if momentum is not None else "N/A"
self.log(f"Exit {symbol}: ibs={ibs_str}, days={days_held}, mom200={momentum_str}")
continue
if symbol in self._pending_orders:
continue
if new_order_count >= max_new_positions:
break
# Filter: pozitivní 200denní momentum
momentum = self._compute_momentum_200(symbol)
if momentum is None or momentum <= 0:
continue
# Entry filtr: IBS < 0.1
ibs = self._compute_ibs(symbol)
if ibs is None or ibs >= 0.1:
continue
# Entry filtr: ADX(10) > 20
adx = self._compute_adx(symbol, period=10)
if adx is None or adx <= 20:
continue
# Limit price: dnešní low - 0.25 * ATR(10)
hist = self._get_history(symbol, 2)
if hist is None:
continue
atr = self._compute_atr(symbol, period=10)
atr_offset = 0.40 * atr if atr is not None else 0.0
daily_low = float(hist.iloc[-1]["low"])
limit_price = round(daily_low - atr_offset, 2)
quantity = int((self.portfolio.total_portfolio_value * position_size) / limit_price)
if quantity <= 0:
continue
ticket = self.limit_order(
symbol, quantity, limit_price,
tag=f"IBS entry limit @ {limit_price:.2f} (low={daily_low:.2f}, atr_offset={atr_offset:.2f}, mom200={momentum:.2%})"
)
self._pending_orders[symbol] = ticket
self._entry_dates[symbol] = today
new_order_count += 1
self.log(f"Limit order placed: {symbol} qty={quantity} @ {limit_price:.2f} mom200={momentum:.2%}")
if exit_targets:
self.set_holdings(exit_targets, liquidate_existing_holdings=False)from AlgorithmImports import *
class DipBuyMeanReversion(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2020, 1, 1)
self.set_cash(100000)
self.universe_settings.resolution = Resolution.DAILY
# --- Tunable parameters ---
self.MAX_POSITIONS = 10
self.POSITION_SIZE = 0.10
self.MIN_DAY_RETURN = -0.03
self.IBS_EXIT = 0.8
self.SMA_WINDOW = 200
self.ATR_WINDOW = 5
self.ATR_MULTIPLIER = 0.9
self.MAX_GROSS_EXPOSURE = 1.0
self.MAX_HOLD_DAYS = 10
self._universe = self.add_universe(self.universe.etf("SPY", self._etf_filter))
self._pending_orders: dict[Symbol, OrderTicket] = {}
self._sma: dict[Symbol, SimpleMovingAverage] = {}
self._atr: dict[Symbol, AverageTrueRange] = {}
self._entry_date: dict[Symbol, datetime] = {}
# SPY regime filter (replaces QQQ)
self._spy = self.add_equity("SPY", Resolution.DAILY).symbol
self._spy_sma = self.sma("SPY", self.SMA_WINDOW, Resolution.DAILY)
self.schedule.on(
self.date_rules.every_day("SPY"),
self.time_rules.before_market_close("SPY", 1),
self._evaluate_signals
)
self.schedule.on(
self.date_rules.every_day("SPY"),
self.time_rules.before_market_close("SPY", 0),
self._cancel_pending_orders
)
self.set_warm_up(self.SMA_WINDOW, Resolution.DAILY)
# ------------------------------------------------------------------ #
# Universe
# ------------------------------------------------------------------ #
def _etf_filter(self, constituents: List[ETFConstituentUniverse]) -> List[Symbol]:
return [c.symbol for c in constituents]
def on_securities_changed(self, changes: SecurityChanges) -> None:
for added in changes.added_securities:
sym = added.symbol
if sym not in self._sma:
self._sma[sym] = self.sma(sym, self.SMA_WINDOW, Resolution.DAILY)
if sym not in self._atr:
self._atr[sym] = self.atr(sym, self.ATR_WINDOW,
MovingAverageType.WILDERS, Resolution.DAILY)
for removed in changes.removed_securities:
sym = removed.symbol
self._entry_date.pop(sym, None)
self._cancel_order_for(sym)
self._sma.pop(sym, None)
self._atr.pop(sym, None)
# ------------------------------------------------------------------ #
# Regime
# ------------------------------------------------------------------ #
def _is_spy_bull(self) -> bool:
if not self._spy_sma.is_ready:
return False
return self.securities[self._spy].price > self._spy_sma.current.value
def _is_stock_uptrend(self, symbol: Symbol) -> bool:
ind = self._sma.get(symbol)
if ind is None or not ind.is_ready:
return False
return self.securities[symbol].price > ind.current.value
# ------------------------------------------------------------------ #
# Signal helpers
# ------------------------------------------------------------------ #
def _compute_day_return(self, symbol: Symbol) -> Optional[float]:
hist = self.history(symbol, 2, Resolution.DAILY)
if hist is None or hist.empty or len(hist) < 2:
return None
prev = float(hist.iloc[-2]["close"])
curr = float(hist.iloc[-1]["close"])
return (curr - prev) / prev if prev != 0 else None
def _compute_ibs(self, symbol: Symbol) -> Optional[float]:
hist = self.history(symbol, 1, Resolution.DAILY)
if hist is None or hist.empty:
return None
last = hist.iloc[-1]
high, low, close = float(last["high"]), float(last["low"]), float(last["close"])
if high == low:
return 0.0
return (close - low) / (high - low)
def _get_close_price(self, symbol: Symbol) -> Optional[float]:
hist = self.history(symbol, 1, Resolution.DAILY)
if hist is None or hist.empty:
return None
return float(hist.iloc[-1]["close"])
def _close_above_prev_high(self, symbol: Symbol) -> bool:
hist = self.history(symbol, 2, Resolution.DAILY)
if hist is None or hist.empty or len(hist) < 2:
return False
prev_high = float(hist.iloc[-2]["high"])
today_close = float(hist.iloc[-1]["close"])
return today_close > prev_high
def _atr_ratio(self, symbol: Symbol) -> float:
atr_ind = self._atr.get(symbol)
price = self.securities[symbol].price
if atr_ind is None or not atr_ind.is_ready or price == 0:
return 0.0
return atr_ind.current.value / price
def _holding_days(self, symbol: Symbol) -> int:
entry = self._entry_date.get(symbol)
if entry is None:
return 0
return (self.time - entry).days
def _current_gross_exposure(self) -> float:
total = self.portfolio.total_portfolio_value
if total <= 0:
return 0.0
return sum(
abs(self.portfolio[s].holdings_value)
for s in self._universe.selected
if self.portfolio[s].invested
) / total
# ------------------------------------------------------------------ #
# Order management
# ------------------------------------------------------------------ #
def _cancel_order_for(self, symbol: Symbol) -> None:
ticket = self._pending_orders.pop(symbol, None)
if ticket is not None:
try:
ticket.cancel("Stale limit order cancelled")
except Exception as e:
self.debug(f"Cancel failed for {symbol.value}: {e}")
def _cancel_pending_orders(self) -> None:
for symbol in list(self._pending_orders.keys()):
self._cancel_order_for(symbol)
def _liquidate_all_market(self, reason: str) -> None:
for symbol in list(self._universe.selected):
if self.portfolio[symbol].invested:
self.market_order(symbol, -self.portfolio[symbol].quantity,
tag=f"Regime exit: {reason}")
self._entry_date.pop(symbol, None)
self._cancel_pending_orders()
def _exit_limit(self, symbol: Symbol, close_price: float, tag: str) -> None:
quantity = self.portfolio[symbol].quantity
ticket = self.limit_order(symbol, -quantity, close_price, tag=tag)
self._pending_orders[symbol] = ticket
# ------------------------------------------------------------------ #
# Core logic
# ------------------------------------------------------------------ #
def _evaluate_signals(self) -> None:
if self.is_warming_up:
return
selected = list(self._universe.selected)
if not selected:
return
# --- Index regime gate ---
if not self._is_spy_bull():
if any(self.portfolio[s].invested for s in selected):
self._liquidate_all_market("SPY below 200 SMA")
return
invested = [s for s in selected if self.portfolio[s].invested]
not_invested = [s for s in selected if not self.portfolio[s].invested]
current = len(invested)
# --- EXIT (three conditions, priority order) ---
for symbol in invested:
if symbol in self._pending_orders:
continue
close_price = self._get_close_price(symbol)
if close_price is None:
continue
# Priority 1: time stop → market order immediately
if self._holding_days(symbol) >= self.MAX_HOLD_DAYS:
self.market_order(symbol, -self.portfolio[symbol].quantity,
tag=f"Time stop: {self._holding_days(symbol)}d held")
self._entry_date.pop(symbol, None)
current -= 1
continue
# Priority 2: close > yesterday's high → mean reversion complete
if self._close_above_prev_high(symbol):
self._exit_limit(symbol, close_price, tag="Exit: close > prev high")
current -= 1
continue
# Priority 3: IBS > 0.8 → closed near top of today's range
ibs = self._compute_ibs(symbol)
if ibs is not None and ibs > self.IBS_EXIT:
self._exit_limit(symbol, close_price, tag="Exit: IBS > 0.8")
current -= 1
# --- ENTRY: stock above own 200 SMA + down ≥ 3% + ATR limit ---
# Sort by ATR/Close descending — prefer highest relative volatility
not_invested.sort(key=self._atr_ratio, reverse=True)
slots = self.MAX_POSITIONS - current
for symbol in not_invested:
if slots <= 0:
break
if symbol in self._pending_orders:
continue
# Hard exposure cap
pending_exposure = len(self._pending_orders) * self.POSITION_SIZE
if self._current_gross_exposure() + pending_exposure >= self.MAX_GROSS_EXPOSURE:
break
# Rule 1: stock above its own 200-day SMA
if not self._is_stock_uptrend(symbol):
continue
# Rule 2: today's close ≤ -3% vs yesterday
day_ret = self._compute_day_return(symbol)
if day_ret is None or day_ret > self.MIN_DAY_RETURN:
continue
close_price = self._get_close_price(symbol)
if close_price is None:
continue
# Rule 3: limit price = close - 0.9 × ATR(5)
atr_ind = self._atr.get(symbol)
if atr_ind is None or not atr_ind.is_ready:
continue
limit_price = round(close_price - self.ATR_MULTIPLIER * atr_ind.current.value, 2)
quantity = int((self.portfolio.total_portfolio_value * self.POSITION_SIZE) / limit_price)
if quantity <= 0:
continue
ticket = self.limit_order(symbol, quantity, limit_price,
tag=f"Dip entry ATR limit | ret={day_ret:.2%} lim={limit_price:.2f}")
self._pending_orders[symbol] = ticket
slots -= 1
def on_order_event(self, order_event: OrderEvent) -> None:
if order_event.status == OrderStatus.FILLED:
symbol = order_event.symbol
self._pending_orders.pop(symbol, None)
if order_event.fill_quantity > 0:
self._entry_date[symbol] = self.time
else:
self._entry_date.pop(symbol, None)
elif order_event.status in (OrderStatus.CANCELED, OrderStatus.INVALID):
self._pending_orders.pop(order_event.symbol, None)from AlgorithmImports import *
from collections import defaultdict
import json
import pandas as pd
class Nasdaq100MarketCapIbs(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2023, 1, 1)
self.set_cash(100000)
self.universe_settings.resolution = Resolution.MINUTE
self.universe_settings.extended_market_hours = True
self._universe = self.add_universe(self.universe.etf("QQQ", self._etf_filter))
self._unfinished_bars = {}
self._databank_key = "nasdaq100_daily_bars"
self._databank = defaultdict(list)
self._entry_dates = {}
self._market_caps = {}
self._pending_orders = {}
self._warmup_done = False
# Warm-up 210 dní aby momentum 200D mělo dostatek dat od prvního dne
self.set_warmup(210, Resolution.DAILY)
if self.object_store.contains_key(self._databank_key):
raw = self.object_store.read_string(self._databank_key)
loaded = json.loads(raw)
for k, v in loaded.items():
self._databank[k] = v
self.schedule.on(
self.date_rules.every_day("QQQ"),
self.time_rules.before_market_close("QQQ", 5),
self._evaluate_signals
)
self.schedule.on(
self.date_rules.every_day("QQQ"),
self.time_rules.after_market_close("QQQ", 0),
self._save_eod_bars
)
self.schedule.on(
self.date_rules.every(DayOfWeek.MONDAY),
self.time_rules.after_market_open("QQQ", 30),
self._refresh_market_caps
)
def _etf_filter(self, constituents: List[ETFConstituentUniverse]) -> List[Symbol]:
return [c.symbol for c in constituents]
def on_warmup_finished(self) -> None:
"""Po skončení warm-up naplníme databank z history callu."""
selected = list(self._universe.selected)
if not selected:
self._warmup_done = True
return
self.log(f"Warm-up finished, loading history for {len(selected)} symbols...")
history = self.history(selected, 210, Resolution.DAILY)
if history.empty:
self._warmup_done = True
return
for symbol in selected:
symbol_str = str(symbol)
# Přeskočit pokud databank už má data (z object store)
if len(self._databank.get(symbol_str, [])) >= 200:
continue
try:
sym_hist = history.loc[symbol]
except KeyError:
continue
bars = []
for date, row in sym_hist.iterrows():
bars.append({
"open": float(row["open"]),
"high": float(row["high"]),
"low": float(row["low"]),
"close": float(row["close"]),
"volume": float(row["volume"]),
"date": str(date.date())
})
if bars:
self._databank[symbol_str] = bars[-300:]
self._warmup_done = True
self.log(f"Databank loaded: {len(self._databank)} symbols")
def _refresh_market_caps(self) -> None:
selected = list(self._universe.selected)
if not selected:
return
for symbol in selected:
try:
cap = self.securities[symbol].fundamentals.market_cap
if cap and cap > 0:
self._market_caps[symbol] = cap
except Exception:
pass
self.log(f"Market caps refreshed: {len(self._market_caps)} symbols")
def on_securities_changed(self, changes: SecurityChanges) -> None:
for added in changes.added_securities:
self._unfinished_bars[added.symbol] = None
added.set_data_normalization_mode(DataNormalizationMode.RAW)
added.set_fill_model(ImmediateFillModel())
try:
cap = added.fundamentals.market_cap
if cap and cap > 0:
self._market_caps[added.symbol] = cap
except Exception:
pass
for removed in changes.removed_securities:
if removed.symbol in self._unfinished_bars:
del self._unfinished_bars[removed.symbol]
if removed.symbol in self._entry_dates:
del self._entry_dates[removed.symbol]
if removed.symbol in self._market_caps:
del self._market_caps[removed.symbol]
if removed.symbol in self._pending_orders:
ticket = self._pending_orders.pop(removed.symbol)
if ticket.status not in [OrderStatus.FILLED, OrderStatus.CANCELED]:
ticket.cancel()
key = str(removed.symbol)
if key in self._databank:
del self._databank[key]
def on_data(self, data: Slice) -> None:
# Během warm-up ignorujeme — databank plníme v on_warmup_finished
if self.is_warming_up:
return
for symbol in self._universe.selected:
if not data.contains_key(symbol):
continue
bar = data[symbol]
if bar is None:
continue
t = self.time
if t.hour < 9 or (t.hour == 9 and t.minute < 30) or t.hour >= 16:
continue
if self._unfinished_bars.get(symbol) is None:
self._unfinished_bars[symbol] = {
"open": float(bar.open),
"high": float(bar.high),
"low": float(bar.low),
"close": float(bar.close),
"volume": float(bar.volume),
"date": str(self.time.date())
}
else:
u = self._unfinished_bars[symbol]
u["high"] = max(u["high"], float(bar.high))
u["low"] = min(u["low"], float(bar.low))
u["close"] = float(bar.close)
u["volume"] += float(bar.volume)
def _compute_momentum_200(self, symbol: Symbol) -> Optional[float]:
bars = self._databank.get(str(symbol), [])
if len(bars) < 200:
return None
past = bars[-200]["close"]
current = bars[-1]["close"]
if past <= 0:
return None
return current / past - 1.0
def _evaluate_signals(self) -> None:
if self.is_warming_up or not self._warmup_done:
return
selected = list(self._universe.selected)
if not selected:
return
today = self.time.date()
for symbol in list(self._pending_orders.keys()):
ticket = self._pending_orders[symbol]
if ticket.status not in [OrderStatus.FILLED, OrderStatus.CANCELED, OrderStatus.INVALID]:
ticket.cancel()
self.log(f"Cancelled pending limit order for {symbol}")
if symbol in self._entry_dates:
del self._entry_dates[symbol]
del self._pending_orders[symbol]
sorted_symbols = sorted(
selected,
key=lambda s: self._market_caps.get(s, 0),
reverse=True
)
exit_targets = []
current_positions = sum(1 for s in selected if self.portfolio[s].invested)
new_order_count = 0
max_positions = 10
position_size = 0.10
max_new_positions = max(0, max_positions - current_positions - len(self._pending_orders))
for symbol in sorted_symbols:
if self.portfolio[symbol].invested:
ibs = self._compute_ibs(symbol)
days_held = (today - self._entry_dates[symbol]).days if symbol in self._entry_dates else 999
momentum = self._compute_momentum_200(symbol)
if (
(ibs is not None and ibs > 0.8) or
days_held >= 10 or
(momentum is not None and momentum <= 0)
):
exit_targets.append(PortfolioTarget(symbol, 0))
if symbol in self._entry_dates:
del self._entry_dates[symbol]
ibs_str = f"{ibs:.2f}" if ibs is not None else "N/A"
momentum_str = f"{momentum:.2%}" if momentum is not None else "N/A"
self.log(f"Exit {symbol}: ibs={ibs_str}, days={days_held}, mom200={momentum_str}")
continue
if symbol in self._pending_orders:
continue
if new_order_count >= max_new_positions:
break
momentum = self._compute_momentum_200(symbol)
if momentum is None or momentum <= 0:
continue
ibs = self._compute_ibs(symbol)
if ibs is None or ibs >= 0.2:
continue
adx = self._compute_adx(symbol, period=10)
if adx is None or adx <= 20:
continue
u = self._unfinished_bars.get(symbol)
if u is None:
continue
atr = self._compute_atr(symbol, period=10)
atr_offset = 0.50 * atr if atr is not None else 0.0
limit_price = round(u["low"] - atr_offset, 2)
quantity = int((self.portfolio.total_portfolio_value * position_size) / limit_price)
if quantity <= 0:
continue
ticket = self.limit_order(
symbol, quantity, limit_price,
tag=f"IBS entry limit @ {limit_price:.2f} (low={u['low']:.2f}, atr_offset={atr_offset:.2f}, mom200={momentum:.2%})"
)
self._pending_orders[symbol] = ticket
self._entry_dates[symbol] = today
new_order_count += 1
self.log(f"Limit order placed: {symbol} qty={quantity} @ {limit_price:.2f} mom200={momentum:.2%}")
if exit_targets:
self.set_holdings(exit_targets, liquidate_existing_holdings=False)
def _compute_ibs(self, symbol: Symbol) -> Optional[float]:
u = self._unfinished_bars.get(symbol)
if u is None:
return None
high, low, close = u["high"], u["low"], u["close"]
if high == low:
return 0.0
return (close - low) / (high - low)
def _compute_atr(self, symbol: Symbol, period: int = 10) -> Optional[float]:
symbol_str = str(symbol)
bars = list(self._databank.get(symbol_str, []))
u = self._unfinished_bars.get(symbol)
if u is not None:
bars.append(u)
if len(bars) < period + 2:
return None
df = pd.DataFrame(bars)
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values("date").tail(period * 3).reset_index(drop=True)
prev_close = df["close"].shift(1)
tr = pd.concat([
df["high"] - df["low"],
(df["high"] - prev_close).abs(),
(df["low"] - prev_close).abs()
], axis=1).max(axis=1)
atr = tr.ewm(alpha=1.0 / period, adjust=False).mean()
val = atr.iloc[-1]
return None if pd.isna(val) else float(val)
def _compute_adx(self, symbol: Symbol, period: int = 10) -> Optional[float]:
symbol_str = str(symbol)
bars = list(self._databank.get(symbol_str, []))
u = self._unfinished_bars.get(symbol)
if u is not None:
bars.append(u)
if len(bars) < period * 3:
return None
df = pd.DataFrame(bars)
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values("date").tail(period * 3).reset_index(drop=True)
high = df["high"]
low = df["low"]
close = df["close"]
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
dm_plus = (high - high.shift(1)).clip(lower=0)
dm_minus = (low.shift(1) - low).clip(lower=0)
mask = dm_plus >= dm_minus
dm_plus = dm_plus.where(mask, 0)
dm_minus = dm_minus.where(~mask, 0)
alpha = 1.0 / period
atr = tr.ewm(alpha=alpha, adjust=False).mean()
di_plus = 100 * dm_plus.ewm(alpha=alpha, adjust=False).mean() / atr.replace(0, float("nan"))
di_minus = 100 * dm_minus.ewm(alpha=alpha, adjust=False).mean() / atr.replace(0, float("nan"))
dx = 100 * (di_plus - di_minus).abs() / (di_plus + di_minus).replace(0, float("nan"))
adx = dx.ewm(alpha=alpha, adjust=False).mean()
val = adx.iloc[-1]
return None if pd.isna(val) else float(val)
def _save_eod_bars(self) -> None:
if self.is_warming_up:
return
for symbol, u in self._unfinished_bars.items():
if u is None:
continue
symbol_str = str(symbol)
self._databank[symbol_str].append(u)
if len(self._databank[symbol_str]) > 300:
self._databank[symbol_str] = self._databank[symbol_str][-300:]
self.object_store.save_string(self._databank_key, json.dumps(dict(self._databank)))
for symbol in self._unfinished_bars:
self._unfinished_bars[symbol] = None