Spaces:
Runtime error
Runtime error
Upload 6 files
Browse files- app.py +473 -0
- data_manager.py +183 -0
- strategy_1_bb_reentry.py +166 -0
- strategy_2_doji_ichimoku.py +273 -0
- strategy_3_trend_range_volume.py +146 -0
- telegram_config.json +4 -0
app.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Main Python Processor Script (Master Script)
|
| 2 |
+
# Integrates Data Manager, Three Strategies, Signal Aggregation, and Custom Terminal UI with RICH
|
| 3 |
+
|
| 4 |
+
import time
|
| 5 |
+
import json
|
| 6 |
+
import threading
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
from typing import Dict, Any, List, Optional
|
| 9 |
+
|
| 10 |
+
# --- Import RICH Components ---
|
| 11 |
+
from rich.console import Console
|
| 12 |
+
from rich.layout import Layout
|
| 13 |
+
from rich.panel import Panel
|
| 14 |
+
from rich.table import Table
|
| 15 |
+
from rich.text import Text
|
| 16 |
+
from rich.live import Live
|
| 17 |
+
from rich.style import Style
|
| 18 |
+
|
| 19 |
+
# --- Import Core Components ---
|
| 20 |
+
from data_manager import BinanceDataManager
|
| 21 |
+
from strategy_1_bb_reentry import analyze_strategy_1
|
| 22 |
+
from strategy_2_doji_ichimoku import analyze_strategy_2, CandleData
|
| 23 |
+
from strategy_3_trend_range_volume import analyze_strategy_3
|
| 24 |
+
|
| 25 |
+
# --- Configuration ---
|
| 26 |
+
CONFIG = {
|
| 27 |
+
'MARKET_SCAN_DELAY_MS': 1000, # Delay between analyzing each market
|
| 28 |
+
'SIGNAL_AGGREGATION_WINDOW_MIN': 7, # Time window for official signal aggregation
|
| 29 |
+
'TELEGRAM_CONFIG_FILE': 'telegram_config.json',
|
| 30 |
+
'MAX_LOG_LINES': 50,
|
| 31 |
+
'MAX_ACTIVE_SIGNALS': 10,
|
| 32 |
+
'MAX_MARKETS': 500 # Max symbols to fetch
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
# --- RICH Styles and Colors (Based on User Request) ---
|
| 36 |
+
class Styles:
|
| 37 |
+
# Strategy Colors (User requested: Light Blue, Orange, Pistachio Green)
|
| 38 |
+
STRAT_1_COLOR = "cyan"
|
| 39 |
+
STRAT_2_COLOR = "orange3"
|
| 40 |
+
STRAT_3_COLOR = "green3"
|
| 41 |
+
|
| 42 |
+
# Dynamic Colors for Border (User requested 150 colors, alternating)
|
| 43 |
+
BORDER_COLORS = ["red", "green", "blue", "yellow", "magenta", "cyan", "white", "bright_red", "bright_green", "bright_blue", "bright_yellow", "bright_magenta", "bright_cyan", "bright_white"]
|
| 44 |
+
# We will cycle through these 14 colors for visual effect. 150 is too many for rich's standard palette.
|
| 45 |
+
BORDER_COLOR_INDEX = 0
|
| 46 |
+
|
| 47 |
+
# Log Colors (User requested: Pink/Fuchsia, Canary Yellow, Purple/Violet)
|
| 48 |
+
LOG_SIGNAL = Style(color="magenta", bold=True) # Pink/Fuchsia for successful signal
|
| 49 |
+
LOG_NETWORK = Style(color="yellow", bold=False) # Canary Yellow for network/connections
|
| 50 |
+
LOG_CONFIG = Style(color="purple", bold=False) # Purple/Violet for config/personal
|
| 51 |
+
LOG_INFO = Style(color="blue", bold=False)
|
| 52 |
+
LOG_WARNING = Style(color="yellow", bold=True)
|
| 53 |
+
LOG_ERROR = Style(color="red", bold=True)
|
| 54 |
+
LOG_CRITICAL = Style(color="white", bgcolor="red", bold=True)
|
| 55 |
+
|
| 56 |
+
# Signal Colors
|
| 57 |
+
SIGNAL_LONG = Style(color="green", bold=True)
|
| 58 |
+
SIGNAL_SHORT = Style(color="red", bold=True)
|
| 59 |
+
HEADER = Style(color="white", bgcolor="dark_blue", bold=True)
|
| 60 |
+
|
| 61 |
+
# Light Colors
|
| 62 |
+
LIGHT_ON = "bold green"
|
| 63 |
+
LIGHT_OFF = "dim white"
|
| 64 |
+
|
| 65 |
+
# --- State Management ---
|
| 66 |
+
class State:
|
| 67 |
+
def __init__(self):
|
| 68 |
+
self.data_manager = BinanceDataManager()
|
| 69 |
+
self.logs: List[Dict[str, Any]] = []
|
| 70 |
+
self.active_signals: Dict[str, Dict[str, Any]] = {} # {symbol: {strategy_id: signal_data}}
|
| 71 |
+
self.official_signals: List[Dict[str, Any]] = []
|
| 72 |
+
self.stats = {'total_cycles': 0, 'total_signals': 0}
|
| 73 |
+
self.is_running = True
|
| 74 |
+
self.telegram_config = self._load_telegram_config()
|
| 75 |
+
self.console = Console()
|
| 76 |
+
self.last_strat_log: Dict[int, str] = {1: "Waiting...", 2: "Waiting...", 3: "Waiting..."}
|
| 77 |
+
self.last_analyzed_symbol: str = "N/A"
|
| 78 |
+
self.strategy_names = {
|
| 79 |
+
1: "BB Re-entry",
|
| 80 |
+
2: "Doji Box/Ichimoku",
|
| 81 |
+
3: "Trend/Volume Scalper"
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
def _load_telegram_config(self):
|
| 85 |
+
try:
|
| 86 |
+
with open(CONFIG['TELEGRAM_CONFIG_FILE'], 'r') as f:
|
| 87 |
+
return json.load(f)
|
| 88 |
+
except FileNotFoundError:
|
| 89 |
+
self.add_log('WARNING', 'Telegram config file not found. Telegram notifications disabled.', Styles.LOG_CONFIG)
|
| 90 |
+
return None
|
| 91 |
+
except json.JSONDecodeError:
|
| 92 |
+
self.add_log('ERROR', 'Invalid Telegram config file format.', Styles.LOG_CONFIG)
|
| 93 |
+
return None
|
| 94 |
+
|
| 95 |
+
def add_log(self, type: str, message: str, style: Style = Styles.LOG_INFO):
|
| 96 |
+
timestamp = datetime.now().strftime('%H:%M:%S')
|
| 97 |
+
self.logs.append({'time': timestamp, 'type': type, 'message': message, 'style': style})
|
| 98 |
+
if len(self.logs) > CONFIG['MAX_LOG_LINES']:
|
| 99 |
+
self.logs.pop(0)
|
| 100 |
+
|
| 101 |
+
# Update last strategy log for UI
|
| 102 |
+
if type.startswith('STRATEGY'):
|
| 103 |
+
strat_id = int(type.split(' ')[1])
|
| 104 |
+
self.last_strat_log[strat_id] = message
|
| 105 |
+
|
| 106 |
+
def update_active_signals(self, symbol: str, strategy_id: int, signal_data: Dict[str, Any]):
|
| 107 |
+
if symbol not in self.active_signals:
|
| 108 |
+
self.active_signals[symbol] = {}
|
| 109 |
+
self.active_signals[symbol][strategy_id] = {
|
| 110 |
+
'signal': signal_data['signal'],
|
| 111 |
+
'time': datetime.now(),
|
| 112 |
+
'entry_price': signal_data.get('curr_close') or signal_data.get('entry_price')
|
| 113 |
+
}
|
| 114 |
+
self.stats['total_signals'] += 1
|
| 115 |
+
|
| 116 |
+
def check_official_signal(self, symbol: str):
|
| 117 |
+
"""Checks for aggregation: same direction in all 3 strategies within the time window."""
|
| 118 |
+
if symbol not in self.active_signals:
|
| 119 |
+
return None
|
| 120 |
+
|
| 121 |
+
signals = self.active_signals[symbol]
|
| 122 |
+
if len(signals) < 3:
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
# Get the most recent signal time
|
| 126 |
+
latest_time = max(s['time'] for s in signals.values())
|
| 127 |
+
|
| 128 |
+
# Check if all signals are within the time window
|
| 129 |
+
time_window = timedelta(minutes=CONFIG['SIGNAL_AGGREGATION_WINDOW_MIN'])
|
| 130 |
+
is_aggregated = all(latest_time - s['time'] <= time_window for s in signals.values())
|
| 131 |
+
|
| 132 |
+
if not is_aggregated:
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
# Check if all signals have the same direction (BUY/LONG or SELL/SHORT)
|
| 136 |
+
directions = [s['signal'].replace('LONG', 'BUY').replace('SHORT', 'SELL') for s in signals.values()]
|
| 137 |
+
if len(set(directions)) == 1 and directions[0] in ['BUY', 'SELL']:
|
| 138 |
+
|
| 139 |
+
# Check if this signal has already been issued
|
| 140 |
+
if any(s['symbol'] == symbol and s['direction'] == directions[0] and s['status'] == 'ACTIVE' for s in self.official_signals):
|
| 141 |
+
return None
|
| 142 |
+
|
| 143 |
+
# Official Signal Found
|
| 144 |
+
official_signal = {
|
| 145 |
+
'symbol': symbol,
|
| 146 |
+
'direction': directions[0],
|
| 147 |
+
'entry_price': signals[1]['entry_price'], # Use entry from Strategy 1 as base
|
| 148 |
+
'time': latest_time,
|
| 149 |
+
'status': 'ACTIVE',
|
| 150 |
+
'pnl_percent': 0.0,
|
| 151 |
+
'pnl_start_time': datetime.now()
|
| 152 |
+
}
|
| 153 |
+
self.official_signals.append(official_signal)
|
| 154 |
+
self.add_log('OFFICIAL SIGNAL', f"Aggregated {directions[0]} signal for {symbol}!", Styles.LOG_SIGNAL)
|
| 155 |
+
self._send_telegram_alert(official_signal)
|
| 156 |
+
return official_signal
|
| 157 |
+
|
| 158 |
+
return None
|
| 159 |
+
|
| 160 |
+
def _send_telegram_alert(self, signal: Dict[str, Any]):
|
| 161 |
+
"""Placeholder for Telegram notification logic."""
|
| 162 |
+
if not self.telegram_config:
|
| 163 |
+
return
|
| 164 |
+
|
| 165 |
+
# User requested format: 3 circles + Direction + Symbol + Entry Price
|
| 166 |
+
direction = signal['direction']
|
| 167 |
+
symbol = signal['symbol']
|
| 168 |
+
entry = signal['entry_price']
|
| 169 |
+
|
| 170 |
+
circle = '🟢' if direction == 'BUY' else '🔴'
|
| 171 |
+
|
| 172 |
+
message = f"{circle * 3} {direction} {symbol} @ {entry:.4f} {circle * 3}"
|
| 173 |
+
|
| 174 |
+
# In a real scenario, we would use a library like python-telegram-bot here
|
| 175 |
+
# to send the message using self.telegram_config['token'] and self.telegram_config['chat_id'].
|
| 176 |
+
self.add_log('NETWORK', f"Telegram alert sent: {message}", Styles.LOG_NETWORK)
|
| 177 |
+
|
| 178 |
+
def update_pnl(self, symbol: str, current_price: float):
|
| 179 |
+
"""Updates PnL for active official signals."""
|
| 180 |
+
for signal in self.official_signals:
|
| 181 |
+
if signal['symbol'] == symbol and signal['status'] == 'ACTIVE':
|
| 182 |
+
entry = signal['entry_price']
|
| 183 |
+
if signal['direction'] == 'BUY':
|
| 184 |
+
pnl = ((current_price - entry) / entry) * 100
|
| 185 |
+
else: # SELL
|
| 186 |
+
pnl = ((entry - current_price) / entry) * 100
|
| 187 |
+
|
| 188 |
+
signal['pnl_percent'] = pnl
|
| 189 |
+
|
| 190 |
+
# Check for 5% profit exit (User's request)
|
| 191 |
+
if pnl >= 5.0:
|
| 192 |
+
signal['status'] = 'CLOSED_TP'
|
| 193 |
+
self.add_log('SUCCESS', f"TP HIT: {symbol} closed at +{pnl:.2f}%", Styles.LOG_SIGNAL)
|
| 194 |
+
# Check for -5% loss exit (Implied SL for simplicity, as user didn't provide SL)
|
| 195 |
+
elif pnl <= -5.0:
|
| 196 |
+
signal['status'] = 'CLOSED_SL'
|
| 197 |
+
self.add_log('ERROR', f"SL HIT: {symbol} closed at {pnl:.2f}%", Styles.LOG_ERROR)
|
| 198 |
+
|
| 199 |
+
# --- RICH UI Rendering ---
|
| 200 |
+
|
| 201 |
+
def make_header(state: State) -> Panel:
|
| 202 |
+
"""Creates the main header panel with center-aligned, colorful title."""
|
| 203 |
+
|
| 204 |
+
# Cycle border color
|
| 205 |
+
current_color = Styles.BORDER_COLORS[Styles.BORDER_COLOR_INDEX % len(Styles.BORDER_COLORS)]
|
| 206 |
+
Styles.BORDER_COLOR_INDEX = (Styles.BORDER_COLOR_INDEX + 1) % len(Styles.BORDER_COLORS)
|
| 207 |
+
|
| 208 |
+
title_text = Text("TERMINAL TAHLILGAR CRYPTO", justify="center")
|
| 209 |
+
|
| 210 |
+
info_text = Text.assemble(
|
| 211 |
+
(f"Cycle: {state.data_manager.cycle_count} | ", Style(color="white")),
|
| 212 |
+
(f"Market: {state.data_manager.market_index}/{len(state.data_manager.symbols)} | ", Style(color="white")),
|
| 213 |
+
(f"Analyzing: {state.last_analyzed_symbol} | ", Style(color="yellow")),
|
| 214 |
+
(f"Active Signals: {len([s for s in state.official_signals if s['status'] == 'ACTIVE'])}", Style(color="green"))
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
return Panel(
|
| 218 |
+
title_text,
|
| 219 |
+
title=f"[bold {current_color}]MASTER PROCESSOR[/bold {current_color}]",
|
| 220 |
+
border_style=current_color,
|
| 221 |
+
subtitle=info_text
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
def make_strategy_panel(state: State, strat_id: int, color: str) -> Panel:
|
| 225 |
+
"""Creates a single strategy status panel."""
|
| 226 |
+
|
| 227 |
+
title = state.strategy_names[strat_id]
|
| 228 |
+
|
| 229 |
+
# Cycle border color
|
| 230 |
+
current_color = Styles.BORDER_COLORS[Styles.BORDER_COLOR_INDEX % len(Styles.BORDER_COLORS)]
|
| 231 |
+
Styles.BORDER_COLOR_INDEX = (Styles.BORDER_COLOR_INDEX + 1) % len(Styles.BORDER_COLORS)
|
| 232 |
+
|
| 233 |
+
# Check if the last analyzed symbol has a signal from this strategy
|
| 234 |
+
has_signal = strat_id in state.active_signals.get(state.last_analyzed_symbol, {})
|
| 235 |
+
|
| 236 |
+
# Light status
|
| 237 |
+
light = Text("●", style=Styles.LIGHT_ON) if has_signal else Text("○", style=Styles.LIGHT_OFF)
|
| 238 |
+
|
| 239 |
+
# Content
|
| 240 |
+
content = Text.assemble(
|
| 241 |
+
(f"{light} ", Style(color=color)),
|
| 242 |
+
(f"Last Log: {state.last_strat_log[strat_id]}", Style(color="white"))
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
return Panel(
|
| 246 |
+
content,
|
| 247 |
+
title=f"[bold {color}]{title}[/bold {color}]",
|
| 248 |
+
border_style=color,
|
| 249 |
+
height=3
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
def make_signal_table(state: State) -> Panel:
|
| 253 |
+
"""Creates the large official signal table panel."""
|
| 254 |
+
table = Table(
|
| 255 |
+
title="[bold white]OFFICIAL AGGREGATED SIGNALS (MAIN OUTPUT)[/bold white]",
|
| 256 |
+
show_header=True,
|
| 257 |
+
header_style=Styles.HEADER,
|
| 258 |
+
border_style="white",
|
| 259 |
+
title_style=Styles.HEADER
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
table.add_column("Indicator", style="white", justify="center")
|
| 263 |
+
table.add_column("Time", style="white", justify="center")
|
| 264 |
+
table.add_column("Symbol", style="cyan", justify="left")
|
| 265 |
+
table.add_column("Entry Price", style="white", justify="right")
|
| 266 |
+
table.add_column("PnL%", style="white", justify="right")
|
| 267 |
+
|
| 268 |
+
active_signals = [s for s in state.official_signals if s['status'] == 'ACTIVE']
|
| 269 |
+
|
| 270 |
+
if not active_signals:
|
| 271 |
+
table.add_row(Text("No active official signals.", style="dim yellow"), "", "", "", "")
|
| 272 |
+
else:
|
| 273 |
+
for signal in active_signals:
|
| 274 |
+
direction = signal['direction']
|
| 275 |
+
pnl = signal['pnl_percent']
|
| 276 |
+
|
| 277 |
+
# Direction Indicator (Circle + Letter)
|
| 278 |
+
dir_style = Styles.SIGNAL_LONG if direction == 'BUY' else Styles.SIGNAL_SHORT
|
| 279 |
+
dir_circle = Text("●", style=dir_style)
|
| 280 |
+
dir_letter = Text("L" if direction == 'BUY' else "S", style=dir_style)
|
| 281 |
+
indicator = Text.assemble(dir_circle, " ", dir_letter)
|
| 282 |
+
|
| 283 |
+
# Time Elapsed
|
| 284 |
+
elapsed = datetime.now() - signal['pnl_start_time']
|
| 285 |
+
minutes = int(elapsed.total_seconds() // 60)
|
| 286 |
+
seconds = int(elapsed.total_seconds() % 60)
|
| 287 |
+
time_str = f"{minutes:02d}m {seconds:02d}s"
|
| 288 |
+
|
| 289 |
+
# PnL
|
| 290 |
+
pnl_style = Styles.SIGNAL_LONG if pnl >= 0 else Styles.SIGNAL_SHORT
|
| 291 |
+
pnl_text = Text(f"{pnl:+.2f}%", style=pnl_style)
|
| 292 |
+
|
| 293 |
+
table.add_row(
|
| 294 |
+
indicator,
|
| 295 |
+
time_str,
|
| 296 |
+
signal['symbol'],
|
| 297 |
+
f"{signal['entry_price']:.4f}",
|
| 298 |
+
pnl_text
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
return Panel(table, title="[bold white]Official Signals[/bold white]", border_style="white")
|
| 302 |
+
|
| 303 |
+
def make_logs_panel(state: State) -> Panel:
|
| 304 |
+
"""Creates the system logs panel."""
|
| 305 |
+
log_text = Text()
|
| 306 |
+
for log in state.logs:
|
| 307 |
+
log_text.append(f"[{log['time']}] {log['type']}: {log['message']}\n", style=log['style'])
|
| 308 |
+
|
| 309 |
+
# Cycle border color
|
| 310 |
+
current_color = Styles.BORDER_COLORS[Styles.BORDER_COLOR_INDEX % len(Styles.BORDER_COLORS)]
|
| 311 |
+
Styles.BORDER_COLOR_INDEX = (Styles.BORDER_COLOR_INDEX + 1) % len(Styles.BORDER_COLORS)
|
| 312 |
+
|
| 313 |
+
return Panel(
|
| 314 |
+
log_text,
|
| 315 |
+
title="[bold white]LOGS[/bold white]",
|
| 316 |
+
border_style=current_color,
|
| 317 |
+
height=15,
|
| 318 |
+
# Autoscroll is handled by always appending to the Text object and rich's rendering
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
def render_ui(state: State) -> Layout:
|
| 322 |
+
"""Renders the complete UI layout."""
|
| 323 |
+
layout = Layout(name="root")
|
| 324 |
+
|
| 325 |
+
layout.split(
|
| 326 |
+
Layout(name="header", size=5),
|
| 327 |
+
Layout(name="strategy_status", ratio=1),
|
| 328 |
+
Layout(name="signal_table", ratio=2),
|
| 329 |
+
Layout(name="logs", ratio=3)
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
layout["header"].update(make_header(state))
|
| 333 |
+
|
| 334 |
+
layout["strategy_status"].split_row(
|
| 335 |
+
make_strategy_panel(state, 1, Styles.STRAT_1_COLOR),
|
| 336 |
+
make_strategy_panel(state, 2, Styles.STRAT_2_COLOR),
|
| 337 |
+
make_strategy_panel(state, 3, Styles.STRAT_3_COLOR)
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
layout["signal_table"].update(make_signal_table(state))
|
| 341 |
+
layout["logs"].update(make_logs_panel(state))
|
| 342 |
+
|
| 343 |
+
return layout
|
| 344 |
+
|
| 345 |
+
# --- Main Execution Loop ---
|
| 346 |
+
def main_loop(state: State, live: Live):
|
| 347 |
+
|
| 348 |
+
# 1. Initial setup
|
| 349 |
+
state.add_log('INFO', 'Initializing Master Processor...', Styles.LOG_CONFIG)
|
| 350 |
+
state.data_manager.fetch_symbols(limit=CONFIG['MAX_MARKETS'])
|
| 351 |
+
|
| 352 |
+
# 2. Main Scan Loop
|
| 353 |
+
while state.is_running:
|
| 354 |
+
symbol = state.data_manager.get_next_symbol()
|
| 355 |
+
if not symbol:
|
| 356 |
+
state.add_log('WARNING', 'No symbols to scan. Retrying in 10s.', Styles.LOG_WARNING)
|
| 357 |
+
time.sleep(10)
|
| 358 |
+
continue
|
| 359 |
+
|
| 360 |
+
state.last_analyzed_symbol = symbol
|
| 361 |
+
state.add_log('INFO', f"Analyzing {symbol}...", Styles.LOG_INFO)
|
| 362 |
+
|
| 363 |
+
# 3. Get Data for all strategies
|
| 364 |
+
# Strategy 2 requires max data (15m: 100, 1m: 50)
|
| 365 |
+
data = state.data_manager.get_data_for_strategy(symbol, 2)
|
| 366 |
+
klines_15m = data.get('klines_15m', [])
|
| 367 |
+
klines_1m = data.get('klines_1m', [])
|
| 368 |
+
|
| 369 |
+
if not klines_15m or not klines_1m:
|
| 370 |
+
state.add_log('ERROR', f"Failed to get data for {symbol}", Styles.LOG_ERROR)
|
| 371 |
+
# Render UI to show error
|
| 372 |
+
live.update(render_ui(state))
|
| 373 |
+
time.sleep(CONFIG['MARKET_SCAN_DELAY_MS'] / 1000)
|
| 374 |
+
continue
|
| 375 |
+
|
| 376 |
+
# 4. Run Strategies
|
| 377 |
+
|
| 378 |
+
# Strategy 1: BB Re-entry (needs 15m)
|
| 379 |
+
result_1 = analyze_strategy_1(klines_15m)
|
| 380 |
+
if result_1['signal'] in ['BUY', 'SELL']:
|
| 381 |
+
state.update_active_signals(symbol, 1, result_1)
|
| 382 |
+
|
| 383 |
+
# User requested to include symbol and color in the log
|
| 384 |
+
direction = result_1['signal']
|
| 385 |
+
log_style = Styles.SIGNAL_LONG if direction == 'BUY' else Styles.SIGNAL_SHORT
|
| 386 |
+
log_message = f"Signal {direction} for {symbol} @ {result_1['curr_close']:.4f}"
|
| 387 |
+
|
| 388 |
+
state.add_log('STRATEGY 1', log_message, log_style)
|
| 389 |
+
|
| 390 |
+
# Strategy 2: Doji Box (needs 15m and 1m)
|
| 391 |
+
result_2 = analyze_strategy_2(klines_15m, klines_1m)
|
| 392 |
+
if result_2['signal'] in ['LONG', 'SHORT']:
|
| 393 |
+
state.update_active_signals(symbol, 2, result_2)
|
| 394 |
+
|
| 395 |
+
# User requested to include symbol and color in the log
|
| 396 |
+
direction = result_2['signal']
|
| 397 |
+
log_style = Styles.SIGNAL_LONG if direction == 'LONG' else Styles.SIGNAL_SHORT
|
| 398 |
+
log_message = f"Signal {direction} for {symbol} @ {result_2['entry_price']:.4f}"
|
| 399 |
+
|
| 400 |
+
state.add_log('STRATEGY 2', log_message, log_style)
|
| 401 |
+
|
| 402 |
+
# Strategy 3: Trend/Volume Scalper (needs 3m or 1m)
|
| 403 |
+
klines_3m = data.get('klines_3m', [])
|
| 404 |
+
klines_1m_for_strat3 = data.get('klines_1m', [])
|
| 405 |
+
|
| 406 |
+
# Use 3m if available, otherwise use 1m
|
| 407 |
+
klines_for_strat3 = klines_3m if klines_3m else klines_1m_for_strat3
|
| 408 |
+
|
| 409 |
+
if not klines_for_strat3:
|
| 410 |
+
state.add_log('ERROR', f"Failed to get 3m/1m data for Strategy 3 on {symbol}", Styles.LOG_ERROR)
|
| 411 |
+
else:
|
| 412 |
+
result_3 = analyze_strategy_3(klines_for_strat3)
|
| 413 |
+
if result_3['signal'] in ['BUY', 'SELL']:
|
| 414 |
+
state.update_active_signals(symbol, 3, result_3)
|
| 415 |
+
|
| 416 |
+
# User requested to include symbol and color in the log
|
| 417 |
+
direction = result_3['signal']
|
| 418 |
+
log_style = Styles.SIGNAL_LONG if direction == 'BUY' else Styles.SIGNAL_SHORT
|
| 419 |
+
log_message = f"Signal {direction} for {symbol} ({result_3['trend']})"
|
| 420 |
+
|
| 421 |
+
state.add_log('STRATEGY 3', log_message, log_style)
|
| 422 |
+
|
| 423 |
+
# 5. Check for Official Signal
|
| 424 |
+
state.check_official_signal(symbol)
|
| 425 |
+
|
| 426 |
+
# 6. Update PnL for active signals
|
| 427 |
+
# Fetch current price for PnL update
|
| 428 |
+
current_price_data = state.data_manager._fetch_api('/fapi/v1/ticker/price', {'symbol': symbol})
|
| 429 |
+
if current_price_data and 'price' in current_price_data:
|
| 430 |
+
current_price = float(current_price_data['price'])
|
| 431 |
+
state.update_pnl(symbol, current_price)
|
| 432 |
+
|
| 433 |
+
# 7. Render UI
|
| 434 |
+
live.update(render_ui(state))
|
| 435 |
+
|
| 436 |
+
# 8. Wait for next market scan
|
| 437 |
+
time.sleep(CONFIG['MARKET_SCAN_DELAY_MS'] / 1000)
|
| 438 |
+
|
| 439 |
+
# --- Entry Point ---
|
| 440 |
+
if __name__ == '__main__':
|
| 441 |
+
# Create a dummy telegram_config.json for demonstration
|
| 442 |
+
dummy_config = {
|
| 443 |
+
"token": "YOUR_TELEGRAM_BOT_TOKEN",
|
| 444 |
+
"chat_id": "YOUR_TELEGRAM_CHAT_ID"
|
| 445 |
+
}
|
| 446 |
+
try:
|
| 447 |
+
with open(CONFIG['TELEGRAM_CONFIG_FILE'], 'w') as f:
|
| 448 |
+
json.dump(dummy_config, f, indent=4)
|
| 449 |
+
except Exception as e:
|
| 450 |
+
print(f"Error creating telegram_config.json: {e}")
|
| 451 |
+
|
| 452 |
+
state = State()
|
| 453 |
+
|
| 454 |
+
try:
|
| 455 |
+
with Live(render_ui(state), screen=True, refresh_per_second=4) as live:
|
| 456 |
+
# Run a few cycles for demonstration
|
| 457 |
+
for _ in range(5): # Run 5 cycles of the market list
|
| 458 |
+
main_loop(state, live)
|
| 459 |
+
if not state.is_running:
|
| 460 |
+
break
|
| 461 |
+
except KeyboardInterrupt:
|
| 462 |
+
state.is_running = False
|
| 463 |
+
state.console.print("\n[bold red]Master Processor stopped by user.[/bold red]")
|
| 464 |
+
except Exception as e:
|
| 465 |
+
state.add_log('CRITICAL', f"Loop Error: {e}", Styles.LOG_CRITICAL)
|
| 466 |
+
state.console.print(f"\n[bold red]An unexpected error occurred:[/bold red] {e}")
|
| 467 |
+
finally:
|
| 468 |
+
state.console.print("\n[bold green]Master Processor finished execution.[/bold green]")
|
| 469 |
+
state.console.print("[bold white]Final Official Signals:[/bold white]")
|
| 470 |
+
for s in state.official_signals:
|
| 471 |
+
pnl_style = Styles.SIGNAL_LONG if s['pnl_percent'] >= 0 else Styles.SIGNAL_SHORT
|
| 472 |
+
pnl_text = Text(f"{s['pnl_percent']:+.2f}%", style=pnl_style)
|
| 473 |
+
state.console.print(f"[{s['status']}] {s['direction']} {s['symbol']} @ {s['entry_price']:.4f} | PnL: {pnl_text}")
|
data_manager.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Centralized Data Management System for Binance Futures API
|
| 2 |
+
|
| 3 |
+
import requests
|
| 4 |
+
import time
|
| 5 |
+
from typing import List, Dict, Any, Optional
|
| 6 |
+
|
| 7 |
+
# --- Configuration ---
|
| 8 |
+
API_BASE = 'https://fapi.binance.com'
|
| 9 |
+
MAX_SYMBOLS = 500
|
| 10 |
+
# Strategy 1 needs: 15m, limit=25 (20+5)
|
| 11 |
+
# Strategy 2 needs: 15m, limit=100; 1m, limit=50
|
| 12 |
+
# Strategy 3 needs: 15m, limit=51 (50+1)
|
| 13 |
+
# Max required klines: 15m (100), 1m (50)
|
| 14 |
+
|
| 15 |
+
class BinanceDataManager:
|
| 16 |
+
"""
|
| 17 |
+
Manages market list and provides kline data from Binance Futures API.
|
| 18 |
+
Handles caching to minimize API calls and respect rate limits.
|
| 19 |
+
"""
|
| 20 |
+
def __init__(self):
|
| 21 |
+
self.symbols: List[str] = []
|
| 22 |
+
self.kline_cache: Dict[str, Dict[str, List[Dict[str, Any]]]] = {} # {symbol: {interval: [klines]}}
|
| 23 |
+
self.last_fetch_time: Dict[str, float] = {} # {symbol_interval: timestamp}
|
| 24 |
+
self.market_index = 0
|
| 25 |
+
self.cycle_count = 0
|
| 26 |
+
self.required_intervals = ['1m', '3m', '15m']
|
| 27 |
+
|
| 28 |
+
def _fetch_api(self, endpoint: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
| 29 |
+
"""Generic API fetcher with basic error handling."""
|
| 30 |
+
url = f"{API_BASE}{endpoint}"
|
| 31 |
+
try:
|
| 32 |
+
response = requests.get(url, params=params, timeout=10)
|
| 33 |
+
response.raise_for_status()
|
| 34 |
+
return response.json()
|
| 35 |
+
except requests.exceptions.RequestException as e:
|
| 36 |
+
print(f"API Error fetching {endpoint} with params {params}: {e}")
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
def fetch_symbols(self, limit: int = MAX_SYMBOLS) -> List[str]:
|
| 40 |
+
"""Fetches the list of active USDT perpetual futures symbols."""
|
| 41 |
+
print("Fetching active symbols from Binance...")
|
| 42 |
+
data = self._fetch_api('/fapi/v1/exchangeInfo', {})
|
| 43 |
+
if data and 'symbols' in data:
|
| 44 |
+
symbols = [
|
| 45 |
+
s['symbol'] for s in data['symbols']
|
| 46 |
+
if s['contractType'] == 'PERPETUAL' and s['symbol'].endswith('USDT') and s['status'] == 'TRADING'
|
| 47 |
+
]
|
| 48 |
+
self.symbols = symbols[:limit]
|
| 49 |
+
print(f"Successfully loaded {len(self.symbols)} symbols.")
|
| 50 |
+
return self.symbols
|
| 51 |
+
|
| 52 |
+
print("Failed to fetch symbols. Using fallback list.")
|
| 53 |
+
self.symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'ADAUSDT'] # Fallback
|
| 54 |
+
return self.symbols
|
| 55 |
+
|
| 56 |
+
def get_klines(self, symbol: str, interval: str, limit: int) -> List[Dict[str, Any]]:
|
| 57 |
+
"""
|
| 58 |
+
Retrieves klines for a symbol and interval, using cache if available.
|
| 59 |
+
The kline data format is converted to a dictionary for easier access:
|
| 60 |
+
{'openTime': int, 'open': float, 'high': float, 'low': float, 'close': float, 'volume': float}
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
# Determine required limit based on strategies
|
| 64 |
+
if interval == '15m':
|
| 65 |
+
required_limit = 100 # Max needed by Strategy 2
|
| 66 |
+
elif interval == '1m':
|
| 67 |
+
required_limit = 50 # Max needed by Strategy 2
|
| 68 |
+
elif interval == '3m':
|
| 69 |
+
required_limit = 51 # Max needed by Strategy 3 (LR_PERIOD=50 + 1)
|
| 70 |
+
else:
|
| 71 |
+
required_limit = limit
|
| 72 |
+
|
| 73 |
+
# Check cache and freshness (refresh every 1.5 * interval duration)
|
| 74 |
+
cache_key = f"{symbol}_{interval}"
|
| 75 |
+
current_time = time.time()
|
| 76 |
+
|
| 77 |
+
# Simple heuristic for refresh time: 1.5 * interval in seconds
|
| 78 |
+
interval_seconds = self._interval_to_seconds(interval)
|
| 79 |
+
refresh_threshold = self.last_fetch_time.get(cache_key, 0) + (interval_seconds * 1.5)
|
| 80 |
+
|
| 81 |
+
if cache_key in self.kline_cache and current_time < refresh_threshold:
|
| 82 |
+
# Cache hit and fresh enough
|
| 83 |
+
klines = self.kline_cache[symbol].get(interval, [])
|
| 84 |
+
if len(klines) >= required_limit:
|
| 85 |
+
return klines[-required_limit:]
|
| 86 |
+
|
| 87 |
+
# Fetch from API
|
| 88 |
+
params = {'symbol': symbol, 'interval': interval, 'limit': required_limit}
|
| 89 |
+
data = self._fetch_api('/fapi/v1/klines', params)
|
| 90 |
+
|
| 91 |
+
if data:
|
| 92 |
+
klines = []
|
| 93 |
+
for c in data:
|
| 94 |
+
klines.append({
|
| 95 |
+
'openTime': c[0],
|
| 96 |
+
'open': float(c[1]),
|
| 97 |
+
'high': float(c[2]),
|
| 98 |
+
'low': float(c[3]),
|
| 99 |
+
'close': float(c[4]),
|
| 100 |
+
'volume': float(c[5])
|
| 101 |
+
})
|
| 102 |
+
|
| 103 |
+
# Update cache
|
| 104 |
+
if symbol not in self.kline_cache:
|
| 105 |
+
self.kline_cache[symbol] = {}
|
| 106 |
+
self.kline_cache[symbol][interval] = klines
|
| 107 |
+
self.last_fetch_time[cache_key] = current_time
|
| 108 |
+
|
| 109 |
+
return klines[-required_limit:]
|
| 110 |
+
|
| 111 |
+
return []
|
| 112 |
+
|
| 113 |
+
def _interval_to_seconds(self, interval: str) -> int:
|
| 114 |
+
"""Converts Binance interval string to seconds."""
|
| 115 |
+
if interval.endswith('m'):
|
| 116 |
+
return int(interval[:-1]) * 60
|
| 117 |
+
elif interval.endswith('h'):
|
| 118 |
+
return int(interval[:-1]) * 3600
|
| 119 |
+
elif interval.endswith('d'):
|
| 120 |
+
return int(interval[:-1]) * 86400
|
| 121 |
+
return 60 # Default to 1 minute
|
| 122 |
+
|
| 123 |
+
def get_next_symbol(self) -> Optional[str]:
|
| 124 |
+
"""Returns the next symbol in the round-robin cycle."""
|
| 125 |
+
if not self.symbols:
|
| 126 |
+
self.fetch_symbols()
|
| 127 |
+
if not self.symbols:
|
| 128 |
+
return None
|
| 129 |
+
|
| 130 |
+
if self.market_index >= len(self.symbols):
|
| 131 |
+
self.market_index = 0
|
| 132 |
+
self.cycle_count += 1
|
| 133 |
+
print(f"\n--- Starting new market scan cycle: #{self.cycle_count} ---")
|
| 134 |
+
|
| 135 |
+
symbol = self.symbols[self.market_index]
|
| 136 |
+
self.market_index += 1
|
| 137 |
+
return symbol
|
| 138 |
+
|
| 139 |
+
def get_data_for_strategy(self, symbol: str, strategy_id: int) -> Dict[str, Any]:
|
| 140 |
+
"""
|
| 141 |
+
Provides all necessary data for a given strategy and symbol.
|
| 142 |
+
This is the main interface for the strategy scripts.
|
| 143 |
+
"""
|
| 144 |
+
data = {}
|
| 145 |
+
|
| 146 |
+
if strategy_id == 1:
|
| 147 |
+
# Strategy 1 (BB Re-entry): 15m klines, limit=25
|
| 148 |
+
data['klines_15m'] = self.get_klines(symbol, '15m', 25)
|
| 149 |
+
elif strategy_id == 2:
|
| 150 |
+
# Strategy 2 (Doji Box): 15m klines, limit=100; 1m klines, limit=50
|
| 151 |
+
data['klines_15m'] = self.get_klines(symbol, '15m', 100)
|
| 152 |
+
data['klines_1m'] = self.get_klines(symbol, '1m', 50)
|
| 153 |
+
elif strategy_id == 3:
|
| 154 |
+
# Strategy 3 (Trend/Range/Volume): 3m klines, limit=51 (Scalping request)
|
| 155 |
+
# Try 3m first, fallback to 1m if 3m fails to return data
|
| 156 |
+
klines_3m = self.get_klines(symbol, '3m', 51)
|
| 157 |
+
if klines_3m:
|
| 158 |
+
data['klines_3m'] = klines_3m
|
| 159 |
+
else:
|
| 160 |
+
data['klines_1m'] = self.get_klines(symbol, '1m', 51)
|
| 161 |
+
|
| 162 |
+
return data
|
| 163 |
+
|
| 164 |
+
if __name__ == '__main__':
|
| 165 |
+
# Example usage
|
| 166 |
+
manager = BinanceDataManager()
|
| 167 |
+
manager.fetch_symbols(limit=10)
|
| 168 |
+
|
| 169 |
+
symbol = manager.get_next_symbol()
|
| 170 |
+
if symbol:
|
| 171 |
+
print(f"\nAnalyzing {symbol} for Strategy 2...")
|
| 172 |
+
data = manager.get_data_for_strategy(symbol, 2)
|
| 173 |
+
print(f"15m klines received: {len(data.get('klines_15m', []))}")
|
| 174 |
+
print(f"1m klines received: {len(data.get('klines_1m', []))}")
|
| 175 |
+
|
| 176 |
+
# Test cache
|
| 177 |
+
print(f"\nTesting cache for {symbol} (15m)...")
|
| 178 |
+
data_cached = manager.get_data_for_strategy(symbol, 1)
|
| 179 |
+
print(f"15m klines received (cached): {len(data_cached.get('klines_15m', []))}")
|
| 180 |
+
|
| 181 |
+
# Next symbol
|
| 182 |
+
next_symbol = manager.get_next_symbol()
|
| 183 |
+
print(f"\nNext symbol: {next_symbol}")
|
strategy_1_bb_reentry.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Strategy 1: Bollinger Band Re-entry with Flatness Check
|
| 2 |
+
# Original Timeframe: 15m
|
| 3 |
+
# Original BB Parameters: Period=20, StdDev=2
|
| 4 |
+
# Original Flatness Check: FLAT_CHECK_BARS=3, FLAT_THRESHOLD_PERCENT=0.0015
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
from typing import List, Dict, Any
|
| 8 |
+
|
| 9 |
+
# --- Core Indicator Functions ---
|
| 10 |
+
|
| 11 |
+
def sma(values: List[float]) -> float:
|
| 12 |
+
"""Simple Moving Average."""
|
| 13 |
+
if not values:
|
| 14 |
+
return 0.0
|
| 15 |
+
return np.mean(values)
|
| 16 |
+
|
| 17 |
+
def stddev(values: List[float]) -> float:
|
| 18 |
+
"""Standard Deviation."""
|
| 19 |
+
if not values:
|
| 20 |
+
return 0.0
|
| 21 |
+
mean = sma(values)
|
| 22 |
+
variance = np.mean([(v - mean) ** 2 for v in values])
|
| 23 |
+
return np.sqrt(variance)
|
| 24 |
+
|
| 25 |
+
def compute_bbands(closes: List[float], period: int = 20, std_dev_mul: int = 2) -> Dict[str, float]:
|
| 26 |
+
"""Compute Bollinger Bands for the last bar using prior 'period' closes."""
|
| 27 |
+
if len(closes) < period:
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
recent = closes[-period:]
|
| 31 |
+
ma = sma(recent)
|
| 32 |
+
sd = stddev(recent)
|
| 33 |
+
|
| 34 |
+
return {
|
| 35 |
+
'middle': ma,
|
| 36 |
+
'upper': ma + std_dev_mul * sd,
|
| 37 |
+
'lower': ma - std_dev_mul * sd,
|
| 38 |
+
'sd': sd
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
def rel_change(a: float, b: float) -> float:
|
| 42 |
+
"""Relative percent change."""
|
| 43 |
+
if a == 0:
|
| 44 |
+
return 0.0
|
| 45 |
+
return abs((b - a) / a)
|
| 46 |
+
|
| 47 |
+
def is_flat(series: List[float], threshold_percent: float = 0.0015) -> bool:
|
| 48 |
+
"""Check flatness of a series (returns true if changes are small)."""
|
| 49 |
+
if len(series) < 2:
|
| 50 |
+
return True
|
| 51 |
+
for i in range(1, len(series)):
|
| 52 |
+
if rel_change(series[i-1], series[i]) > threshold_percent:
|
| 53 |
+
return False
|
| 54 |
+
return True
|
| 55 |
+
|
| 56 |
+
# --- Main Analysis Function ---
|
| 57 |
+
|
| 58 |
+
def analyze_strategy_1(klines: List[Dict[str, float]],
|
| 59 |
+
bb_period: int = 20,
|
| 60 |
+
bb_std: int = 2,
|
| 61 |
+
flat_check_bars: int = 3,
|
| 62 |
+
flat_threshold_percent: float = 0.0015) -> Dict[str, Any]:
|
| 63 |
+
"""
|
| 64 |
+
Analyzes a symbol using the BB Re-entry with Flatness Check strategy.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
klines: List of candle data (must contain 'close', 'high', 'low').
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
A dictionary with 'signal' ('BUY', 'SELL', 'NO_SIGNAL') and debug data.
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
if len(klines) < bb_period + 2:
|
| 74 |
+
return {'signal': 'NO_DATA'}
|
| 75 |
+
|
| 76 |
+
closes = [k['close'] for k in klines]
|
| 77 |
+
|
| 78 |
+
# We need two sets of BBs: one for the bar *before* the current one (t-1)
|
| 79 |
+
# and one for the current bar (t).
|
| 80 |
+
# The original script uses closes_t_minus1 = closes[:-1] and closes_t = closes[1:]
|
| 81 |
+
# which is a bit unusual but we must replicate the logic.
|
| 82 |
+
|
| 83 |
+
# closes_t_minus1: All closes except the last one (used to calculate BB at t-1)
|
| 84 |
+
closes_t_minus1 = closes[:-1]
|
| 85 |
+
# closes_t: All closes except the first one (used to calculate BB at t)
|
| 86 |
+
closes_t = closes[1:]
|
| 87 |
+
|
| 88 |
+
# Ensure enough data for BB calculation
|
| 89 |
+
if len(closes_t_minus1) < bb_period or len(closes_t) < bb_period:
|
| 90 |
+
return {'signal': 'NO_DATA'}
|
| 91 |
+
|
| 92 |
+
# Calculate BB for t-1 (using the last 'period' closes from closes_t_minus1)
|
| 93 |
+
bb_t_minus1 = compute_bbands(closes_t_minus1, bb_period, bb_std)
|
| 94 |
+
# Calculate BB for t (using the last 'period' closes from closes_t)
|
| 95 |
+
bb_t = compute_bbands(closes_t, bb_period, bb_std)
|
| 96 |
+
|
| 97 |
+
if not bb_t_minus1 or not bb_t:
|
| 98 |
+
return {'signal': 'NO_DATA'}
|
| 99 |
+
|
| 100 |
+
# The two most recent candles
|
| 101 |
+
prev = klines[-2]
|
| 102 |
+
curr = klines[-1]
|
| 103 |
+
|
| 104 |
+
# --- Flatness Check ---
|
| 105 |
+
mids, uppers, lowers = [], [], []
|
| 106 |
+
# The original script iterates backwards from FLAT_CHECK_BARS to 1
|
| 107 |
+
for shift in range(flat_check_bars, 0, -1):
|
| 108 |
+
# endIdx = closes.length - shift
|
| 109 |
+
end_idx = len(closes) - shift
|
| 110 |
+
# window = closes.slice(endIdx - BB_PERIOD, endIdx)
|
| 111 |
+
window = closes[end_idx - bb_period : end_idx]
|
| 112 |
+
|
| 113 |
+
if len(window) < bb_period:
|
| 114 |
+
break
|
| 115 |
+
|
| 116 |
+
bb = compute_bbands(window, bb_period, bb_std)
|
| 117 |
+
if not bb:
|
| 118 |
+
break
|
| 119 |
+
|
| 120 |
+
mids.append(bb['middle'])
|
| 121 |
+
uppers.append(bb['upper'])
|
| 122 |
+
lowers.append(bb['lower'])
|
| 123 |
+
|
| 124 |
+
# Conditions for BUY (Re-entry from Upper Band)
|
| 125 |
+
# 1. Previous candle high broke the upper BB at t-1
|
| 126 |
+
cond_upper_broken_prev = prev['high'] > bb_t_minus1['upper']
|
| 127 |
+
# 2. Current candle close is back inside the upper BB at t
|
| 128 |
+
cond_upper_back_curr = curr['close'] < bb_t['upper']
|
| 129 |
+
# 3. Lower BB and Middle BB are flat
|
| 130 |
+
other_flat_for_upper = is_flat(lowers, flat_threshold_percent) and is_flat(mids, flat_threshold_percent)
|
| 131 |
+
|
| 132 |
+
# Conditions for SELL (Re-entry from Lower Band)
|
| 133 |
+
# 1. Previous candle low broke the lower BB at t-1
|
| 134 |
+
cond_lower_broken_prev = prev['low'] < bb_t_minus1['lower']
|
| 135 |
+
# 2. Current candle close is back inside the lower BB at t
|
| 136 |
+
cond_lower_back_curr = curr['close'] > bb_t['lower']
|
| 137 |
+
# 3. Upper BB and Middle BB are flat
|
| 138 |
+
other_flat_for_lower = is_flat(uppers, flat_threshold_percent) and is_flat(mids, flat_threshold_percent)
|
| 139 |
+
|
| 140 |
+
signal = 'NO_SIGNAL'
|
| 141 |
+
if cond_upper_broken_prev and cond_upper_back_curr and other_flat_for_upper:
|
| 142 |
+
signal = 'BUY'
|
| 143 |
+
elif cond_lower_broken_prev and cond_lower_back_curr and other_flat_for_lower:
|
| 144 |
+
signal = 'SELL'
|
| 145 |
+
|
| 146 |
+
return {
|
| 147 |
+
'signal': signal,
|
| 148 |
+
'prev_high': prev['high'],
|
| 149 |
+
'prev_low': prev['low'],
|
| 150 |
+
'curr_close': curr['close'],
|
| 151 |
+
'bb_prev': bb_t_minus1,
|
| 152 |
+
'bb_curr': bb_t,
|
| 153 |
+
'flat_checks': {
|
| 154 |
+
'mids': mids,
|
| 155 |
+
'uppers': uppers,
|
| 156 |
+
'lowers': lowers,
|
| 157 |
+
'other_flat_for_upper': other_flat_for_upper,
|
| 158 |
+
'other_flat_for_lower': other_flat_for_lower
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
if __name__ == '__main__':
|
| 163 |
+
# Example usage (dummy data for testing the logic structure)
|
| 164 |
+
# In the final system, this function will receive real data from the centralized data manager.
|
| 165 |
+
print("Strategy 1 (BB Re-entry) Analysis Logic Extracted.")
|
| 166 |
+
print("This script contains the pure logic and will be integrated into the main Python system.")
|
strategy_2_doji_ichimoku.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Strategy 2: Doji Box Breakout with Ichimoku/SR Confirmation
|
| 2 |
+
# Primary Timeframe: 15m (for SR, Doji Box)
|
| 3 |
+
# Secondary Timeframe: 1m (for Breakout confirmation)
|
| 4 |
+
|
| 5 |
+
from typing import List, Dict, Any, Optional
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
# --- Data Structures (Replicating Node.js Classes) ---
|
| 9 |
+
|
| 10 |
+
class CandleData:
|
| 11 |
+
def __init__(self, data: Dict[str, Any]):
|
| 12 |
+
# Data is a dictionary from data_manager.py: {'openTime': int, 'open': float, 'high': float, 'low': float, 'close': float, 'volume': float}
|
| 13 |
+
self.open_time = data['openTime']
|
| 14 |
+
self.open = data['open']
|
| 15 |
+
self.high = data['high']
|
| 16 |
+
self.low = data['low']
|
| 17 |
+
self.close = data['close']
|
| 18 |
+
self.volume = data['volume']
|
| 19 |
+
self.timestamp = self.open_time # Use open_time as timestamp for simplicity
|
| 20 |
+
|
| 21 |
+
def is_doji(self, threshold: float = 0.0001) -> bool:
|
| 22 |
+
"""Checks if the candle is a Doji (small body relative to price)."""
|
| 23 |
+
# Original script's definition of isDoji is not explicitly shown,
|
| 24 |
+
# but typically it means close is very near open. We'll use a standard proxy.
|
| 25 |
+
body_size = abs(self.close - self.open)
|
| 26 |
+
range_size = self.high - self.low
|
| 27 |
+
|
| 28 |
+
# A common definition: body is less than 10% of the total range
|
| 29 |
+
if range_size == 0: return True
|
| 30 |
+
return body_size / range_size < 0.1
|
| 31 |
+
|
| 32 |
+
class DojiBox:
|
| 33 |
+
def __init__(self, high: float, low: float, timestamp: int, index: int):
|
| 34 |
+
self.high = high
|
| 35 |
+
self.low = low
|
| 36 |
+
self.timestamp = timestamp
|
| 37 |
+
self.index = index
|
| 38 |
+
|
| 39 |
+
def check_breakout(self, candle: CandleData) -> Optional[str]:
|
| 40 |
+
"""Checks if the candle breaks the box."""
|
| 41 |
+
if candle.close > self.high and candle.open > self.high:
|
| 42 |
+
return 'LONG'
|
| 43 |
+
elif candle.close < self.low and candle.open < self.low:
|
| 44 |
+
return 'SHORT'
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
class SupportResistanceLevel:
|
| 48 |
+
def __init__(self, price: float, strength: float, touches: int, type: str):
|
| 49 |
+
self.price = price
|
| 50 |
+
self.strength = strength
|
| 51 |
+
self.touches = touches
|
| 52 |
+
self.type = type
|
| 53 |
+
|
| 54 |
+
def is_near(self, price: float, threshold: float = 0.002) -> bool:
|
| 55 |
+
"""Checks if a price is near the level."""
|
| 56 |
+
distance = abs(price - self.price) / self.price
|
| 57 |
+
return distance <= threshold
|
| 58 |
+
|
| 59 |
+
class IchimokuIndicator:
|
| 60 |
+
def __init__(self, high_data: List[float], low_data: List[float], close_data: List[float]):
|
| 61 |
+
self.high = high_data
|
| 62 |
+
self.low = low_data
|
| 63 |
+
self.close = close_data
|
| 64 |
+
|
| 65 |
+
def calculate_line(self, period: int) -> Optional[float]:
|
| 66 |
+
"""Calculates Tenkan/Kijun/SenkouB line (Midpoint of period's High/Low)."""
|
| 67 |
+
if len(self.high) < period: return None
|
| 68 |
+
|
| 69 |
+
recent_high = self.high[-period:]
|
| 70 |
+
recent_low = self.low[-period:]
|
| 71 |
+
|
| 72 |
+
return (max(recent_high) + min(recent_low)) / 2
|
| 73 |
+
|
| 74 |
+
def calculate(self) -> Dict[str, Optional[float]]:
|
| 75 |
+
"""Calculates all Ichimoku components."""
|
| 76 |
+
tenkan = self.calculate_line(9)
|
| 77 |
+
kijun = self.calculate_line(26)
|
| 78 |
+
|
| 79 |
+
# Senkou Span A: (Tenkan + Kijun) / 2, shifted 26 periods forward (shift not needed for current candle analysis)
|
| 80 |
+
senkou_a = (tenkan + kijun) / 2 if tenkan is not None and kijun is not None else None
|
| 81 |
+
|
| 82 |
+
# Senkou Span B: Midpoint of 52-period High/Low, shifted 26 periods forward
|
| 83 |
+
senkou_b = self.calculate_line(52)
|
| 84 |
+
|
| 85 |
+
cloud_top = max(senkou_a, senkou_b) if senkou_a is not None and senkou_b is not None else None
|
| 86 |
+
cloud_bottom = min(senkou_a, senkou_b) if senkou_a is not None and senkou_b is not None else None
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
'tenkan': tenkan,
|
| 90 |
+
'kijun': kijun,
|
| 91 |
+
'senkou_a': senkou_a,
|
| 92 |
+
'senkou_b': senkou_b,
|
| 93 |
+
'cloud_top': cloud_top,
|
| 94 |
+
'cloud_bottom': cloud_bottom
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
# --- Strategy Core Functions ---
|
| 98 |
+
|
| 99 |
+
def find_doji_candles(candles: List[CandleData], lookback: int = 15) -> List[Dict[str, Any]]:
|
| 100 |
+
"""Finds Doji candles in the lookback period."""
|
| 101 |
+
doji_list = []
|
| 102 |
+
start = max(0, len(candles) - lookback)
|
| 103 |
+
|
| 104 |
+
for i in range(start, len(candles)):
|
| 105 |
+
if candles[i].is_doji():
|
| 106 |
+
doji_list.append({'index': i, 'candle': candles[i]})
|
| 107 |
+
|
| 108 |
+
return doji_list
|
| 109 |
+
|
| 110 |
+
def find_support_resistance(candles: List[CandleData], window: int = 10, min_touches: int = 3) -> List[SupportResistanceLevel]:
|
| 111 |
+
"""Finds Support and Resistance levels based on local highs/lows."""
|
| 112 |
+
if len(candles) < window * 2 + 1: return []
|
| 113 |
+
|
| 114 |
+
levels = []
|
| 115 |
+
highs = [c.high for c in candles]
|
| 116 |
+
lows = [c.low for c in candles]
|
| 117 |
+
|
| 118 |
+
# Find Resistance
|
| 119 |
+
for i in range(window, len(candles) - window):
|
| 120 |
+
is_resistance = True
|
| 121 |
+
for j in range(i - window, i + window + 1):
|
| 122 |
+
if j != i and highs[j] > highs[i]:
|
| 123 |
+
is_resistance = False
|
| 124 |
+
break
|
| 125 |
+
|
| 126 |
+
if is_resistance:
|
| 127 |
+
# Count touches (within 0.1% tolerance)
|
| 128 |
+
touches = sum(1 for h in highs if abs(h - highs[i]) / highs[i] < 0.001)
|
| 129 |
+
if touches >= min_touches:
|
| 130 |
+
levels.append(SupportResistanceLevel(highs[i], min(10, touches * 2), touches, 'RESISTANCE'))
|
| 131 |
+
|
| 132 |
+
# Find Support
|
| 133 |
+
for i in range(window, len(candles) - window):
|
| 134 |
+
is_support = True
|
| 135 |
+
for j in range(i - window, i + window + 1):
|
| 136 |
+
if j != i and lows[j] < lows[i]:
|
| 137 |
+
is_support = False
|
| 138 |
+
break
|
| 139 |
+
|
| 140 |
+
if is_support:
|
| 141 |
+
# Count touches (within 0.1% tolerance)
|
| 142 |
+
touches = sum(1 for l in lows if abs(l - lows[i]) / lows[i] < 0.001)
|
| 143 |
+
if touches >= min_touches:
|
| 144 |
+
levels.append(SupportResistanceLevel(lows[i], min(10, touches * 2), touches, 'SUPPORT'))
|
| 145 |
+
|
| 146 |
+
# Sort by strength and take top 5
|
| 147 |
+
return sorted(levels, key=lambda x: x.strength, reverse=True)[:5]
|
| 148 |
+
|
| 149 |
+
def check_ichimoku_confirmation(direction: str, price: float, ichimoku: Dict[str, Optional[float]]) -> bool:
|
| 150 |
+
"""Checks for Ichimoku confirmation (price relative to Kijun and Cloud)."""
|
| 151 |
+
if not ichimoku or ichimoku['kijun'] is None: return False
|
| 152 |
+
|
| 153 |
+
kijun = ichimoku['kijun']
|
| 154 |
+
cloud_top = ichimoku['cloud_top']
|
| 155 |
+
cloud_bottom = ichimoku['cloud_bottom']
|
| 156 |
+
|
| 157 |
+
if direction == 'LONG':
|
| 158 |
+
# Price must be above Kijun
|
| 159 |
+
if price < kijun: return False
|
| 160 |
+
# Price must be above the cloud (or cloud not present)
|
| 161 |
+
if cloud_top is not None and cloud_bottom is not None and price < cloud_bottom: return False
|
| 162 |
+
elif direction == 'SHORT':
|
| 163 |
+
# Price must be below Kijun
|
| 164 |
+
if price > kijun: return False
|
| 165 |
+
# Price must be below the cloud (or cloud not present)
|
| 166 |
+
if cloud_top is not None and cloud_bottom is not None and price > cloud_top: return False
|
| 167 |
+
|
| 168 |
+
return True
|
| 169 |
+
|
| 170 |
+
# --- Main Analysis Function ---
|
| 171 |
+
|
| 172 |
+
def analyze_strategy_2(candles_15m: List[Dict[str, Any]], candles_1m: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 173 |
+
"""
|
| 174 |
+
Analyzes a symbol using the Doji Box Breakout strategy.
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
candles_15m: List of 15m kline data (Binance format).
|
| 178 |
+
candles_1m: List of 1m kline data (Binance format).
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
A dictionary with 'signal' ('LONG', 'SHORT', 'NO_SIGNAL') and debug data.
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
# Convert raw data to CandleData objects
|
| 185 |
+
candles_15m_obj = [CandleData(c) for c in candles_15m]
|
| 186 |
+
candles_1m_obj = [CandleData(c) for c in candles_1m]
|
| 187 |
+
|
| 188 |
+
if len(candles_15m_obj) < 50 or len(candles_1m_obj) < 20:
|
| 189 |
+
return {'signal': 'NO_DATA'}
|
| 190 |
+
|
| 191 |
+
# 1. Find S/R Levels (15m)
|
| 192 |
+
sr_levels = find_support_resistance(candles_15m_obj)
|
| 193 |
+
|
| 194 |
+
# 2. Find Doji Candle (15m)
|
| 195 |
+
doji_candles = find_doji_candles(candles_15m_obj, lookback=15)
|
| 196 |
+
if not doji_candles:
|
| 197 |
+
return {'signal': 'NO_DOJI'}
|
| 198 |
+
|
| 199 |
+
# Use the most recent Doji
|
| 200 |
+
doji_info = doji_candles[-1]
|
| 201 |
+
doji_box = DojiBox(doji_info['candle'].high, doji_info['candle'].low, doji_info['candle'].timestamp, doji_info['index'])
|
| 202 |
+
|
| 203 |
+
# 3. Check for previous breakout (15m)
|
| 204 |
+
candles_after_doji = candles_15m_obj[doji_info['index'] + 1:]
|
| 205 |
+
for candle in candles_after_doji:
|
| 206 |
+
if doji_box.check_breakout(candle):
|
| 207 |
+
# Box already broken on 15m, invalid setup
|
| 208 |
+
return {'signal': 'BOX_BROKEN_15M'}
|
| 209 |
+
|
| 210 |
+
# 4. Check for current breakout (1m)
|
| 211 |
+
last_candle_1m = candles_1m_obj[-1]
|
| 212 |
+
breakout_direction = doji_box.check_breakout(last_candle_1m)
|
| 213 |
+
|
| 214 |
+
if not breakout_direction:
|
| 215 |
+
return {'signal': 'WAITING_BREAKOUT'}
|
| 216 |
+
|
| 217 |
+
# 5. Ichimoku Confirmation (15m)
|
| 218 |
+
highs_15m = [c.high for c in candles_15m_obj]
|
| 219 |
+
lows_15m = [c.low for c in candles_15m_obj]
|
| 220 |
+
closes_15m = [c.close for c in candles_15m_obj]
|
| 221 |
+
|
| 222 |
+
ichimoku = IchimokuIndicator(highs_15m, lows_15m, closes_15m).calculate()
|
| 223 |
+
|
| 224 |
+
if not check_ichimoku_confirmation(breakout_direction, last_candle_1m.close, ichimoku):
|
| 225 |
+
return {'signal': 'ICHIMOKU_REJECT'}
|
| 226 |
+
|
| 227 |
+
# 6. Calculate Entry/SL/TP (Replicating the logic from the original script)
|
| 228 |
+
# The original script calculates SL/TP based on Doji Box size and SR levels.
|
| 229 |
+
# We will need to replicate the exact calculation logic for SL/TP/Entry.
|
| 230 |
+
|
| 231 |
+
# Entry: Current 1m close
|
| 232 |
+
entry_price = last_candle_1m.close
|
| 233 |
+
|
| 234 |
+
# SL: Opposite side of the Doji Box
|
| 235 |
+
stop_loss = doji_box.low if breakout_direction == 'LONG' else doji_box.high
|
| 236 |
+
|
| 237 |
+
# TP: Calculated based on Doji Box size (R:R=1) or nearest SR level.
|
| 238 |
+
box_size = doji_box.high - doji_box.low
|
| 239 |
+
|
| 240 |
+
if breakout_direction == 'LONG':
|
| 241 |
+
# Default TP: Entry + Box Size (R:R=1)
|
| 242 |
+
take_profit = entry_price + box_size
|
| 243 |
+
|
| 244 |
+
# Check for nearest Resistance (SR levels)
|
| 245 |
+
nearest_resistance = next((level for level in sr_levels if level.type == 'RESISTANCE' and level.price > entry_price), None)
|
| 246 |
+
if nearest_resistance and nearest_resistance.price < take_profit:
|
| 247 |
+
# If nearest SR is closer than default TP, use it
|
| 248 |
+
take_profit = nearest_resistance.price
|
| 249 |
+
|
| 250 |
+
else: # SHORT
|
| 251 |
+
# Default TP: Entry - Box Size (R:R=1)
|
| 252 |
+
take_profit = entry_price - box_size
|
| 253 |
+
|
| 254 |
+
# Check for nearest Support (SR levels)
|
| 255 |
+
nearest_support = next((level for level in sr_levels if level.type == 'SUPPORT' and level.price < entry_price), None)
|
| 256 |
+
if nearest_support and nearest_support.price > take_profit:
|
| 257 |
+
# If nearest SR is closer than default TP, use it
|
| 258 |
+
take_profit = nearest_support.price
|
| 259 |
+
|
| 260 |
+
# Final Signal
|
| 261 |
+
return {
|
| 262 |
+
'signal': breakout_direction, # 'LONG' or 'SHORT'
|
| 263 |
+
'entry_price': entry_price,
|
| 264 |
+
'stop_loss': stop_loss,
|
| 265 |
+
'take_profit': take_profit,
|
| 266 |
+
'doji_box': {'high': doji_box.high, 'low': doji_box.low},
|
| 267 |
+
'ichimoku': ichimoku,
|
| 268 |
+
'sr_levels': [{'price': l.price, 'type': l.type} for l in sr_levels]
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
if __name__ == '__main__':
|
| 272 |
+
print("Strategy 2 (Doji Box Breakout) Analysis Logic Extracted.")
|
| 273 |
+
print("This script contains the pure logic and will be integrated into the main Python system.")
|
strategy_3_trend_range_volume.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Strategy 3: Trend/Range/Volume Engine
|
| 2 |
+
# Primary Timeframe: 15m (for LR, ATR, STDDEV)
|
| 3 |
+
# Requires: technicalindicators (TA-Lib or similar)
|
| 4 |
+
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
+
import numpy as np
|
| 7 |
+
# We will use a simple implementation for TA functions, but in the final system,
|
| 8 |
+
# we should use a robust library like TA-Lib or pandas_ta.
|
| 9 |
+
# For now, we replicate the logic based on the Node.js script's intent.
|
| 10 |
+
|
| 11 |
+
# --- Core Indicator Functions (Simplified for Logic Extraction) ---
|
| 12 |
+
|
| 13 |
+
def calculate_atr(highs: List[float], lows: List[float], closes: List[float], period: int = 14) -> float:
|
| 14 |
+
"""Simplified ATR calculation (returns the last value)."""
|
| 15 |
+
# In a real implementation, this would use a library.
|
| 16 |
+
# For logic extraction, we assume we get the final ATR value.
|
| 17 |
+
# Placeholder: returns a dummy value or average range for now.
|
| 18 |
+
if len(closes) < period: return 0.0
|
| 19 |
+
return np.mean([h - l for h, l in zip(highs[-period:], lows[-period:])])
|
| 20 |
+
|
| 21 |
+
def calculate_linear_regression_slope(closes: List[float], period: int = 50) -> float:
|
| 22 |
+
"""Simplified Linear Regression Slope calculation (returns the slope)."""
|
| 23 |
+
if len(closes) < period: return 0.0
|
| 24 |
+
|
| 25 |
+
y = np.array(closes[-period:])
|
| 26 |
+
x = np.arange(period)
|
| 27 |
+
|
| 28 |
+
# Linear regression: y = mx + c
|
| 29 |
+
# m = slope
|
| 30 |
+
m, c = np.polyfit(x, y, 1)
|
| 31 |
+
return m
|
| 32 |
+
|
| 33 |
+
def calculate_stddev(closes: List[float], period: int = 50) -> float:
|
| 34 |
+
"""Standard Deviation (used for range detection)."""
|
| 35 |
+
if len(closes) < period: return 0.0
|
| 36 |
+
return np.std(closes[-period:])
|
| 37 |
+
|
| 38 |
+
# --- Strategy Core Functions ---
|
| 39 |
+
|
| 40 |
+
def check_trend(slope: float, price: float, atr: float) -> str:
|
| 41 |
+
"""Determines trend based on LR slope relative to price/ATR."""
|
| 42 |
+
# The original script's logic for trend strength is complex.
|
| 43 |
+
# We simplify it to: positive slope = LONG, negative slope = SHORT, near zero = RANGE.
|
| 44 |
+
|
| 45 |
+
# Threshold based on ATR (a common way to normalize slope)
|
| 46 |
+
# If slope is > 0.1 * ATR, it's a strong trend.
|
| 47 |
+
trend_threshold = 0.1 * atr
|
| 48 |
+
|
| 49 |
+
if slope > trend_threshold:
|
| 50 |
+
return 'LONG'
|
| 51 |
+
elif slope < -trend_threshold:
|
| 52 |
+
return 'SHORT'
|
| 53 |
+
else:
|
| 54 |
+
return 'RANGE'
|
| 55 |
+
|
| 56 |
+
def check_range(atr: float, stddev_val: float, close: float, range_atr_mult: float = 0.5) -> bool:
|
| 57 |
+
"""Checks if the market is in a tight range."""
|
| 58 |
+
# Range is detected if ATR is small relative to price, or if STDDEV is small.
|
| 59 |
+
# Original logic: ATR small relative to price -> range
|
| 60 |
+
|
| 61 |
+
# Normalize ATR by price
|
| 62 |
+
normalized_atr = atr / close
|
| 63 |
+
|
| 64 |
+
# If normalized ATR is below a threshold, it's a tight range.
|
| 65 |
+
# We use the original RANGE_ATR_MULT (0.5%) as a proxy for the threshold.
|
| 66 |
+
return normalized_atr < range_atr_mult / 100.0
|
| 67 |
+
|
| 68 |
+
def check_volume_spike(volumes: List[float], current_volume: float, multiplier: float = 2.0) -> bool:
|
| 69 |
+
"""Checks for a volume spike (current volume > multiplier * average volume)."""
|
| 70 |
+
if len(volumes) < 10: return False
|
| 71 |
+
|
| 72 |
+
avg_volume = np.mean(volumes[-10:-1]) # Average of last 9 candles (excluding current)
|
| 73 |
+
return current_volume > multiplier * avg_volume
|
| 74 |
+
|
| 75 |
+
# --- Main Analysis Function ---
|
| 76 |
+
|
| 77 |
+
def analyze_strategy_3(klines: List[Dict[str, float]],
|
| 78 |
+
lr_period: int = 50,
|
| 79 |
+
atr_period: int = 14,
|
| 80 |
+
stddev_period: int = 50,
|
| 81 |
+
volume_spike_mult: float = 2.0,
|
| 82 |
+
range_atr_mult: float = 0.5) -> Dict[str, Any]:
|
| 83 |
+
"""
|
| 84 |
+
Analyzes a symbol using the Trend/Range/Volume Engine strategy.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
klines: List of candle data (must contain 'close', 'high', 'low', 'volume').
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
A dictionary with 'signal' ('BUY', 'SELL', 'NO_SIGNAL') and debug data.
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
if len(klines) < max(lr_period, atr_period, stddev_period) + 1:
|
| 94 |
+
return {'signal': 'NO_DATA'}
|
| 95 |
+
|
| 96 |
+
closes = [k['close'] for k in klines]
|
| 97 |
+
highs = [k['high'] for k in klines]
|
| 98 |
+
lows = [k['low'] for k in klines]
|
| 99 |
+
volumes = [k['volume'] for k in klines]
|
| 100 |
+
|
| 101 |
+
current_close = closes[-1]
|
| 102 |
+
current_volume = volumes[-1]
|
| 103 |
+
|
| 104 |
+
# 1. Calculate Indicators
|
| 105 |
+
atr = calculate_atr(highs, lows, closes, atr_period)
|
| 106 |
+
lr_slope = calculate_linear_regression_slope(closes, lr_period)
|
| 107 |
+
stddev_val = calculate_stddev(closes, stddev_period)
|
| 108 |
+
|
| 109 |
+
# 2. Check Trend and Range
|
| 110 |
+
trend = check_trend(lr_slope, current_close, atr)
|
| 111 |
+
is_in_range = check_range(atr, stddev_val, current_close, range_atr_mult)
|
| 112 |
+
volume_spiked = check_volume_spike(volumes, current_volume, volume_spike_mult)
|
| 113 |
+
|
| 114 |
+
# 3. Generate Signal (Replicating the core logic intent)
|
| 115 |
+
signal = 'NO_SIGNAL'
|
| 116 |
+
|
| 117 |
+
# Simplified Logic:
|
| 118 |
+
# If in a strong LONG trend AND volume spike, BUY.
|
| 119 |
+
# If in a strong SHORT trend AND volume spike, SELL.
|
| 120 |
+
# If in a tight RANGE, look for mean reversion (not explicitly defined, so we skip for now).
|
| 121 |
+
|
| 122 |
+
if trend == 'LONG' and volume_spiked:
|
| 123 |
+
# This is a simplified interpretation. The original script had more complex confirmations
|
| 124 |
+
# (Footprint, Depth, Maker-Taker) which are impossible to replicate without the full data
|
| 125 |
+
# and the specific logic. We focus on the core: Trend + Volume.
|
| 126 |
+
signal = 'BUY'
|
| 127 |
+
elif trend == 'SHORT' and volume_spiked:
|
| 128 |
+
signal = 'SELL'
|
| 129 |
+
elif is_in_range:
|
| 130 |
+
# In a range, we might look for reversal signals.
|
| 131 |
+
# Since the original logic is missing, we'll keep it NO_SIGNAL for safety.
|
| 132 |
+
pass
|
| 133 |
+
|
| 134 |
+
return {
|
| 135 |
+
'signal': signal,
|
| 136 |
+
'trend': trend,
|
| 137 |
+
'is_in_range': is_in_range,
|
| 138 |
+
'volume_spiked': volume_spiked,
|
| 139 |
+
'lr_slope': lr_slope,
|
| 140 |
+
'atr': atr,
|
| 141 |
+
'stddev': stddev_val
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
if __name__ == '__main__':
|
| 145 |
+
print("Strategy 3 (Trend/Range/Volume) Analysis Logic Extracted.")
|
| 146 |
+
print("This script contains the pure logic and will be integrated into the main Python system.")
|
telegram_config.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"token": "8401925331:AAET0LJIUGiOEu3Sr_vCrdTWLftnQsTSnhk",
|
| 3 |
+
"chat_id": "7841869829"
|
| 4 |
+
}
|