daafa999 commited on
Commit
8e0fe91
Β·
verified Β·
1 Parent(s): fb89ae5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +250 -747
app.py CHANGED
@@ -1,758 +1,261 @@
1
- import os
2
- import time
3
- import logging
4
- import traceback
5
-
 
 
 
6
  import gradio as gr
7
- import pandas as pd
8
- import numpy as np
9
- import requests
10
- import joblib
11
-
12
- from sklearn.preprocessing import MinMaxScaler
13
- from sklearn.model_selection import TimeSeriesSplit
14
-
15
- from xgboost import XGBClassifier
16
-
17
- from ta.trend import ADXIndicator
18
-
19
-
20
- # ============================================================
21
- # CONFIG
22
- # ============================================================
23
-
24
- SYMBOLS = [
25
- "BTCUSDT",
26
- "ETHUSDT",
27
- "SOLUSDT",
28
- "BNBUSDT",
29
- "XRPUSDT",
30
- "DOGEUSDT",
31
- ]
32
-
33
- WINDOW = 20
34
-
35
- MODEL_DIR = "models"
36
- DATA_DIR = "data"
37
-
38
- os.makedirs(MODEL_DIR, exist_ok=True)
39
- os.makedirs(DATA_DIR, exist_ok=True)
40
-
41
- CACHE_SECONDS = 60
42
-
43
- LAST_DOWNLOAD = {}
44
- LAST_RETRAIN = 0
45
-
46
-
47
- # ============================================================
48
- # LOGGING
49
- # ============================================================
50
-
51
- logging.basicConfig(level=logging.INFO)
52
-
53
- logger = logging.getLogger("crypto_ai")
54
-
55
-
56
- # ============================================================
57
- # PATHS
58
- # ============================================================
59
-
60
- def model_path(symbol):
61
-
62
- return os.path.join(
63
- MODEL_DIR,
64
- f"{symbol}_model.pkl"
65
- )
66
-
67
-
68
- def scaler_path(symbol):
69
-
70
- return os.path.join(
71
- MODEL_DIR,
72
- f"{symbol}_scaler.pkl"
73
- )
74
-
75
-
76
- def data_path(symbol):
77
-
78
- return os.path.join(
79
- DATA_DIR,
80
- f"{symbol}.csv"
81
- )
82
-
83
-
84
- # ============================================================
85
- # DOWNLOAD BINANCE DATA
86
- # ============================================================
87
-
88
- def safe_download(symbol):
89
-
90
- try:
91
-
92
- now = time.time()
93
-
94
- csv_file = data_path(symbol)
95
-
96
- # ============================================
97
- # CACHE
98
- # ============================================
99
-
100
- if os.path.exists(csv_file):
101
-
102
- last_time = LAST_DOWNLOAD.get(symbol, 0)
103
-
104
- if now - last_time < CACHE_SECONDS:
105
-
106
- df = pd.read_csv(
107
- csv_file,
108
- index_col=0
109
- )
110
-
111
- df.index = pd.to_datetime(df.index)
112
-
113
- return df
114
-
115
- # ============================================
116
- # BINANCE API
117
- # ============================================
118
-
119
- url = (
120
- "https://api.binance.com/api/v3/klines"
121
- )
122
-
123
- params = {
124
- "symbol": symbol,
125
- "interval": "1h",
126
- "limit": 1000
127
- }
128
-
129
- response = requests.get(
130
- url,
131
- params=params,
132
- timeout=20
133
- )
134
-
135
- # ============================================
136
- # VALIDATE RESPONSE
137
- # ============================================
138
-
139
- if response.status_code != 200:
140
-
141
- logger.warning(
142
- f"{symbol} API ERROR: "
143
- f"{response.status_code}"
144
- )
145
-
146
- return None
147
-
148
- data = response.json()
149
-
150
- # BINANCE ERROR RESPONSE
151
-
152
- if not isinstance(data, list):
153
-
154
- logger.warning(
155
- f"{symbol} INVALID RESPONSE: {data}"
156
- )
157
-
158
- return None
159
-
160
- if len(data) == 0:
161
-
162
- logger.warning(
163
- f"{symbol} EMPTY DATA"
164
- )
165
-
166
- return None
167
-
168
- rows = []
169
-
170
- for k in data:
171
-
172
- # VALIDATE CANDLE
173
-
174
- if not isinstance(k, list):
175
-
176
- continue
177
-
178
- if len(k) < 6:
179
-
180
- continue
181
-
182
- rows.append({
183
- "time": pd.to_datetime(
184
- int(k[0]),
185
- unit="ms"
186
- ),
187
- "open": float(k[1]),
188
- "high": float(k[2]),
189
- "low": float(k[3]),
190
- "close": float(k[4]),
191
- "volume": float(k[5]),
192
- })
193
-
194
- if len(rows) == 0:
195
-
196
- logger.warning(
197
- f"{symbol} NO VALID ROWS"
198
- )
199
-
200
- return None
201
-
202
- df = pd.DataFrame(rows)
203
-
204
- df = df.set_index("time")
205
-
206
- df.to_csv(csv_file)
207
-
208
- LAST_DOWNLOAD[symbol] = now
209
-
210
- return df
211
-
212
- except Exception as e:
213
-
214
- logger.warning(
215
- f"{symbol} DOWNLOAD ERROR: {e}"
216
- )
217
-
218
- # ============================================
219
- # FALLBACK CACHE
220
- # ============================================
221
-
222
- try:
223
-
224
- if os.path.exists(csv_file):
225
-
226
- logger.info(
227
- f"Using cache: {symbol}"
228
- )
229
-
230
- df = pd.read_csv(
231
- csv_file,
232
- index_col=0
233
- )
234
-
235
- df.index = pd.to_datetime(df.index)
236
-
237
- return df
238
-
239
- except Exception as e:
240
-
241
- logger.warning(e)
242
-
243
- return None
244
-
245
-
246
- # ============================================================
247
- # FEATURE ENGINEERING
248
- # ============================================================
249
-
250
- def fetch_and_prepare(symbol):
251
-
252
- df = safe_download(symbol)
253
-
254
- if df is None:
255
-
256
- return None, None, None, None
257
-
258
- df.index = pd.to_datetime(df.index)
259
-
260
- df = df.resample("4h").agg({
261
- "open": "first",
262
- "high": "max",
263
- "low": "min",
264
- "close": "last",
265
- "volume": "sum"
266
- }).dropna()
267
-
268
- # RSI
269
-
270
- delta = df["close"].diff()
271
-
272
- gain = delta.where(
273
- delta > 0,
274
- 0
275
- ).rolling(14).mean()
276
-
277
- loss = (-delta.where(
278
- delta < 0,
279
- 0
280
- )).rolling(14).mean()
281
-
282
- rs = gain / loss.replace(0, np.nan)
283
-
284
- df["rsi"] = (
285
- 100 - (100 / (1 + rs))
286
- ).fillna(50)
287
-
288
- # MACD
289
-
290
- ema12 = (
291
- df["close"]
292
- .ewm(span=12)
293
- .mean()
294
- )
295
-
296
- ema26 = (
297
- df["close"]
298
- .ewm(span=26)
299
- .mean()
300
- )
301
-
302
- df["macd"] = ema12 - ema26
303
-
304
- df["macd_signal"] = (
305
- df["macd"]
306
- .ewm(span=9)
307
- .mean()
308
- )
309
-
310
- # EMA
311
-
312
- df["ema9"] = (
313
- df["close"]
314
- .ewm(span=9)
315
- .mean()
316
- )
317
-
318
- df["ema21"] = (
319
- df["close"]
320
- .ewm(span=21)
321
- .mean()
322
- )
323
-
324
- df["ema50"] = (
325
- df["close"]
326
- .ewm(span=50)
327
- .mean()
328
- )
329
-
330
- df["ema200"] = (
331
- df["close"]
332
- .ewm(span=200)
333
- .mean()
334
- )
335
-
336
- # BOLLINGER
337
-
338
- sma20 = (
339
- df["close"]
340
- .rolling(20)
341
- .mean()
342
- )
343
-
344
- std20 = (
345
- df["close"]
346
- .rolling(20)
347
- .std()
348
- )
349
-
350
- df["bb_upper"] = sma20 + (2 * std20)
351
-
352
- df["bb_lower"] = sma20 - (2 * std20)
353
-
354
- # ATR
355
-
356
- df["atr"] = (
357
- (df["high"] - df["low"])
358
- .rolling(14)
359
- .mean()
360
- )
361
-
362
- # VOLUME
363
-
364
- df["vol_sma"] = (
365
- df["volume"]
366
- .rolling(20)
367
- .mean()
368
- )
369
-
370
- # ADX
371
-
372
- adx = ADXIndicator(
373
- high=df["high"],
374
- low=df["low"],
375
- close=df["close"]
376
- )
377
-
378
- df["adx"] = adx.adx()
379
-
380
- # VOLATILITY
381
-
382
- df["volatility"] = (
383
- df["close"]
384
- .pct_change()
385
- .rolling(20)
386
- .std()
387
- )
388
-
389
- # REGIME
390
-
391
- def detect_regime(row):
392
-
393
- if row["volatility"] < 0.01:
394
- return "SIDEWAYS"
395
-
396
- if row["ema50"] > row["ema200"]:
397
- return "BULL"
398
-
399
- return "BEAR"
400
-
401
- df["regime"] = df.apply(
402
- detect_regime,
403
- axis=1
404
- )
405
-
406
- # RETURNS
407
-
408
- df["return1"] = (
409
- df["close"]
410
- .pct_change()
411
- )
412
-
413
- df["return4"] = (
414
- df["close"]
415
- .pct_change(4)
416
- )
417
-
418
- # LABEL
419
-
420
- df["next_close"] = (
421
- df["close"]
422
- .shift(-1)
423
- )
424
-
425
- df["label"] = (
426
- df["next_close"]
427
- >
428
- df["close"]
429
- ).astype(int)
430
-
431
- df = df.replace(
432
- [np.inf, -np.inf],
433
- np.nan
434
- )
435
-
436
- df = df.dropna()
437
-
438
- feat_cols = [
439
- "open",
440
- "high",
441
- "low",
442
- "close",
443
- "volume",
444
- "rsi",
445
- "macd",
446
- "macd_signal",
447
- "ema9",
448
- "ema21",
449
- "ema50",
450
- "ema200",
451
- "bb_upper",
452
- "bb_lower",
453
- "atr",
454
- "adx",
455
- "return1",
456
- "return4"
457
- ]
458
-
459
- X_list = []
460
- y_list = []
461
-
462
- for i in range(WINDOW, len(df)):
463
-
464
- window = df.iloc[
465
- i-WINDOW:i
466
- ][feat_cols].values
467
-
468
- if np.isnan(window).any():
469
- continue
470
-
471
- X_list.append(
472
- window.flatten()
473
- )
474
-
475
- y_list.append(
476
- df.iloc[i]["label"]
477
- )
478
-
479
- X = np.array(X_list)
480
-
481
- y = np.array(y_list)
482
-
483
- return X, y, df, feat_cols
484
-
485
-
486
- # ============================================================
487
- # TRAIN
488
- # ============================================================
489
-
490
- def run_train(symbol):
491
-
492
- try:
493
-
494
- result = fetch_and_prepare(symbol)
495
-
496
- if result is None:
497
- return False
498
-
499
- X, y, df, feat_cols = result
500
-
501
- if X is None:
502
- return False
503
-
504
- if len(X) < 50:
505
- return False
506
-
507
- scaler = MinMaxScaler()
508
-
509
- X_scaled = scaler.fit_transform(X)
510
-
511
- model = XGBClassifier(
512
- n_estimators=300,
513
- max_depth=6,
514
- learning_rate=0.03,
515
- subsample=0.8,
516
- colsample_bytree=0.8,
517
- eval_metric="logloss",
518
- random_state=42
519
- )
520
-
521
- model.fit(X_scaled, y)
522
-
523
- joblib.dump(
524
- model,
525
- model_path(symbol)
526
- )
527
-
528
- joblib.dump(
529
- scaler,
530
- scaler_path(symbol)
531
- )
532
-
533
- logger.info(
534
- f"TRAINED: {symbol}"
535
- )
536
-
537
- return True
538
-
539
- except Exception as e:
540
-
541
- logger.error(
542
- f"TRAIN ERROR {symbol}: {e}"
543
- )
544
-
545
- return False
546
-
547
-
548
- # ============================================================
549
- # TRAIN ALL
550
- # ============================================================
551
-
552
- def train_all():
553
-
554
- for symbol in SYMBOLS:
555
-
556
- try:
557
-
558
- run_train(symbol)
559
-
560
- time.sleep(1)
561
-
562
- except Exception as e:
563
-
564
- logger.error(e)
565
-
566
-
567
- # ============================================================
568
- # TRADE LEVELS
569
- # ============================================================
570
-
571
- def trade_levels(df, signal):
572
-
573
- last = df.iloc[-1]
574
-
575
- close_price = float(
576
- last["close"]
577
- )
578
-
579
- atr = float(
580
- last["atr"]
581
- )
582
-
583
- if signal == "BUY":
584
-
585
- tp1 = close_price + atr
586
- tp2 = close_price + atr * 2
587
- tp3 = close_price + atr * 3
588
-
589
- sl = close_price - atr * 1.5
590
-
591
- elif signal == "SELL":
592
-
593
- tp1 = close_price - atr
594
- tp2 = close_price - atr * 2
595
- tp3 = close_price - atr * 3
596
-
597
- sl = close_price + atr * 1.5
598
-
599
- else:
600
-
601
- tp1 = close_price
602
- tp2 = close_price
603
- tp3 = close_price
604
-
605
- sl = close_price
606
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  return {
608
- "entry": round(close_price, 2),
609
- "tp1": round(tp1, 2),
610
- "tp2": round(tp2, 2),
611
- "tp3": round(tp3, 2),
612
- "sl": round(sl, 2)
613
  }
614
 
 
 
615
 
616
- # ============================================================
617
- # PREDICT
618
- # ============================================================
619
-
620
- def run_predict(symbol):
621
-
622
- # ============================================
623
- # AUTO TRAIN
624
- # ============================================
625
-
626
- if not os.path.exists(
627
- model_path(symbol)
628
- ):
629
-
630
- ok = run_train(symbol)
631
-
632
- if not ok:
633
-
634
- return {
635
- "Pair": symbol,
636
- "Signal": "ERROR",
637
- "Confidence": 0,
638
- "Score": 0,
639
- "Entry": 0,
640
- "TP1": 0,
641
- "TP2": 0,
642
- "TP3": 0,
643
- "SL": 0,
644
- "RSI": 0,
645
- "ADX": 0,
646
- "Regime": "ERROR"
647
- }
648
-
649
- if not os.path.exists(
650
- scaler_path(symbol)
651
- ):
652
-
653
- return {
654
- "Pair": symbol,
655
- "Signal": "NO MODEL",
656
- "Confidence": 0,
657
- "Score": 0,
658
- "Entry": 0,
659
- "TP1": 0,
660
- "TP2": 0,
661
- "TP3": 0,
662
- "SL": 0,
663
- "RSI": 0,
664
- "ADX": 0,
665
- "Regime": "ERROR"
666
- }
667
-
668
-
669
- # ============================================================
670
- # REALTIME SCAN
671
- # ============================================================
672
-
673
- def realtime_scan():
674
-
675
- global LAST_RETRAIN
676
-
677
- now = time.time()
678
-
679
- # AUTO RETRAIN EVERY 6 HOURS
680
-
681
- if now - LAST_RETRAIN > 21600:
682
-
683
- logger.info(
684
- "AUTO RETRAIN..."
685
- )
686
-
687
- train_all()
688
-
689
- LAST_RETRAIN = now
690
-
691
- rows = []
692
-
693
- for symbol in SYMBOLS:
694
-
695
- try:
696
-
697
- time.sleep(0.5)
698
-
699
- result = run_predict(symbol)
700
-
701
- rows.append(result)
702
-
703
- except Exception:
704
-
705
- logger.error(
706
- traceback.format_exc()
707
- )
708
-
709
- df = pd.DataFrame(rows)
710
-
711
- if len(df) > 0:
712
-
713
- df = df.sort_values(
714
- by="Score",
715
- ascending=False
716
- )
717
 
 
 
 
 
 
718
  return df
719
 
720
-
721
- # ============================================================
722
- # UI
723
- # ============================================================
724
-
725
- with gr.Blocks(
726
- title="Crypto AI Scanner"
727
- ) as demo:
728
-
729
- gr.Markdown(
730
- "# πŸ€– Crypto AI Scanner PRO"
731
- )
732
-
733
- table = gr.Dataframe(
734
- interactive=False
735
- )
736
-
737
- timer = gr.Timer(30)
738
-
739
- timer.tick(
740
- fn=realtime_scan,
741
- outputs=table
742
- )
743
-
744
-
745
- # ============================================================
746
- # START
747
- # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
 
749
  if __name__ == "__main__":
750
-
751
- train_all()
752
-
753
- demo.queue()
754
-
755
- demo.launch(
756
- server_name="0.0.0.0",
757
- server_port=7860
758
- )
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ HUGGING FACE SPACE: BINANCE SPOT TRADING SIMULATOR
4
+ Gradio Dashboard | No API Key Required | Rule-Based Virtual Execution
5
+ """
6
+ import os, time, json, threading, logging, requests, pandas as pd
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ # ==========================================
12
+ # KONFIGURASI
13
+ # ==========================================
14
+ CONFIG = {
15
+ "WATCHLIST": ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "LINKUSDT"],
16
+ "INTERVAL": "1h",
17
+ "RISK_PCT": 0.01,
18
+ "MAX_DAILY_TRADES": 2,
19
+ "DAILY_LOSS_LIMIT": -0.03,
20
+ "DAILY_PROFIT_LOCK": 0.05,
21
+ "AI_THRESHOLD": 70,
22
+ "TP1": 0.03, "TP1_SHARE": 0.3,
23
+ "TP2": 0.05, "TP2_SHARE": 0.3,
24
+ "TP3_SHARE": 0.4,
25
+ "TRAIL_PCT": 0.02,
26
+ "TELEGRAM_TOKEN": os.getenv("TELEGRAM_TOKEN", ""),
27
+ "TELEGRAM_CHAT_ID": os.getenv("TELEGRAM_CHAT_ID", "")
28
+ }
29
+
30
+ STATE_FILE = "sim_state.json"
31
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
32
+
33
+ # ==========================================
34
+ # STATE MANAGEMENT
35
+ # ==========================================
36
+ def load_state():
37
+ if os.path.exists(STATE_FILE):
38
+ with open(STATE_FILE) as f: return json.load(f)
39
  return {
40
+ "date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
41
+ "balance": 1000.0, "trades": 0, "daily_pnl": 0.0,
42
+ "positions": [], "logs": []
 
 
43
  }
44
 
45
+ def save_state(s):
46
+ with open(STATE_FILE, "w") as f: json.dump(s, f, indent=2)
47
 
48
+ # ==========================================
49
+ # PUBLIC BINANCE FETCHER (NO API KEY)
50
+ # ==========================================
51
+ def fetch_klines(sym, interval="1h", limit=100):
52
+ try:
53
+ r = requests.get("https://api.binance.com/api/v3/klines",
54
+ params={"symbol": sym, "interval": interval, "limit": limit}, timeout=10)
55
+ r.raise_for_status()
56
+ d = r.json()
57
+ df = pd.DataFrame(d, columns=["ts","o","h","l","c","v"] + ["_"]*6)
58
+ df["ts"] = pd.to_datetime(df["ts"], unit="ms")
59
+ for c in "o h l c v".split(): df[c] = df[c].astype(float)
60
+ return df[["ts","o","h","l","c","v"]]
61
+ except Exception as e:
62
+ return pd.DataFrame()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ # ==========================================
65
+ # INDIKATOR & AI SCORE
66
+ # ==========================================
67
+ def add_ema(df, periods=[20, 50]):
68
+ for p in periods: df[f"ema_{p}"] = df["c"].ewm(span=p, adjust=False).mean()
69
  return df
70
 
71
+ def calc_ai_score(df):
72
+ if len(df) < 50: return 0
73
+ trend = 30 if df["ema_20"].iloc[-1] > df["ema_50"].iloc[-1] else 0
74
+ vol_ma = df["v"].rolling(20).mean().iloc[-1]
75
+ vol_ratio = df["v"].iloc[-1] / max(vol_ma, 1e-9)
76
+ vol = min(vol_ratio * 10, 40) if vol_ratio > 1 else 0
77
+ mom = min((df["c"].iloc[-1] / df["c"].iloc[-5] - 1) * 1000, 30)
78
+ return max(0, min(100, trend + vol + mom))
79
+
80
+ # ==========================================
81
+ # TELEGRAM ALERT
82
+ # ==========================================
83
+ def tg_send(txt):
84
+ if not CONFIG["TELEGRAM_TOKEN"] or not CONFIG["TELEGRAM_CHAT_ID"]: return
85
+ try:
86
+ requests.post(f"https://api.telegram.org/bot{CONFIG['TELEGRAM_TOKEN']}/sendMessage",
87
+ json={"chat_id": CONFIG["TELEGRAM_CHAT_ID"], "text": txt, "parse_mode": "HTML"}, timeout=5)
88
+ except: pass
89
+
90
+ def tg_fmt(emoji, title, msg):
91
+ return f"{emoji} <b>{title}</b>\n{msg}\n⏰ {datetime.now(timezone.utc).strftime('%H:%M UTC')}"
92
+
93
+ # ==========================================
94
+ # SIMULATION ENGINE
95
+ # ==========================================
96
+ class SimEngine:
97
+ def __init__(self):
98
+ self.state = load_state()
99
+ self._lock = threading.Lock()
100
+ self._running = False
101
+ self._stop = threading.Event()
102
+ self._thread = None
103
+
104
+ def start(self):
105
+ if self._running: return "⚠️ Sudah berjalan"
106
+ self._running = True
107
+ self._stop.clear()
108
+ self._thread = threading.Thread(target=self._loop, daemon=True)
109
+ self._thread.start()
110
+ return "βœ… Simulator Started"
111
+
112
+ def stop(self):
113
+ self._stop.set()
114
+ self._running = False
115
+ return "⏹ Simulator Stopped"
116
+
117
+ def _reset_daily(self):
118
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
119
+ if self.state["date"] != today:
120
+ with self._lock:
121
+ self.state.update({"date": today, "trades": 0, "daily_pnl": 0.0, "positions": []})
122
+ save_state(self.state)
123
+
124
+ def _can_trade(self):
125
+ with self._lock:
126
+ if self.state["trades"] >= CONFIG["MAX_DAILY_TRADES"]: return False, "Max 2 trade/hari"
127
+ if self.state["daily_pnl"] <= CONFIG["DAILY_LOSS_LIMIT"]: return False, "Daily Loss Limit -3%"
128
+ if self.state["daily_pnl"] >= CONFIG["DAILY_PROFIT_LOCK"]: return False, "Profit Lock +5%"
129
+ return True, "OK"
130
+
131
+ def _check_positions(self):
132
+ with self._lock:
133
+ for p in self.state["positions"][:]:
134
+ sym = p["symbol"]
135
+ df = fetch_klines(sym, CONFIG["INTERVAL"], limit=1)
136
+ if df.empty: continue
137
+ price = df["c"].iloc[-1]
138
+ pnl = (price - p["entry"]) / p["entry"]
139
+ if price > p["trail_h"]: p["trail_h"] = price
140
+
141
+ if pnl >= CONFIG["TP1"] and not p["tp1"]:
142
+ p["tp1"] = True
143
+ self._add_log(f"πŸ’° TP1 {sym} +3% (30% closed)")
144
+ tg_send(tg_fmt("πŸ’°", f"TP1 {sym}", f"+3% | 30% closed | Virtual"))
145
+ if pnl >= CONFIG["TP2"] and not p["tp2"]:
146
+ p["tp2"] = True
147
+ self._add_log(f"πŸ’° TP2 {sym} +5% (30% closed)")
148
+ tg_send(tg_fmt("πŸ’°", f"TP2 {sym}", f"+5% | 30% closed | Virtual"))
149
+
150
+ trail_sl = p["trail_h"] * (1 - CONFIG["TRAIL_PCT"])
151
+ exit_price = price
152
+ if price <= max(trail_sl, p["sl"]):
153
+ close_share = CONFIG["TP3_SHARE"] if p["tp2"] else 1.0
154
+ pnl_real = pnl * close_share
155
+ self.state["daily_pnl"] += pnl_real
156
+ self.state["balance"] *= (1 + pnl_real)
157
+ self._add_log(f"πŸ“‰ EXIT {sym} | PnL: {pnl:.2%} | Balance: ${self.state['balance']:.2f}")
158
+ tg_send(tg_fmt("πŸ“‰", f"EXIT {sym}", f"PnL: {pnl:.2%} | Virtual"))
159
+ self.state["positions"].remove(p)
160
+ save_state(self.state)
161
+ elif price <= p["sl"]:
162
+ pnl_real = pnl
163
+ self.state["daily_pnl"] += pnl_real
164
+ self.state["balance"] *= (1 + pnl_real)
165
+ self._add_log(f"β›” SL {sym} -2% | Balance: ${self.state['balance']:.2f}")
166
+ tg_send(tg_fmt("β›”", f"SL {sym}", f"Loss: -2% | Virtual"))
167
+ self.state["positions"].remove(p)
168
+ save_state(self.state)
169
+
170
+ def _loop(self):
171
+ tg_send(tg_fmt("πŸš€", "System Started", "Simulator Mode (No API Key)"))
172
+ while not self._stop.is_set():
173
+ try:
174
+ self._reset_daily()
175
+ ok, reason = self._can_trade()
176
+ if not ok:
177
+ time.sleep(60); continue
178
+
179
+ # BTC Filter
180
+ btc = add_ema(fetch_klines("BTCUSDT", CONFIG["INTERVAL"]))
181
+ if btc.empty or btc["ema_20"].iloc[-1] <= btc["ema_50"].iloc[-1]:
182
+ time.sleep(60); continue
183
+
184
+ # Scan Watchlist
185
+ for sym in CONFIG["WATCHLIST"]:
186
+ if sym == "BTCUSDT" or self._stop.is_set(): continue
187
+ df = add_ema(fetch_klines(sym, CONFIG["INTERVAL"]))
188
+ if df.empty: continue
189
+ score = calc_ai_score(df)
190
+ vol_ma = df["v"].rolling(20).mean().iloc[-1]
191
+ vol_r = df["v"].iloc[-1] / max(vol_ma, 1e-9)
192
+ if not (df["ema_20"].iloc[-1] > df["ema_50"].iloc[-1] and vol_r > 1.2 and score > CONFIG["AI_THRESHOLD"]):
193
+ continue
194
+
195
+ price = df["c"].iloc[-1]
196
+ sl = price * 0.98
197
+ risk_usd = self.state["balance"] * CONFIG["RISK_PCT"]
198
+ qty = (risk_usd / (price - sl)) / price if price > sl else 0
199
+
200
+ with self._lock:
201
+ self.state["positions"].append({
202
+ "symbol": sym, "entry": price, "qty": qty, "sl": sl,
203
+ "tp1": False, "tp2": False, "trail_h": price
204
+ })
205
+ self.state["trades"] += 1
206
+ save_state(self.state)
207
+ self._add_log(f"πŸ“₯ ENTRY {sym} @ {price:.2f} | Risk: 1% | SL: {sl:.2f}")
208
+ tg_send(tg_fmt("πŸ“₯", f"ENTRY {sym}", f"Price: {price:.2f} | Qty: {qty:.4f} | Virtual"))
209
+ break # Anti overtrading
210
+
211
+ self._check_positions()
212
+ time.sleep(300)
213
+ except Exception as e:
214
+ self._add_log(f"⚠️ {str(e)}")
215
+ time.sleep(60)
216
+ self._add_log("πŸ›‘ System Stopped")
217
+
218
+ def _add_log(self, msg):
219
+ ts = datetime.now().strftime("%H:%M:%S")
220
+ with self._lock:
221
+ self.state["logs"].append(f"[{ts}] {msg}")
222
+ if len(self.state["logs"]) > 150: self.state["logs"] = self.state["logs"][-50:]
223
+
224
+ def get_state(self):
225
+ with self._lock:
226
+ return json.loads(json.dumps(self.state)) # Deep copy
227
+
228
+ engine = SimEngine()
229
+
230
+ # ==========================================
231
+ # GRADIO DASHBOARD
232
+ # ==========================================
233
+ def ui_update():
234
+ s = engine.get_state()
235
+ logs = "\n".join(s["logs"][-20:])
236
+ pos = [[p["symbol"], f"${p['entry']:.2f}", f"{p['qty']:.4f}", f"${p['sl']:.2f}",
237
+ "WAIT" if not p["tp1"] else "TP1" if not p["tp2"] else "TP2"] for p in s["positions"]]
238
+ return (f"Balance: ${s['balance']:.2f}",
239
+ f"Trades: {s['trades']}/{CONFIG['MAX_DAILY_TRADES']}",
240
+ f"Daily PnL: {s['daily_pnl']:.2%}",
241
+ pos, logs)
242
+
243
+ with gr.Blocks(title="Binance Spot Simulator") as demo:
244
+ gr.Markdown("# πŸ“Š System Trading + Profit Management (SIMULATOR)")
245
+ gr.Markdown("*βœ… Tanpa API Key | βœ… Data Publik Binance | βœ… Semua Rule Aktif | βœ… Telegram Ready*")
246
+ with gr.Row():
247
+ btn_start = gr.Button("β–Ά START SIMULATOR", variant="primary")
248
+ btn_stop = gr.Button("⏹ STOP")
249
+ with gr.Row():
250
+ bal = gr.Textbox(label="πŸ’° Balance", value="Balance: $1000.00", interactive=False)
251
+ trades = gr.Textbox(label="πŸ“ˆ Trades Hari Ini", value="Trades: 0/2", interactive=False)
252
+ pnl = gr.Textbox(label="πŸ“Š Daily PnL", value="Daily PnL: 0.00%", interactive=False)
253
+ pos_table = gr.Dataframe(headers=["Pair", "Entry", "Qty", "SL", "Status"], value=[], interactive=False, label="πŸ“ Open Positions")
254
+ log_box = gr.Textbox(label="πŸ“ System Log", value="", lines=10, interactive=False)
255
+
256
+ btn_start.click(lambda: engine.start(), outputs=None)
257
+ btn_stop.click(lambda: engine.stop(), outputs=None)
258
+ demo.load(ui_update, inputs=None, outputs=[bal, trades, pnl, pos_table, log_box], every=5)
259
 
260
  if __name__ == "__main__":
261
+ demo.launch(server_name="0.0.0.0", server_port=7860)