-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
234 lines (203 loc) · 9.33 KB
/
Copy pathmain.py
File metadata and controls
234 lines (203 loc) · 9.33 KB
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
#!/usr/bin/env python3
import os
import sys
import json
import time
import argparse
from dotenv import load_dotenv
import joblib
from trading.utils import setup_logger, fetch_futures_klines_df
from trading.strategy import (
build_features, make_labels, train_model, load_model_bundle, predict_signal,
compute_atr, position_size_from_atr
)
from trading.bot import BasicBot, place_protective_stop_if_requested
log = setup_logger()
load_dotenv()
def parse_args():
p = argparse.ArgumentParser(description="ML-Enhanced Binance USDT-M Futures Testnet Bot (train/backtest/live)")
sub = p.add_subparsers(dest="cmd", required=True)
# Shared
p.add_argument("--symbol", required=True, help="Futures symbol, e.g., BTCUSDT")
p.add_argument("--interval", default="5m", help="Kline interval, e.g., 1m, 5m, 15m")
p.add_argument("--limit", type=int, default=1500, help="Number of klines to load")
# API
for q in (p,):
q.add_argument("--api-key", default=os.getenv("BINANCE_API_KEY"), help="Key or env BINANCE_API_KEY")
q.add_argument("--api-secret", default=os.getenv("BINANCE_API_SECRET"), help="Secret or env BINANCE_API_SECRET")
q.add_argument("--no-testnet", action="store_true", help="Use live endpoints (NOT recommended)")
# train
sp_train = sub.add_parser("train", help="Train model")
sp_train.add_argument("--out", default="models", help="Model output dir")
# backtest
sp_bt = sub.add_parser("backtest", help="Backtest model")
sp_bt.add_argument("--model", required=True, help="Path to model .joblib bundle")
sp_bt.add_argument("--threshold", type=float, default=0.55, help="Prob threshold for long/short")
sp_bt.add_argument("--fee-bps", type=float, default=2.0, help="Approx fees per round trip in bps")
sp_bt.add_argument("--atr-n", type=int, default=14, help="ATR period")
sp_bt.add_argument("--atr-mult", type=float, default=1.5, help="Stop distance in ATR multiples")
sp_bt.add_argument("--risk-usdt", type=float, default=10.0, help="Per-trade risk budget")
# live
sp_live = sub.add_parser("live", help="Live (Testnet) trading")
sp_live.add_argument("--model", required=True, help="Path to model .joblib bundle")
sp_live.add_argument("--threshold", type=float, default=0.6, help="Prob threshold to enter")
sp_live.add_argument("--atr-n", type=int, default=14, help="ATR period")
sp_live.add_argument("--atr-mult", type=float, default=1.5, help="Stop distance in ATR multiples")
sp_live.add_argument("--risk-usdt", type=float, default=10.0, help="Per-trade risk budget")
sp_live.add_argument("--tif", default="GTC", choices=["GTC","IOC","FOK","GTX"], help="Time in force")
sp_live.add_argument("--reduce-only", action="store_true", help="Reduce-only orders")
sp_live.add_argument("--position-side", default="BOTH", choices=["BOTH","LONG","SHORT"], help="Hedge mode users")
sp_live.add_argument("--leverage", type=int, default=None, help="Optional leverage")
sp_live.add_argument("--margin", type=str, choices=["CROSSED","ISOLATED","crossed","isolated"], default=None, help="Margin type")
sp_live.add_argument("--protect-stop", action="store_true", help="Place protective STOP_MARKET after entry")
sp_live.add_argument("--loop", action="store_true", help="Run continuous loop")
sp_live.add_argument("--sleep", type=int, default=60, help="Seconds between loops")
return p.parse_args()
def cmd_train(args):
from binance import Client
client = Client(args.api_key, args.api_secret)
if not args.no_testnet:
client.FUTURES_URL = "https://testnet.binancefuture.com/fapi"
df = fetch_futures_klines_df(client, args.symbol, args.interval, args.limit)
X = build_features(df)
y = make_labels(df, horizon=1)
bundle = train_model(X, y)
os.makedirs(args.out, exist_ok=True)
out_path = os.path.join(args.out, f"{args.symbol}_{args.interval}.joblib")
bundle["meta"] = {"symbol": args.symbol, "interval": args.interval, "ts": int(time.time())}
from joblib import dump
dump(bundle, out_path)
print(json.dumps({"saved": out_path}, indent=2))
def simple_backtest(pr, df, threshold, atr, atr_mult, risk_usdt, fee_bps):
# pr: predicted proba for up move
close = df["close"].values
ret = (close[1:] - close[:-1]) / close[:-1]
decisions = []
pnl = 0.0
equity = 0.0
trades = 0
fees = 0.0
for i in range(len(pr)-1):
prob = pr[i]
side = 0
if prob >= threshold:
side = +1
elif (1.0 - prob) >= threshold:
side = -1
if side != 0:
stop_dist = atr.iloc[i] * atr_mult
qty = position_size_from_atr(close[i], stop_dist, risk_usdt)
move = ret[i+0] # one-bar ahead
pnl_trade = qty * close[i] * move * side
# Deduct fees approx
trade_notional = qty * close[i]
fee = trade_notional * (fee_bps / 10000.0) * 2.0
fees += fee
pnl += pnl_trade - fee
trades += 1
decisions.append((i, side, qty))
return {"pnl_usdt": pnl, "trades": trades, "fees": fees, "decisions": decisions}
def simple_backtest(pr, df, threshold, atr, atr_mult, risk_usdt, fee_bps):
import numpy as np
close = df["close"].values
ret = (close[1:] - close[:-1]) / close[:-1]
# Start after both ret and atr are valid
start_i = 1
if hasattr(atr, "first_valid_index") and atr.first_valid_index() is not None:
start_i = max(start_i, int(atr.index.get_loc(atr.first_valid_index())))
pnl = 0.0
fees = 0.0
trades = 0
decisions = []
for i in range(start_i, len(pr) - 1):
prob = pr[i]
if np.isnan(prob) or np.isinf(prob):
continue
side = 0
if prob >= threshold:
side = +1
elif (1.0 - prob) >= threshold:
side = -1
if side == 0:
continue
stop_dist = float(atr.iloc[i] * atr_mult)
if not np.isfinite(stop_dist) or stop_dist <= 0:
continue
qty = position_size_from_atr(close[i], stop_dist, risk_usdt)
if not np.isfinite(qty) or qty <= 0:
continue
move = ret[i] # one-bar ahead return aligned with bar i
trade_notional = qty * close[i]
fee = trade_notional * (fee_bps / 10000.0) * 2.0
pnl += qty * close[i] * move * side - fee
fees += fee
trades += 1
decisions.append((i, side, qty))
return {"pnl_usdt": float(pnl), "trades": trades, "fees": float(fees), "decisions": decisions}
def cmd_backtest(args):
from binance import Client
client = Client(args.api_key, args.api_secret)
if not args.no_testnet:
client.FUTURES_URL = "https://testnet.binancefuture.com/fapi"
df = fetch_futures_klines_df(client, args.symbol, args.interval, args.limit)
X = build_features(df)
bundle = load_model_bundle(args.model)
proba = bundle["model"].predict_proba(X[bundle["features"]])[:, 1]
atr = compute_atr(df, n=args.atr_n)
res = simple_backtest(proba, df, args.threshold, atr, args.atr_mult, args.risk_usdt, args.fee_bps)
print(json.dumps({"summary": res}, indent=2))
def one_live_iteration(args, bot, bundle, client, symbol):
df = fetch_futures_klines_df(client, symbol, args.interval, max(args.limit, 200))
X = build_features(df)
proba, side = predict_signal(bundle, X, threshold=args.threshold)
last_close = float(df["close"].iloc[-1])
atr = compute_atr(df, n=args.atr_n).iloc[-1]
stop_dist = atr * args.atr_mult
qty = position_size_from_atr(last_close, stop_dist, args.risk_usdt)
if side == 0:
log.info("No trade signal at this time")
return
# Place market order
order_side = "BUY" if side > 0 else "SELL"
resp = bot.place_market_order(symbol, order_side, quantity=qty, reduce_only=args.reduce_only, position_side=args.position_side)
print(json.dumps({"market_order": resp}, indent=2))
# Optional protective STOP_MARKET (reduceOnly closePosition)
if args.protect_stop:
place_protective_stop_if_requested(bot, symbol, side, last_close, stop_dist)
def cmd_live(args):
from binance import Client
client = Client(args.api_key, args.api_secret)
if not args.no_testnet:
client.FUTURES_URL = "https://testnet.binancefuture.com/fapi"
# Init bot and account prefs
bot = BasicBot(args.api_key, args.api_secret, testnet=(not args.no_testnet))
if args.margin:
try: bot.set_margin_type(args.symbol, args.margin.upper())
except Exception as e: log.warning(f"Margin set warning: {e}")
if args.leverage:
try: bot.set_leverage(args.symbol, args.leverage)
except Exception as e: log.warning(f"Leverage set warning: {e}")
bundle = load_model_bundle(args.model)
symbol = args.symbol.upper()
if args.loop:
while True:
try:
one_live_iteration(args, bot, bundle, client, symbol)
except Exception as e:
log.exception("Live iteration failed")
time.sleep(args.sleep)
else:
one_live_iteration(args, bot, bundle, client, symbol)
def main():
args = parse_args()
if args.cmd == "train":
cmd_train(args)
elif args.cmd == "backtest":
cmd_backtest(args)
elif args.cmd == "live":
cmd_live(args)
else:
print("Unknown command")
sys.exit(2)
if __name__ == "__main__":
main()