File size: 20,742 Bytes
bc0d20b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806c155
bc0d20b
806c155
bc0d20b
 
 
 
 
806c155
bc0d20b
 
 
 
806c155
bc0d20b
 
 
806c155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc0d20b
806c155
 
bc0d20b
 
 
 
 
 
 
 
 
 
806c155
 
 
 
 
 
 
 
bc0d20b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806c155
 
 
 
 
bc0d20b
806c155
bc0d20b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806c155
bc0d20b
 
 
 
 
 
 
 
 
 
 
 
 
 
806c155
 
bc0d20b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806c155
 
 
 
 
bc0d20b
 
 
806c155
bc0d20b
 
806c155
bc0d20b
 
 
806c155
bc0d20b
 
806c155
bc0d20b
 
806c155
 
 
bc0d20b
 
 
 
 
806c155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc0d20b
806c155
bc0d20b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806c155
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# learning_hub/statistical_analyzer.py
# (هذا الملف هو النسخة المطورة من learning_engine (39).py القديم)
# وهو يمثل "التعلم البطيء" (الإحصائي)

import json
import asyncio
from datetime import datetime
from typing import Dict, Any, List
import numpy as np

# (نفترض أن هذه الدوال المساعدة سيتم نقلها إلى ملف helpers.py عام)
# (لأغراض هذا الملف، سنعرفها هنا مؤقتاً)
def normalize_weights(weights_dict):
    total = sum(weights_dict.values())
    if total > 0:
        for key in weights_dict:
            weights_dict[key] /= total
    return weights_dict

def should_update_weights(history_length):
    return history_length % 5 == 0 # (تحديث الأوزان كل 5 صفقات)


class StatisticalAnalyzer:
    def __init__(self, r2_service: Any, data_manager: Any):
        self.r2_service = r2_service
        self.data_manager = data_manager # (لجلب سياق السوق)
        
        # --- (هذه هي نفس متغيرات الحالة من learning_engine القديم) ---
        self.weights = {} # (أوزان استراتيجيات الدخول)
        self.performance_history = []
        self.strategy_effectiveness = {} # (إحصائيات استراتيجيات الدخول)
        self.exit_profile_effectiveness = {} # (إحصائيات مزيج الدخول+الخروج)
        self.market_patterns = {}
        # --- (نهاية متغيرات الحالة) ---
        
        self.initialized = False
        self.lock = asyncio.Lock()
        
        print("✅ Learning Hub Module: Statistical Analyzer (Slow-Learner) loaded")

    async def initialize(self):
        """تهيئة المحلل الإحصائي (التعلم البطيء)"""
        async with self.lock:
            if self.initialized:
                return
            print("🔄 [StatsAnalyzer] تهيئة نظام التعلم الإحصائي (البطيء)...")
            try:
                await self.load_weights_from_r2()
                await self.load_performance_history()
                await self.load_exit_profile_effectiveness()
                
                if not self.weights or not self.strategy_effectiveness:
                    await self.initialize_default_weights()
                    
                self.initialized = True
                print("✅ [StatsAnalyzer] نظام التعلم الإحصائي جاهز.")
            except Exception as e:
                print(f"❌ [StatsAnalyzer] فشل التهيئة: {e}")
                await self.initialize_default_weights()
                self.initialized = True

    # ---------------------------------------------------------------------------
    # (الدوال التالية مأخوذة مباشرة من learning_engine (39).py القديم)
    # (مع تعديلات طفيفة)
    # ---------------------------------------------------------------------------

    async def initialize_default_weights(self):
        """إعادة تعيين الأوزان إلى الوضع الافتراضي"""
        # 🔴 --- START OF CHANGE --- 🔴
        self.weights = {
            # 1. أوزان اختيار الاستراتيجية (MLProcessor)
            "strategy_weights": {
                "trend_following": 0.18, "mean_reversion": 0.15, "breakout_momentum": 0.22,
                "volume_spike": 0.12, "whale_tracking": 0.15, "pattern_recognition": 0.10,
                "hybrid_ai": 0.08
            },
            # 2. أوزان المؤشرات العامة (MLProcessor)
            "indicator_weights": {
                "rsi": 0.2, "macd": 0.2, "bbands": 0.15, "atr": 0.1,
                "volume_ratio": 0.2, "vwap": 0.15
            },
            # 3. أوزان الأنماط العامة (MLProcessor)
            "pattern_weights": {
                "Double Bottom": 0.3, "Breakout Up": 0.3, "Uptrend": 0.2,
                "Near Support": 0.2, "Double Top": -0.3 # (وزن سلبي)
            },
            # 4. أوزان كاشف الانعكاس 5m (للحارس)
            "reversal_indicator_weights": {
                "pattern": 0.4, 
                "rsi": 0.3, 
                "macd": 0.3
            },
            # 5. أوزان زناد الدخول 1m (للحارس)
            "entry_trigger_weights": {
                "cvd": 0.25, 
                "order_book": 0.25, 
                "ema_1m": 0.25, 
                "macd_1m": 0.25
            },
            # 6. عتبة تفعيل زناد الدخول
            "entry_trigger_threshold": 0.75
        }
        # 🔴 --- END OF CHANGE --- 🔴
        
        self.strategy_effectiveness = {}
        self.exit_profile_effectiveness = {}
        self.market_patterns = {}

    async def load_weights_from_r2(self):
        key = "learning_statistical_weights.json" # (ملف جديد)
        try:
            response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key)
            data = json.loads(response['Body'].read())
            self.weights = data.get("weights", {})
            # (إضافة: التحقق من وجود الأوزان الجديدة، وإلا إضافتها من الافتراضيات)
            if "reversal_indicator_weights" not in self.weights:
                defaults = await self.get_default_strategy_weights() # (سيحتوي على كل شيء)
                self.weights["reversal_indicator_weights"] = defaults.get("reversal_indicator_weights")
                self.weights["entry_trigger_weights"] = defaults.get("entry_trigger_weights")
                self.weights["entry_trigger_threshold"] = defaults.get("entry_trigger_threshold")
                print("ℹ️ [StatsAnalyzer] تم تحديث ملف الأوزان ببيانات الحارس الجديدة.")
                
            self.strategy_effectiveness = data.get("strategy_effectiveness", {})
            self.market_patterns = data.get("market_patterns", {})
            print(f"✅ [StatsAnalyzer] تم تحميل الأوزان والإحصائيات من R2.")
        except Exception as e:
            print(f"ℹ️ [StatsAnalyzer] فشل تحميل الأوزان ({e}). استخدام الافتراضيات.")
            await self.initialize_default_weights()

    async def save_weights_to_r2(self):
        key = "learning_statistical_weights.json"
        try:
            data = {
                "weights": self.weights,
                "strategy_effectiveness": self.strategy_effectiveness,
                "market_patterns": self.market_patterns,
                "last_updated": datetime.now().isoformat()
            }
            data_json = json.dumps(data, indent=2, ensure_ascii=False).encode('utf-8')
            self.r2_service.s3_client.put_object(
                Bucket="trading", Key=key, Body=data_json, ContentType="application/json"
            )
        except Exception as e:
            print(f"❌ [StatsAnalyzer] فشل حفظ الأوزان في R2: {e}")

    async def load_performance_history(self):
        key = "learning_performance_history.json" # (مشترك)
        try:
            response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key)
            data = json.loads(response['Body'].read())
            self.performance_history = data.get("history", [])
        except Exception as e:
            self.performance_history = []

    async def save_performance_history(self):
        key = "learning_performance_history.json"
        try:
            data = {"history": self.performance_history[-1000:]} # (آخر 1000 صفقة فقط)
            data_json = json.dumps(data, indent=2, ensure_ascii=False).encode('utf-8')
            self.r2_service.s3_client.put_object(
                Bucket="trading", Key=key, Body=data_json, ContentType="application/json"
            )
        except Exception as e:
            print(f"❌ [StatsAnalyzer] فشل حفظ تاريخ الأداء: {e}")

    async def load_exit_profile_effectiveness(self):
        key = "learning_exit_profile_effectiveness.json" # (مشترك)
        try:
            response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key)
            data = json.loads(response['Body'].read())
            self.exit_profile_effectiveness = data.get("effectiveness", {})
        except Exception as e:
            self.exit_profile_effectiveness = {}

    async def save_exit_profile_effectiveness(self):
        key = "learning_exit_profile_effectiveness.json"
        try:
            data = {
                "effectiveness": self.exit_profile_effectiveness,
                "last_updated": datetime.now().isoformat()
            }
            data_json = json.dumps(data, indent=2, ensure_ascii=False).encode('utf-8')
            self.r2_service.s3_client.put_object(
                Bucket="trading", Key=key, Body=data_json, ContentType="application/json"
            )
        except Exception as e:
            print(f"❌ [StatsAnalyzer] فشل حفظ أداء ملف الخروج: {e}")

    async def update_statistics(self, trade_object: Dict[str, Any], close_reason: str):
        """
        هذه هي الدالة الرئيسية التي تحدث الإحصائيات (التعلم البطيء).
        (تدمج update_strategy_effectiveness و update_market_patterns من الملف القديم)
        """
        if not self.initialized:
            await self.initialize()
            
        try:
            strategy = trade_object.get('strategy', 'unknown')
            decision_data = trade_object.get('decision_data', {})
            exit_profile = decision_data.get('exit_profile', 'unknown')
            combined_key = f"{strategy}_{exit_profile}"
            
            pnl_percent = trade_object.get('pnl_percent', 0)
            is_success = pnl_percent > 0.1 # (اعتبار الربح الطفيف نجاحاً)
            
            # 🔴 --- START OF CHANGE --- 🔴
            # (استخدام بيانات السوق وقت القرار إذا كانت مخزنة، وإلا جلب الحالية)
            market_context = decision_data.get('market_context_at_decision', {})
            if not market_context:
                 market_context = await self.get_current_market_conditions()
            market_condition = market_context.get('current_trend', 'sideways_market')
            # 🔴 --- END OF CHANGE --- 🔴

            # --- 1. تحديث تاريخ الأداء (للتتبع العام) ---
            analysis_entry = {
                "timestamp": datetime.now().isoformat(),
                "trade_id": trade_object.get('id', 'N/A'),
                "symbol": trade_object.get('symbol', 'N/A'),
                "outcome": close_reason,
                "market_conditions": market_context,
                "strategy_used": strategy,
                "exit_profile_used": exit_profile,
                "pnl_percent": pnl_percent
            }
            self.performance_history.append(analysis_entry)
            
            # --- 2. تحديث إحصائيات استراتيجية الدخول (strategy_effectiveness) ---
            if strategy not in self.strategy_effectiveness:
                self.strategy_effectiveness[strategy] = {"total_trades": 0, "successful_trades": 0, "total_pnl_percent": 0}
            
            self.strategy_effectiveness[strategy]["total_trades"] += 1
            self.strategy_effectiveness[strategy]["total_pnl_percent"] += pnl_percent
            if is_success:
                self.strategy_effectiveness[strategy]["successful_trades"] += 1

            # --- 3. تحديث إحصائيات مزيج (الدخول + الخروج) (exit_profile_effectiveness) ---
            if combined_key not in self.exit_profile_effectiveness:
                self.exit_profile_effectiveness[combined_key] = {"total_trades": 0, "successful_trades": 0, "total_pnl_percent": 0, "pnl_list": []}
            
            self.exit_profile_effectiveness[combined_key]["total_trades"] += 1
            self.exit_profile_effectiveness[combined_key]["total_pnl_percent"] += pnl_percent
            self.exit_profile_effectiveness[combined_key]["pnl_list"].append(pnl_percent)
            if len(self.exit_profile_effectiveness[combined_key]["pnl_list"]) > 100:
                self.exit_profile_effectiveness[combined_key]["pnl_list"] = self.exit_profile_effectiveness[combined_key]["pnl_list"][-100:]
            if is_success:
                self.exit_profile_effectiveness[combined_key]["successful_trades"] += 1

            # --- 4. تحديث إحصائيات ظروف السوق (market_patterns) ---
            if market_condition not in self.market_patterns:
                self.market_patterns[market_condition] = {"total_trades": 0, "successful_trades": 0, "total_pnl_percent": 0}
                
            self.market_patterns[market_condition]["total_trades"] += 1
            self.market_patterns[market_condition]["total_pnl_percent"] += pnl_percent
            if is_success:
                self.market_patterns[market_condition]["successful_trades"] += 1

            # --- 5. تكييف الأوزان والحفظ (إذا لزم الأمر) ---
            # (ملاحظة: نحتاج إلى إضافة منطق لتعلم أوزان الحارس هنا مستقبلاً)
            if should_update_weights(len(self.performance_history)):
                await self.adapt_weights_based_on_performance()
                await self.save_weights_to_r2()
                await self.save_performance_history()
                await self.save_exit_profile_effectiveness()

            print(f"✅ [StatsAnalyzer] تم تحديث الإحصائيات لـ {strategy} / {exit_profile}")

        except Exception as e:
            print(f"❌ [StatsAnalyzer] فشل تحديث الإحصائيات: {e}")
            traceback.print_exc()

    async def adapt_weights_based_on_performance(self):
        """تكييف أوزان استراتيجيات الدخول بناءً على الأداء الإحصائي"""
        # (ملاحظة: هذا المنطق حالياً يكيف فقط strategy_weights)
        # (سنحتاج لتطويره لاحقاً ليكيف أوزان الحارس)
        print("🔄 [StatsAnalyzer] تكييف أوزان الاستراتيجيات (التعلم البطيء)...")
        try:
            strategy_performance = {}
            total_performance = 0

            for strategy, data in self.strategy_effectiveness.items():
                if data.get("total_trades", 0) > 2: # (يتطلب 3 صفقات على الأقل للتكيف)
                    success_rate = data["successful_trades"] / data["total_trades"]
                    avg_pnl = data["total_pnl_percent"] / data["total_trades"]
                    
                    # مقياس مركب: (معدل النجاح * 60%) + (متوسط الربح * 40%)
                    # (يتم تقييد متوسط الربح بين -5 و +5)
                    normalized_pnl = min(max(avg_pnl, -5.0), 5.0) / 5.0 # (من -1 إلى 1)
                    
                    composite_performance = (success_rate * 0.6) + (normalized_pnl * 0.4)
                    
                    strategy_performance[strategy] = composite_performance
                    total_performance += composite_performance

            if total_performance > 0 and strategy_performance:
                base_weights = self.weights.get("strategy_weights", {})
                for strategy, performance in strategy_performance.items():
                    current_weight = base_weights.get(strategy, 0.1)
                    
                    # (تعديل طفيف: 80% من الوزن الحالي + 20% من الأداء)
                    new_weight = (current_weight * 0.8) + (performance * 0.2)
                    base_weights[strategy] = max(new_weight, 0.05) # (الحد الأدنى للوزن 5%)
                
                normalize_weights(base_weights)
                self.weights["strategy_weights"] = base_weights
                print(f"✅ [StatsAnalyzer] تم تكييف الأوزان: {base_weights}")
            
        except Exception as e:
            print(f"❌ [StatsAnalyzer] فشل تكييف الأوزان: {e}")

    # --- (الدوال المساعدة لجلب البيانات - مأخوذة من الملف القديم) ---
    async def get_best_exit_profile(self, entry_strategy: str) -> str:
        """يجد أفضل ملف خروج إحصائياً لاستراتيجية دخول معينة."""
        if not self.initialized or not self.exit_profile_effectiveness:
            return "unknown"
            
        relevant_profiles = {}
        for combined_key, data in self.exit_profile_effectiveness.items():
            if combined_key.startswith(f"{entry_strategy}_"):
                if data.get("total_trades", 0) >= 3: # (يتطلب 3 صفقات)
                    exit_profile_name = combined_key.replace(f"{entry_strategy}_", "", 1)
                    avg_pnl = data["total_pnl_percent"] / data["total_trades"]
                    relevant_profiles[exit_profile_name] = avg_pnl
        
        if not relevant_profiles:
            return "unknown"
            
        best_profile = max(relevant_profiles, key=relevant_profiles.get)
        return best_profile

    # 🔴 --- START OF CHANGE --- 🔴
    async def get_optimized_weights(self, market_condition: str) -> Dict[str, float]:
        """
        جلب جميع الأوزان المعدلة إحصائياً (لكل من MLProcessor والحارس).
        """
        if not self.initialized or "strategy_weights" not in self.weights:
            await self.initialize()
        
        base_weights = self.weights.copy()
        
        # (يمكننا إضافة منطق تعديل الأوزان بناءً على ظروف السوق هنا)
        # (لكن في الوقت الحالي، سنعيد الأوزان المعدلة إحصائياً كما هي)
        
        if not base_weights:
             # (العودة إلى الافتراضيات إذا كانت الأوزان فارغة)
             return await self.get_default_strategy_weights()

        return base_weights
    # 🔴 --- END OF CHANGE --- 🔴

    async def get_default_strategy_weights(self) -> Dict[str, float]:
        """إرجاع الأوزان الافتراضDية عند الفشل"""
        # 🔴 --- START OF CHANGE --- 🔴
        # (إرجاع كل شيء، وليس فقط أوزان الاستراتيجية)
        return {
            "strategy_weights": {
                "trend_following": 0.18, "mean_reversion": 0.15, "breakout_momentum": 0.22,
                "volume_spike": 0.12, "whale_tracking": 0.15, "pattern_recognition": 0.10,
                "hybrid_ai": 0.08
            },
            "indicator_weights": {
                "rsi": 0.2, "macd": 0.2, "bbands": 0.15, "atr": 0.1,
                "volume_ratio": 0.2, "vwap": 0.15
            },
            "pattern_weights": {
                "Double Bottom": 0.3, "Breakout Up": 0.3, "Uptrend": 0.2,
                "Near Support": 0.2, "Double Top": -0.3
            },
            "reversal_indicator_weights": {
                "pattern": 0.4, "rsi": 0.3, "macd": 0.3
            },
            "entry_trigger_weights": {
                "cvd": 0.25, "order_book": 0.25, "ema_1m": 0.25, "macd_1m": 0.25
            },
            "entry_trigger_threshold": 0.75
        }
        # 🔴 --- END OF CHANGE --- 🔴
    
    async def get_current_market_conditions(self) -> Dict[str, Any]:
        """جلب سياق السوق الحالي (من الملف القديم)"""
        try:
            if not self.data_manager:
                raise ValueError("DataManager unavailable")
            market_context = await self.data_manager.get_market_context_async()
            if not market_context:
                raise ValueError("Market context fetch failed")
            
            # (نحتاج دالة لحساب التقلب - نفترض أنها في helpers)
            # volatility = calculate_market_volatility(market_context) 
            
            return {
                "current_trend": market_context.get('market_trend', 'sideways_market'),
                "volatility": "medium", # (قيمة مؤقتة)
                "market_sentiment": market_context.get('btc_sentiment', 'NEUTRAL'),
            }
        except Exception as e:
            return {"current_trend": "sideways_market", "volatility": "medium", "market_sentiment": "NEUTRAL"}
}