0206ef7cbb
Real-time orderflow trading system with absorption, initiative, sweep, exhaustion, and divergence detection. Features volume profile framing, state machine trade lifecycle, MT5 + Bybit feeds, FastAPI dashboard, and Telegram alerts for 30+ instruments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
783 lines
34 KiB
Python
783 lines
34 KiB
Python
"""
|
|
Demo data generator for standalone dashboard mode.
|
|
Provides realistic-looking orderflow data when no live feed is connected.
|
|
"""
|
|
import math
|
|
import random
|
|
import time
|
|
|
|
# ── Seed for reproducibility within a session ──
|
|
_session_seed = int(time.time()) % 10000
|
|
random.seed(_session_seed)
|
|
|
|
|
|
def _stable_rng(symbol: str, tf: int = 0, range_s: int = 0) -> random.Random:
|
|
"""Return a Random instance seeded by symbol + time-window.
|
|
|
|
The time-window reseeds every 60 seconds so the data drifts
|
|
slowly but repeated calls within the same minute return identical data.
|
|
"""
|
|
window = int(time.time()) // 60 # changes every 60 s
|
|
seed = hash((symbol, tf, range_s, window, _session_seed))
|
|
return random.Random(seed)
|
|
|
|
# ── Base prices for instruments (full MT5 coverage) ──
|
|
_BASE_PRICES = {
|
|
# Forex (main pairs)
|
|
"EURUSD": 1.0785, "GBPUSD": 1.2615, "USDJPY": 152.30,
|
|
"AUDUSD": 0.6540, "USDCAD": 1.3520, "USDCHF": 0.8850, "NZDUSD": 0.6120,
|
|
"EURGBP": 0.8550, "EURJPY": 164.20, "GBPJPY": 192.10,
|
|
# Metals
|
|
"XAUUSDT": 2920.0, "XAGUSD": 32.50,
|
|
# Indices
|
|
"NAS100USDT": 21450.0, "SP500": 6100.0, "DJ30": 44200.0,
|
|
"DAX40": 18900.0, "UK100": 8350.0,
|
|
# Crypto
|
|
"BTCUSDT": 98500.0, "ETHUSDT": 2680.0, "SOLUSDT": 195.0,
|
|
"XRPUSDT": 2.45, "BNBUSDT": 680.0,
|
|
# Stocks
|
|
"AAPL": 232.0, "TSLA": 365.0, "AMZN": 228.0, "MSFT": 415.0,
|
|
"NVDA": 138.0, "META": 680.0, "GOOGL": 185.0,
|
|
}
|
|
|
|
_TICK_SIZES = {
|
|
# Forex
|
|
**{s: 0.0001 for s in ["EURUSD","GBPUSD","AUDUSD","NZDUSD","USDCAD","USDCHF","EURGBP"]},
|
|
**{s: 0.01 for s in ["USDJPY","EURJPY","GBPJPY"]},
|
|
# Metals
|
|
"XAUUSDT": 0.10, "XAGUSD": 0.01,
|
|
# Indices
|
|
"NAS100USDT": 0.5, "SP500": 0.25, "DJ30": 1.0, "DAX40": 0.5, "UK100": 0.5,
|
|
# Crypto
|
|
"BTCUSDT": 0.5, "ETHUSDT": 0.1, "SOLUSDT": 0.01, "XRPUSDT": 0.0001, "BNBUSDT": 0.1,
|
|
# Stocks
|
|
**{s: 0.01 for s in ["AAPL","TSLA","AMZN","MSFT","NVDA","META","GOOGL"]},
|
|
}
|
|
|
|
|
|
def _base_price(symbol: str) -> float:
|
|
return _BASE_PRICES.get(symbol, 1000.0)
|
|
|
|
|
|
def _tick_size(symbol: str) -> float:
|
|
return _TICK_SIZES.get(symbol, 0.5)
|
|
|
|
|
|
def _gen_price_walk(base: float, n: int, volatility: float = 0.001, rng: random.Random | None = None) -> list:
|
|
"""Generate a random-walk price series."""
|
|
r = rng or random
|
|
prices = [base]
|
|
for _ in range(n - 1):
|
|
change = base * volatility * r.gauss(0, 1)
|
|
prices.append(prices[-1] + change)
|
|
return prices
|
|
|
|
|
|
# ════════════════════════════════════════════
|
|
# Public API — called by app.py endpoints
|
|
# ════════════════════════════════════════════
|
|
|
|
|
|
def demo_instruments():
|
|
"""Return a list of demo instruments."""
|
|
rng = _stable_rng("__instruments__")
|
|
now_ms = int(time.time() * 1000)
|
|
result = []
|
|
for sym, price in _BASE_PRICES.items():
|
|
drift = price * rng.uniform(-0.002, 0.002)
|
|
result.append({
|
|
"symbol": sym,
|
|
"price": round(price + drift, 5),
|
|
"volume_24h": rng.randint(50000, 500000),
|
|
"tick_count": rng.randint(10000, 100000),
|
|
"candle_count": rng.randint(200, 1000),
|
|
"data_source": "demo",
|
|
"trade_phase": "none",
|
|
"trade_direction": "none",
|
|
"last_update_ms": now_ms,
|
|
})
|
|
return result
|
|
|
|
|
|
def demo_candles(symbol: str, tf: int = 60, range_s: int = 86400):
|
|
"""Generate realistic OHLC candle data — stable per symbol/tf/range."""
|
|
rng = _stable_rng(symbol, tf, range_s)
|
|
base = _base_price(symbol)
|
|
tick = _tick_size(symbol)
|
|
now = int(time.time())
|
|
start = now - range_s
|
|
n_candles = range_s // tf
|
|
|
|
# Limit to reasonable number
|
|
n_candles = min(n_candles, 1500)
|
|
|
|
vol = 0.0004 if base > 1000 else 0.0008
|
|
walk = _gen_price_walk(base, n_candles + 1, vol, rng)
|
|
|
|
candles = []
|
|
for i in range(n_candles):
|
|
t = start + i * tf
|
|
o = walk[i]
|
|
c = walk[i + 1]
|
|
h = max(o, c) + abs(rng.gauss(0, base * vol * 0.5))
|
|
l = min(o, c) - abs(rng.gauss(0, base * vol * 0.5))
|
|
volume = rng.randint(50, 800)
|
|
delta = rng.randint(-200, 200)
|
|
|
|
candles.append({
|
|
"time": t,
|
|
"open": round(o, 5),
|
|
"high": round(h, 5),
|
|
"low": round(l, 5),
|
|
"close": round(c, 5),
|
|
"volume": volume,
|
|
"delta": delta,
|
|
})
|
|
|
|
return candles
|
|
|
|
|
|
def demo_delta(symbol: str, tf: int = 60, range_s: int = 86400):
|
|
"""Generate cumulative delta data — stable per symbol/tf/range."""
|
|
rng = _stable_rng(symbol, tf, range_s)
|
|
now = int(time.time())
|
|
start = now - range_s
|
|
n = min(range_s // tf, 1500)
|
|
|
|
cum = 0
|
|
result = []
|
|
for i in range(n):
|
|
bar_delta = rng.gauss(0, 50)
|
|
cum += bar_delta
|
|
result.append({
|
|
"time": start + i * tf,
|
|
"value": round(cum, 1),
|
|
"bar_delta": round(bar_delta, 1),
|
|
})
|
|
return result
|
|
|
|
|
|
def demo_volume_profile(symbol: str):
|
|
"""Generate a realistic volume profile."""
|
|
rng = _stable_rng(symbol, 0, 0)
|
|
base = _base_price(symbol)
|
|
tick = _tick_size(symbol)
|
|
n_levels = 60
|
|
|
|
# Bell-curve volume distribution (P-shape / D-shape / b-shape)
|
|
shape = rng.choice(["P-shape", "D-shape", "b-shape", "Balanced"])
|
|
center = base + rng.uniform(-base * 0.002, base * 0.002)
|
|
|
|
volume_at_price = {}
|
|
prices = []
|
|
for i in range(n_levels):
|
|
price = round(center - (n_levels // 2 - i) * tick, 5)
|
|
prices.append(price)
|
|
|
|
# Gaussian volume distribution
|
|
dist = abs(i - n_levels // 2) / (n_levels / 4)
|
|
vol = int(max(10, 500 * math.exp(-dist * dist) + rng.randint(5, 50)))
|
|
volume_at_price[str(price)] = vol
|
|
|
|
# Find POC (max volume)
|
|
poc_price = max(volume_at_price, key=volume_at_price.get)
|
|
poc = float(poc_price)
|
|
|
|
# Value area = 70% of volume
|
|
sorted_levels = sorted(volume_at_price.items(), key=lambda x: x[1], reverse=True)
|
|
total_vol = sum(v for _, v in sorted_levels)
|
|
va_vol = 0
|
|
va_prices = []
|
|
for p, v in sorted_levels:
|
|
va_vol += v
|
|
va_prices.append(float(p))
|
|
if va_vol >= total_vol * 0.7:
|
|
break
|
|
|
|
vah = max(va_prices)
|
|
val = min(va_prices)
|
|
|
|
return [{
|
|
"poc": round(poc, 5),
|
|
"vah": round(vah, 5),
|
|
"val": round(val, 5),
|
|
"total_volume": total_vol,
|
|
"shape": shape,
|
|
"poc_position_pct": 50.0 + rng.uniform(-15, 15),
|
|
"lvn_levels": [round(prices[n_levels // 4], 5), round(prices[3 * n_levels // 4], 5)],
|
|
"volume_at_price": volume_at_price,
|
|
}]
|
|
|
|
|
|
def demo_bias(symbol: str):
|
|
"""Generate daily bias data."""
|
|
rng = _stable_rng(symbol, 0, 1)
|
|
base = _base_price(symbol)
|
|
tick = _tick_size(symbol)
|
|
direction = rng.choice(["long", "short", "neutral"])
|
|
confidence = rng.randint(40, 95)
|
|
shape = rng.choice(["P-shape", "D-shape", "b-shape", "Balanced"])
|
|
|
|
poc = round(base + rng.uniform(-base * 0.001, base * 0.001), 5)
|
|
spread = base * 0.003
|
|
vah = round(poc + spread, 5)
|
|
val = round(poc - spread, 5)
|
|
|
|
levels = []
|
|
n_levels = rng.randint(1, 4)
|
|
for _ in range(n_levels):
|
|
lv_dir = rng.choice(["buy", "sell"])
|
|
lv_price = round(base + rng.uniform(-base * 0.005, base * 0.005), 5)
|
|
levels.append({
|
|
"price": lv_price,
|
|
"direction": lv_dir,
|
|
"level_type": rng.choice(["POC", "VAH", "VAL", "LVN", "Composite"]),
|
|
"strength": rng.randint(50, 100),
|
|
})
|
|
|
|
return {
|
|
"direction": direction,
|
|
"confidence": confidence,
|
|
"profile_shape": shape,
|
|
"poc": poc,
|
|
"vah": vah,
|
|
"val": val,
|
|
"qualified_levels": levels,
|
|
"notes": f"Demo bias — {shape} profile detected, {direction} bias at {confidence}%",
|
|
}
|
|
|
|
|
|
def demo_orderbook(symbol: str):
|
|
"""Generate a realistic orderbook snapshot."""
|
|
rng = _stable_rng(symbol, 0, 2)
|
|
base = _base_price(symbol)
|
|
tick = _tick_size(symbol)
|
|
n_levels = 20
|
|
|
|
mid = base + rng.uniform(-tick * 2, tick * 2)
|
|
bids = []
|
|
asks = []
|
|
|
|
for i in range(n_levels):
|
|
bid_price = round(mid - (i + 1) * tick, 5)
|
|
ask_price = round(mid + (i + 1) * tick, 5)
|
|
bid_size = rng.randint(5, 300)
|
|
ask_size = rng.randint(5, 300)
|
|
|
|
# Add some thin levels (sweep targets)
|
|
if rng.random() < 0.15:
|
|
bid_size = rng.randint(1, 5)
|
|
if rng.random() < 0.15:
|
|
ask_size = rng.randint(1, 5)
|
|
|
|
bids.append({"price": bid_price, "size": bid_size})
|
|
asks.append({"price": ask_price, "size": ask_size})
|
|
|
|
total_bid = sum(b["size"] for b in bids)
|
|
total_ask = sum(a["size"] for a in asks)
|
|
|
|
return {
|
|
"snapshot": True,
|
|
"bids": bids,
|
|
"asks": asks,
|
|
"last_price": round(mid, 5),
|
|
"spread": round(tick, 5),
|
|
"bid_total": total_bid,
|
|
"ask_total": total_ask,
|
|
"imbalance": round(total_bid / max(total_bid + total_ask, 1) * 100, 1),
|
|
}
|
|
|
|
|
|
def demo_strategy_status(symbol: str):
|
|
"""Generate a strategy status with step checklist."""
|
|
rng = _stable_rng(symbol, 0, 3)
|
|
base = _base_price(symbol)
|
|
direction = rng.choice(["buy", "sell"])
|
|
bias_dir = "long" if direction == "buy" else "short"
|
|
|
|
# Pick a random phase
|
|
phases = [
|
|
("WAITING_FOR_PRICE", 2),
|
|
("AT_LEVEL_SCANNING", 3),
|
|
("WATCHING", 4),
|
|
("ENTRY_READY", 5),
|
|
]
|
|
overall, steps_done = rng.choice(phases)
|
|
|
|
steps = [
|
|
{"name": "Volume Profile", "icon": "📊", "status": "completed", "detail": "D-shape identified, POC at " + str(round(base, 1))},
|
|
{"name": "Daily Bias", "icon": "🧭", "status": "completed", "detail": f"{bias_dir.upper()} bias — confidence 78%"},
|
|
{"name": "Qualified Level", "icon": "📍", "status": "completed" if steps_done >= 3 else "pending",
|
|
"detail": f"{'VAH rejection zone at ' + str(round(base * 1.002, 1)) if steps_done >= 3 else 'Scanning for level...'}"},
|
|
{"name": "Orderflow Confirm", "icon": "🔬", "status": "completed" if steps_done >= 4 else "pending",
|
|
"detail": "Absorption detected (3 attempts)" if steps_done >= 4 else "Waiting for orderflow signal..."},
|
|
{"name": "Entry Trigger", "icon": "🎯", "status": "active" if steps_done >= 5 else "pending",
|
|
"detail": "Initiative buying confirmed" if steps_done >= 5 else "Waiting for trigger..."},
|
|
{"name": "Trade Management", "icon": "⚙️", "status": "pending", "detail": "Not in trade"},
|
|
]
|
|
|
|
return {
|
|
"overall": overall,
|
|
"reason": f"Price approaching qualified level — {overall.replace('_', ' ').lower()}",
|
|
"steps": steps[:6],
|
|
"bias_direction": bias_dir,
|
|
"bias_confidence": rng.randint(60, 95),
|
|
"current_price": round(base, 5),
|
|
"trade": None,
|
|
}
|
|
|
|
|
|
def demo_scanner():
|
|
"""Generate scanner data for all demo instruments."""
|
|
rng = _stable_rng("__scanner__", 0, 0)
|
|
results = []
|
|
statuses = ["WAITING_FOR_PRICE", "AT_LEVEL_SCANNING", "WATCHING",
|
|
"ENTRY_READY", "IDLE", "WAITING_FOR_PRICE"]
|
|
|
|
for i, sym in enumerate(_BASE_PRICES):
|
|
overall = statuses[i % len(statuses)]
|
|
steps_done = rng.randint(0, 5)
|
|
bias_dir = rng.choice(["buy", "sell", "neutral"])
|
|
|
|
results.append({
|
|
"symbol": sym,
|
|
"overall": overall,
|
|
"reason": f"Demo — {overall.replace('_', ' ').lower()}",
|
|
"priority": rng.randint(10, 90),
|
|
"steps_done": steps_done,
|
|
"steps_total": 6,
|
|
"bias_direction": bias_dir,
|
|
"bias_confidence": rng.randint(30, 95),
|
|
"current_price": round(_base_price(sym), 5),
|
|
})
|
|
|
|
results.sort(key=lambda x: x["priority"], reverse=True)
|
|
return results
|
|
|
|
|
|
def demo_markers(symbol: str):
|
|
"""Generate a few chart markers for demo mode."""
|
|
rng = _stable_rng(symbol, 0, 4)
|
|
base = _base_price(symbol)
|
|
now = int(time.time())
|
|
markers = []
|
|
|
|
types = [
|
|
("ABS", "#26a69a", "arrowUp", "belowBar"),
|
|
("INIT", "#66bb6a", "arrowUp", "belowBar"),
|
|
("SWEEP", "#ab47bc", "arrowDown", "aboveBar"),
|
|
("EXHAUST", "#ffeb3b", "circle", "aboveBar"),
|
|
("DIV", "#ff9800", "circle", "aboveBar"),
|
|
]
|
|
|
|
for i in range(8):
|
|
t, color, shape, pos = rng.choice(types)
|
|
markers.append({
|
|
"time": now - rng.randint(300, 80000),
|
|
"position": pos,
|
|
"color": color,
|
|
"shape": shape,
|
|
"text": t,
|
|
})
|
|
|
|
markers.sort(key=lambda x: x["time"])
|
|
return markers
|
|
|
|
|
|
def demo_footprint(symbol: str, tf: int = 60, range_s: int = 86400):
|
|
"""Generate footprint chart data with bid/ask at each price level."""
|
|
rng = _stable_rng(symbol, tf, range_s)
|
|
base = _base_price(symbol)
|
|
tick = _tick_size(symbol)
|
|
now = int(time.time())
|
|
start = now - range_s
|
|
n_bars = min(range_s // tf, 200)
|
|
|
|
vol = 0.0004 if base > 1000 else 0.0008
|
|
walk = _gen_price_walk(base, n_bars + 1, vol, rng=rng)
|
|
bars = []
|
|
|
|
for i in range(n_bars):
|
|
t = start + i * tf
|
|
o = walk[i]
|
|
c = walk[i + 1]
|
|
h = max(o, c) + abs(rng.gauss(0, base * vol * 0.5))
|
|
l = min(o, c) - abs(rng.gauss(0, base * vol * 0.5))
|
|
|
|
# Generate levels from low to high at tick increments
|
|
n_levels = max(3, int((h - l) / tick))
|
|
n_levels = min(n_levels, 60) # cap
|
|
levels = []
|
|
max_vol = 0
|
|
poc_price = None
|
|
|
|
for j in range(n_levels):
|
|
price = round(l + j * tick, 5)
|
|
# Volume distribution — more near open/close, absorption zones
|
|
dist_from_mid = abs(price - (o + c) / 2) / max(h - l, tick)
|
|
base_vol = max(1, int(80 * math.exp(-dist_from_mid * 2)))
|
|
|
|
# Simulate bid/ask imbalance
|
|
if price < (o + c) / 2:
|
|
bid = base_vol + rng.randint(0, 40)
|
|
ask = max(1, base_vol - rng.randint(0, 20))
|
|
else:
|
|
bid = max(1, base_vol - rng.randint(0, 20))
|
|
ask = base_vol + rng.randint(0, 40)
|
|
|
|
# Random absorption spikes
|
|
if rng.random() < 0.08:
|
|
bid = bid * rng.randint(3, 6)
|
|
if rng.random() < 0.08:
|
|
ask = ask * rng.randint(3, 6)
|
|
|
|
total = bid + ask
|
|
if total > max_vol:
|
|
max_vol = total
|
|
poc_price = price
|
|
|
|
levels.append({"price": price, "bid": bid, "ask": ask})
|
|
|
|
bars.append({
|
|
"time": t,
|
|
"open": round(o, 5),
|
|
"high": round(h, 5),
|
|
"low": round(l, 5),
|
|
"close": round(c, 5),
|
|
"poc": poc_price,
|
|
"levels": levels,
|
|
})
|
|
|
|
return bars
|
|
|
|
|
|
def demo_tape_trades(symbol: str, count: int = 60):
|
|
"""Generate recent time & sales trades for initial tape fill."""
|
|
rng = _stable_rng(symbol, 0, 5)
|
|
base = _base_price(symbol)
|
|
tick = _tick_size(symbol)
|
|
now = time.time()
|
|
trades = []
|
|
price = base
|
|
for i in range(count):
|
|
price += tick * rng.choice([-2, -1, -1, 0, 1, 1, 2])
|
|
side = rng.choice(["buy", "sell"])
|
|
size = rng.randint(1, 50)
|
|
# occasional big trades
|
|
if rng.random() < 0.08:
|
|
size = rng.randint(80, 500)
|
|
trades.append({
|
|
"time": now - (count - i) * rng.uniform(0.3, 2.0),
|
|
"price": round(price, 5),
|
|
"size": size,
|
|
"side": side,
|
|
})
|
|
return trades
|
|
|
|
|
|
def demo_microstructure(symbol: str):
|
|
"""Generate a complete microstructure snapshot for initial panel fill."""
|
|
rng = _stable_rng(symbol, 0, 6)
|
|
base = _base_price(symbol)
|
|
direction = rng.choice(["buy", "sell"])
|
|
market_state = rng.choice(["TRENDING", "COMPRESSION", "REBALANCING"])
|
|
sessions = {
|
|
"London Open": 3600000 * 2,
|
|
"NY Open": 3600000 * 4,
|
|
"NY AM": 3600000 * 3,
|
|
"London PM": 3600000 * 1,
|
|
"Asia": 3600000 * 6,
|
|
}
|
|
session_name = rng.choice(list(sessions.keys()))
|
|
return {
|
|
"marketState": market_state,
|
|
"session": {
|
|
"name": session_name,
|
|
"remaining": sessions[session_name],
|
|
},
|
|
"absorption": {
|
|
"level": round(base + rng.uniform(-base * 0.001, base * 0.001), 5),
|
|
"attempts": rng.randint(1, 4),
|
|
"strength": rng.randint(30, 95),
|
|
"side": direction,
|
|
},
|
|
"initiative": {
|
|
"count": rng.randint(0, 5),
|
|
"direction": "up" if direction == "buy" else "down",
|
|
"strength": rng.randint(40, 90),
|
|
},
|
|
"delta": {
|
|
"cumulative": round(rng.uniform(-5000, 5000), 1),
|
|
"direction": rng.uniform(-1, 1),
|
|
"divergence": rng.random() < 0.2,
|
|
},
|
|
"exhaustion": rng.randint(10, 80),
|
|
"patterns": [
|
|
{
|
|
"type": rng.choice(["absorption", "initiative", "exhaustion", "sweep", "divergence"]),
|
|
"confidence": round(rng.uniform(0.5, 0.95), 2),
|
|
"price": round(base + rng.uniform(-base * 0.002, base * 0.002), 5),
|
|
}
|
|
for _ in range(rng.randint(1, 4))
|
|
],
|
|
}
|
|
|
|
|
|
def demo_signals():
|
|
"""Generate institutional-grade demo signals with deep trade analysis."""
|
|
now = int(time.time() * 1000)
|
|
signals = []
|
|
|
|
# ── Detailed pattern library with full narrative context ──
|
|
_setups = [
|
|
{
|
|
"pattern": "Bid Absorption",
|
|
"signal_type": "absorption",
|
|
"narrative": "Institutional buyers are defending {price:.2f} with repeated absorption. {abscount} rejection attempts in the last {minutes}min — each time sellers hit the bid, resting limit orders immediately refill. This is classic accumulation behavior at a key demand zone.",
|
|
"thesis": "Large passive buyers are accumulating. Once the selling pressure is exhausted, expect an aggressive markup move as trapped shorts cover.",
|
|
"edge": "Aggressive sellers are being absorbed at the bid, creating a floor. The orderbook shows {bookimb}% bid-heavy imbalance. Delta is confirming net buying pressure despite the flat price — this divergence suggests hidden accumulation.",
|
|
"invalidation": "Setup fails if {sl:.2f} breaks on volume > 2x average, indicating absorption wall has been removed and genuine supply is present.",
|
|
"htf_context": "HTF context: {htf_bias} on the {htf_tf} with price {htf_position} of the value area. {poc_context}",
|
|
},
|
|
{
|
|
"pattern": "Initiative Auction",
|
|
"signal_type": "initiative_auction",
|
|
"narrative": "Aggressive directional buying detected at {price:.2f}. Market orders are overwhelming the ask side — {init_count} initiative sweeps in the last {minutes}min. Order flow shows {delta_dir} delta acceleration with zero absorption resistance above.",
|
|
"thesis": "Smart money is initiating a move. Market-order aggression + thin liquidity above = high probability of follow-through. The auction is being driven, not responding.",
|
|
"edge": "Initiative buyers are lifting every ask level aggressively. The footprint shows {delta_val:+.0f} net delta in the most recent bars with ask-side depletion. This is not just buying — it's urgent, informed buying that sweeps through resting orders.",
|
|
"invalidation": "Watch for exhaustion candle (long upper wick, declining delta). If initiative volume drops >50% within 3 bars, the move may stall.",
|
|
"htf_context": "HTF alignment: {htf_bias} on {htf_tf}. Price breaking out of {htf_position}, confirmed by higher-timeframe delta momentum.",
|
|
},
|
|
{
|
|
"pattern": "Selling Exhaustion",
|
|
"signal_type": "exhaustion",
|
|
"narrative": "Selling pressure is dying at {price:.2f}. Despite making new lows, each successive push has declining delta: {delta_seq}. Volume is dropping on downside tests — sellers are losing conviction.",
|
|
"thesis": "Diminishing seller follow-through after multiple downside tests = exhaustion. The market is running out of sellers at this level. Expect mean reversion as shorts take profit and new buyers step in.",
|
|
"edge": "Three-test exhaustion pattern: each low is made on declining delta and volume. The delta divergence ({delta_div_pct}% weaker on last push vs first) indicates seller capitulation. Footprint shows ask volume shifting from initiative to responsive.",
|
|
"invalidation": "If fresh initiative selling appears with accelerating delta on the 4th push, exhaustion thesis is negated — treat as breakdown.",
|
|
"htf_context": "Higher timeframe: {htf_bias} with price at {htf_position}. {poc_context} Exhaustion at this level is consistent with HTF demand.",
|
|
},
|
|
{
|
|
"pattern": "Liquidity Sweep",
|
|
"signal_type": "book_sweep",
|
|
"narrative": "Stop-hunt complete at {price:.2f}. Price pierced below {sweep_level:.2f} to trigger clustered stops, then immediately reversed with {reversal_vol} contracts of aggressive buying. Classic institutional liquidity grab.",
|
|
"thesis": "Smart money engineered a liquidity sweep below the obvious support to fill their orders. The immediate reversal with high volume confirms this was a manufactured move, not a genuine breakdown.",
|
|
"edge": "The sweep cleared {stops_cleared} stops at {sweep_level:.2f} and the bid immediately reloaded with {reload_vol} contracts. The V-shaped reversal candle with positive delta ({sweep_delta:+.0f}) confirms aggressive re-entry. Book imbalance flipped from {pre_imb}% ask to {post_imb}% bid within seconds.",
|
|
"invalidation": "If price returns to the sweep low within 15min, the liquidity engineered thesis is invalid — real supply exists below.",
|
|
"htf_context": "HTF positioning: {htf_bias} with sweep occurring at {htf_position}. This level aligns with {poc_context}",
|
|
},
|
|
{
|
|
"pattern": "Delta Divergence",
|
|
"signal_type": "delta_divergence",
|
|
"narrative": "Bearish delta divergence at {price:.2f}. Price made a new high but cumulative delta is {delta_val:+.0f} — {delta_pct}% lower than the previous swing high. Buyers are losing control despite higher prices.",
|
|
"thesis": "Divergence between price and orderflow is an early warning of trend exhaustion. Smart money is distributing into the rally — selling into strength while retail chases the breakout.",
|
|
"edge": "Three indicators confirm distribution: (1) Declining delta on new highs, (2) Increasing ask-side volume in the footprint, (3) Bid depth withdrawing from {depth_from:.2f}-{depth_to:.2f} range. The composite signal has been historically reliable at VP extremes.",
|
|
"invalidation": "Divergence thesis is invalidated if delta accelerates positive on a fresh breakout with initiative buying above {tp:.2f}.",
|
|
"htf_context": "HTF: {htf_bias}. Price is at {htf_position} — a common distribution zone. {poc_context}",
|
|
},
|
|
{
|
|
"pattern": "Composite POC Bounce",
|
|
"signal_type": "poc_bounce",
|
|
"narrative": "Price is testing the composite POC at {poc_level:.2f} — the highest volume node over {days}D. This level has attracted {poc_reactions} reactions in the last 5 sessions with an average bounce of {poc_avg_bounce:.1f}%.",
|
|
"thesis": "The composite POC is 'fair value' — where the most business was transacted. Price tends to rotate around this level. A reaction here with bid absorption confirmation suggests value-buyers are defending the level.",
|
|
"edge": "The POC at {poc_level:.2f} has a volume node of {poc_volume} contracts. The current test shows {abscount} absorption events with bid-side delta strengthening. The footprint profile shows responsive buying appearing at each POC test — institutions are re-accumulating at fair value.",
|
|
"invalidation": "If the POC breaks with initiative selling and delta acceleration, the value area is shifting. Expect a rotation to VAL at {val_level:.2f}.",
|
|
"htf_context": "HTF trend: {htf_bias}. The composite POC at {htf_position}. {poc_context}",
|
|
}
|
|
]
|
|
|
|
_sessions = [
|
|
{"name": "London Open", "detail": "High liquidity, typically large directional moves as European institutions set positioning"},
|
|
{"name": "NY Open", "detail": "Peak volatility as US institutions react to overnight flow and European positioning"},
|
|
{"name": "NY AM", "detail": "Continuation or reversal of NY Open initiative — strongest volume period"},
|
|
{"name": "London PM", "detail": "European close — profit-taking and positioning ahead of US session"},
|
|
{"name": "Asia", "detail": "Lower volume, range-bound. Watch for accumulation/distribution patterns"},
|
|
]
|
|
|
|
_models = ["Fabio Rejection", "Absorption → Initiative", "Sweep & Reverse", "LVN Bounce", "Value Area Rotation", "Composite"]
|
|
|
|
_htf_biases = [
|
|
("Bullish", "above POC", "Price is in the upper value area, favoring longs on pullbacks"),
|
|
("Bullish", "between POC and VAH", "Healthy uptrend — pullbacks to POC are high-probability entries"),
|
|
("Bearish", "below POC", "Price is below fair value, favoring shorts on rallies"),
|
|
("Bearish", "between VAL and POC", "Selling pressure dominant — rallies into POC are distribution zones"),
|
|
("Neutral", "at POC", "Price is at fair value with no clear directional edge — wait for initiative break"),
|
|
]
|
|
|
|
_market_regimes = [
|
|
{"state": "Trending", "detail": "Strong directional move underway — initiative activity dominating. Favor with-trend entries."},
|
|
{"state": "Ranging", "detail": "Balanced market, rotating between value extremes. Fade the edges, avoid the middle."},
|
|
{"state": "Balanced Volatile", "detail": "Wide range with high participation — institutional battle zone. Wait for resolution."},
|
|
{"state": "Low Volume Grind", "detail": "Thin market, easily manipulated. Reduce size, widen stops, avoid illiquid breakouts."},
|
|
{"state": "Breakout", "detail": "Value area migration in progress — new balance forming. Trail initiative entries."},
|
|
]
|
|
|
|
_blockers_pool = [
|
|
{"text": "High-impact news (NFP/FOMC) in next 30min — defer entry until after release", "severity": "high"},
|
|
{"text": "Spread widening to 3x average — liquidity deteriorating, slippage risk elevated", "severity": "high"},
|
|
{"text": "Counter-trend signal — trade against daily bias, reduce position to 50%", "severity": "medium"},
|
|
{"text": "Near session close — limited follow-through time, consider passing", "severity": "medium"},
|
|
{"text": "VIX spike detected — stop-hunt risk elevated, widen stops or reduce size", "severity": "medium"},
|
|
{"text": "Correlated asset divergence — BTCUSDT and NAS100 moving opposite, caution", "severity": "low"},
|
|
]
|
|
|
|
rng = _stable_rng("__signals__", 0, 7)
|
|
for i in range(8):
|
|
sym = rng.choice(list(_BASE_PRICES.keys()))
|
|
direction = rng.choice(["buy", "sell"])
|
|
setup = rng.choice(_setups)
|
|
score = rng.randint(45, 98)
|
|
session = rng.choice(_sessions)
|
|
regime = rng.choice(_market_regimes)
|
|
htf = rng.choice(_htf_biases)
|
|
model = rng.choice(_models)
|
|
|
|
base = _base_price(sym)
|
|
tick = _tick_size(sym)
|
|
entry = round(base + rng.uniform(-base * 0.001, base * 0.001), 5)
|
|
sl_dist = base * rng.uniform(0.001, 0.003)
|
|
tp_dist = sl_dist * rng.uniform(1.5, 3.5)
|
|
|
|
if direction == "buy":
|
|
sl = round(entry - sl_dist, 5)
|
|
tp = round(entry + tp_dist, 5)
|
|
else:
|
|
sl = round(entry + sl_dist, 5)
|
|
tp = round(entry - tp_dist, 5)
|
|
|
|
risk = abs(entry - sl)
|
|
reward = abs(tp - entry)
|
|
rr = round(reward / risk, 1) if risk > 0 else 0
|
|
|
|
bias_dir = "long" if direction == "buy" else "short"
|
|
|
|
# Generate rich context values for template formatting
|
|
ctx = {
|
|
"price": entry, "sl": sl, "tp": tp,
|
|
"abscount": rng.randint(2, 6),
|
|
"minutes": rng.randint(5, 30),
|
|
"bookimb": rng.randint(60, 88),
|
|
"htf_bias": htf[0],
|
|
"htf_tf": rng.choice(["4H", "1D", "Weekly"]),
|
|
"htf_position": htf[1],
|
|
"poc_context": htf[2],
|
|
"init_count": rng.randint(3, 8),
|
|
"delta_dir": "positive" if direction == "buy" else "negative",
|
|
"delta_val": rng.uniform(500, 5000) * (1 if direction == "buy" else -1),
|
|
"delta_seq": f"{rng.randint(-800,-200)} → {rng.randint(-600,-100)} → {rng.randint(-300,-30)}",
|
|
"delta_div_pct": rng.randint(30, 65),
|
|
"delta_pct": rng.randint(15, 50),
|
|
"sweep_level": round(entry - (sl_dist * 0.7 * (1 if direction == "buy" else -1)), 5),
|
|
"reversal_vol": rng.randint(150, 800),
|
|
"stops_cleared": rng.randint(40, 200),
|
|
"reload_vol": rng.randint(200, 600),
|
|
"sweep_delta": rng.uniform(300, 2000) * (1 if direction == "buy" else -1),
|
|
"pre_imb": rng.randint(55, 75),
|
|
"post_imb": rng.randint(60, 85),
|
|
"depth_from": round(entry - base * 0.002, 2),
|
|
"depth_to": round(entry + base * 0.002, 2),
|
|
"poc_level": round(entry + rng.uniform(-base * 0.001, base * 0.001), 5),
|
|
"val_level": round(entry - base * rng.uniform(0.003, 0.006), 5),
|
|
"days": rng.choice([5, 10, 20]),
|
|
"poc_reactions": rng.randint(3, 8),
|
|
"poc_avg_bounce": rng.uniform(0.2, 0.8),
|
|
"poc_volume": rng.randint(5000, 50000),
|
|
}
|
|
|
|
# Format the deep narrative templates
|
|
try:
|
|
narrative = setup["narrative"].format(**ctx)
|
|
thesis = setup["thesis"]
|
|
edge = setup["edge"].format(**ctx)
|
|
invalidation = setup["invalidation"].format(**ctx)
|
|
htf_context = setup["htf_context"].format(**ctx)
|
|
except (KeyError, ValueError):
|
|
narrative = f"{setup['pattern']} detected at {entry:.2f}"
|
|
thesis = setup["thesis"]
|
|
edge = f"Confidence {score}%"
|
|
invalidation = f"Stop loss at {sl:.2f}"
|
|
htf_context = f"{htf[0]} bias on higher timeframes"
|
|
|
|
# Build ordered reasons chain
|
|
reasons = [
|
|
f"1. {setup['pattern']} detected at {entry:.5g}",
|
|
f"2. Daily bias: {bias_dir.upper()} ({htf[0]} on {ctx['htf_tf']})",
|
|
f"3. Session: {session['name']} — {session['detail'][:60]}",
|
|
f"4. {regime['state']}: {regime['detail'][:60]}",
|
|
]
|
|
if rng.random() > 0.3:
|
|
reasons.append(f"5. Orderbook imbalance {ctx['bookimb']}% on {'bid' if direction == 'buy' else 'ask'} side")
|
|
if rng.random() > 0.4:
|
|
reasons.append(f"6. Composite VP POC confluence at {ctx['poc_level']:.5g}")
|
|
|
|
# Blockers
|
|
blockers = []
|
|
if score < 60:
|
|
b = rng.choice(_blockers_pool)
|
|
blockers.append(b["text"])
|
|
if rng.random() < 0.25:
|
|
b = rng.choice(_blockers_pool)
|
|
if b["text"] not in blockers:
|
|
blockers.append(b["text"])
|
|
|
|
# Quality grade
|
|
grade = "A+" if score >= 85 else "A" if score >= 70 else "B" if score >= 55 else "C"
|
|
|
|
# Multi-timeframe confluence checklist
|
|
mtf_confluence = {
|
|
"weekly_bias": rng.choice(["Bullish", "Bearish", "Neutral"]),
|
|
"daily_bias": htf[0],
|
|
"h4_trend": rng.choice(["Uptrend", "Downtrend", "Sideways"]),
|
|
"h1_structure": rng.choice(["Higher highs", "Lower lows", "Range-bound", "Breakout"]),
|
|
"m15_trigger": setup["pattern"],
|
|
}
|
|
|
|
signals.append({
|
|
"id": now - i * 100000 + rng.randint(0, 999),
|
|
"timestamp": now - i * rng.randint(60000, 600000),
|
|
"timestamp_ms": now - i * rng.randint(60000, 600000),
|
|
"symbol": sym,
|
|
"direction": "long" if direction == "buy" else "short",
|
|
"confidence": score / 100,
|
|
"composite_score": score,
|
|
"action": "enter" if score >= 70 else "alert_only",
|
|
"pattern": setup["pattern"],
|
|
"signal_type": setup["signal_type"],
|
|
"entry": entry,
|
|
"stopLoss": sl,
|
|
"takeProfit": tp,
|
|
"entry_price": entry,
|
|
"suggested_sl": sl,
|
|
"suggested_tp": tp,
|
|
"riskReward": rr,
|
|
"bias": bias_dir.upper(),
|
|
"session": session["name"],
|
|
"session_detail": session["detail"],
|
|
"model": model,
|
|
"grade": grade,
|
|
"reasons": reasons,
|
|
"blockers": blockers,
|
|
"notes": f"{setup['pattern']} — {grade} grade — score {score}%",
|
|
"signals": [{"signal_type": setup["signal_type"], "confidence": score}],
|
|
# ── Deep analysis fields ──
|
|
"narrative": narrative,
|
|
"thesis": thesis,
|
|
"edge": edge,
|
|
"invalidation": invalidation,
|
|
"htf_context": htf_context,
|
|
"market_regime": regime,
|
|
"mtf_confluence": mtf_confluence,
|
|
# ── Orderflow metrics ──
|
|
"market_state": regime["state"],
|
|
"volume_context": rng.choice(["Above Average", "Normal", "Below Average", "Spiking"]),
|
|
"key_level_type": rng.choice(["POC", "VAH", "VAL", "LVN", "Composite Node"]),
|
|
"key_level_price": round(entry + rng.uniform(-base * 0.001, base * 0.001), 5),
|
|
"absorption_count": ctx["abscount"] if "absorption" in setup["signal_type"] else rng.randint(0, 3),
|
|
"delta_confirm": rng.choice([True, False]),
|
|
"delta_value": round(ctx["delta_val"], 1),
|
|
"initiative_strength": rng.randint(40, 100),
|
|
"book_imbalance_pct": ctx["bookimb"],
|
|
"footprint_summary": f"{'Bid' if direction == 'buy' else 'Ask'}-heavy profile with {('absorption at bid' if direction == 'buy' else 'supply at ask')}. POC at {ctx['poc_level']:.5g}, delta {'+' if direction == 'buy' else '-'}{abs(ctx['delta_val']):.0f}",
|
|
})
|
|
return signals
|