Initial strategy32 research and live runtime
This commit is contained in:
2
live/__init__.py
Normal file
2
live/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from __future__ import annotations
|
||||
|
||||
111
live/binance_account.py
Normal file
111
live/binance_account.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
REST_TESTNET = "https://testnet.binancefuture.com"
|
||||
REST_MAINNET = "https://fapi.binance.com"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BinanceUsdMAccountClient:
|
||||
api_key: str
|
||||
api_secret: str
|
||||
testnet: bool = False
|
||||
_response_cache: dict[str, tuple[float, Any]] = field(default_factory=dict, init=False, repr=False)
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return REST_TESTNET if self.testnet else REST_MAINNET
|
||||
|
||||
def _sign_params(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
payload = dict(params or {})
|
||||
payload["timestamp"] = int(time.time() * 1000)
|
||||
query = urlencode(payload)
|
||||
signature = hmac.new(self.api_secret.encode("utf-8"), query.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
payload["signature"] = signature
|
||||
return payload
|
||||
|
||||
def _get(self, path: str, params: dict[str, Any] | None = None, *, auth: bool = True) -> Any:
|
||||
query_params = self._sign_params(params) if auth else dict(params or {})
|
||||
url = f"{self.base_url}{path}?{urlencode(query_params)}"
|
||||
request = Request(url, headers={"X-MBX-APIKEY": self.api_key})
|
||||
with urlopen(request, timeout=15) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
|
||||
def _post(self, path: str, params: dict[str, Any] | None = None, *, auth: bool = True) -> Any:
|
||||
payload = self._sign_params(params) if auth else dict(params or {})
|
||||
encoded = urlencode(payload).encode("utf-8")
|
||||
url = f"{self.base_url}{path}"
|
||||
request = Request(url, data=encoded, method="POST", headers={"X-MBX-APIKEY": self.api_key})
|
||||
with urlopen(request, timeout=15) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
|
||||
def _get_cached(self, key: str, ttl_seconds: float, loader) -> Any:
|
||||
now = time.time()
|
||||
cached = self._response_cache.get(key)
|
||||
if cached is not None:
|
||||
loaded_at, payload = cached
|
||||
if now - loaded_at <= ttl_seconds:
|
||||
return payload
|
||||
payload = loader()
|
||||
self._response_cache[key] = (now, payload)
|
||||
return payload
|
||||
|
||||
def get_balance(self) -> list[dict[str, Any]]:
|
||||
payload = self._get_cached("balance", 10.0, lambda: list(self._get("/fapi/v2/balance")))
|
||||
return list(payload)
|
||||
|
||||
def get_position_risk(self) -> list[dict[str, Any]]:
|
||||
payload = self._get_cached("position_risk", 5.0, lambda: list(self._get("/fapi/v2/positionRisk")))
|
||||
return list(payload)
|
||||
|
||||
def get_exchange_info(self) -> dict[str, Any]:
|
||||
payload = self._get_cached("exchange_info", 3600.0, lambda: dict(self._get("/fapi/v1/exchangeInfo", auth=False)))
|
||||
return dict(payload)
|
||||
|
||||
def get_ticker_price(self, symbol: str | None = None) -> Any:
|
||||
params = {"symbol": symbol} if symbol else None
|
||||
key = f"ticker_price:{symbol or '*'}"
|
||||
return self._get_cached(key, 5.0, lambda: self._get("/fapi/v1/ticker/price", params=params, auth=False))
|
||||
|
||||
def set_leverage(self, symbol: str, leverage: int) -> dict[str, Any]:
|
||||
return dict(self._post("/fapi/v1/leverage", {"symbol": symbol, "leverage": max(1, int(leverage))}))
|
||||
|
||||
def place_market_order(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
side: str,
|
||||
quantity: float,
|
||||
reduce_only: bool = False,
|
||||
client_order_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
params: dict[str, Any] = {
|
||||
"symbol": symbol,
|
||||
"side": side.upper(),
|
||||
"type": "MARKET",
|
||||
"quantity": self._format_decimal(quantity),
|
||||
"newOrderRespType": "RESULT",
|
||||
}
|
||||
if reduce_only:
|
||||
params["reduceOnly"] = "true"
|
||||
if client_order_id:
|
||||
clean = "".join(ch for ch in str(client_order_id) if ch.isalnum() or ch in "-_.")
|
||||
if clean:
|
||||
params["newClientOrderId"] = clean[:36]
|
||||
response = dict(self._post("/fapi/v1/order", params))
|
||||
self._response_cache.pop("balance", None)
|
||||
self._response_cache.pop("position_risk", None)
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _format_decimal(value: float) -> str:
|
||||
return ("%.12f" % float(value)).rstrip("0").rstrip(".")
|
||||
27
live/env.py
Normal file
27
live/env.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_dotenv(path: str | Path) -> None:
|
||||
target = Path(path)
|
||||
if not target.exists():
|
||||
return
|
||||
for raw_line in target.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip("'\"")
|
||||
if key and key not in os.environ:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def env_bool(name: str, default: bool = False) -> bool:
|
||||
raw = os.getenv(name)
|
||||
if raw is None:
|
||||
return default
|
||||
return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
|
||||
|
||||
275
live/executor.py
Normal file
275
live/executor.py
Normal file
@@ -0,0 +1,275 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from strategy32.live.binance_account import BinanceUsdMAccountClient
|
||||
|
||||
|
||||
KNOWN_QUOTES = ("USDT", "USDC")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SymbolRule:
|
||||
contract_symbol: str
|
||||
base_asset: str
|
||||
quote_asset: str
|
||||
step_size: float
|
||||
min_qty: float
|
||||
min_notional: float
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class LiveExecutionConfig:
|
||||
enabled: bool = False
|
||||
leverage: int = 1
|
||||
min_target_notional_usd: float = 25.0
|
||||
min_rebalance_notional_usd: float = 10.0
|
||||
close_orphan_positions: bool = True
|
||||
entry_only_refinement: bool = True
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ExecutionResult:
|
||||
executed_at: str
|
||||
enabled: bool
|
||||
account_equity_usd: float
|
||||
target_symbols: list[str] = field(default_factory=list)
|
||||
orders: list[dict[str, Any]] = field(default_factory=list)
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class LiveFuturesExecutor:
|
||||
def __init__(self, client: BinanceUsdMAccountClient, config: LiveExecutionConfig) -> None:
|
||||
self.client = client
|
||||
self.config = config
|
||||
self._rules: dict[str, SymbolRule] | None = None
|
||||
self._applied_leverage: set[str] = set()
|
||||
|
||||
def reconcile(self, snapshot: dict[str, Any]) -> ExecutionResult:
|
||||
result = ExecutionResult(
|
||||
executed_at=str(snapshot.get("generated_at", "")),
|
||||
enabled=self.config.enabled,
|
||||
account_equity_usd=self._account_equity_usd(),
|
||||
)
|
||||
quote_by_symbol = dict(snapshot.get("universe", {}).get("quote_by_symbol", {}))
|
||||
execution_targets = snapshot.get("execution_targets")
|
||||
if isinstance(execution_targets, list):
|
||||
target_rows = list(execution_targets)
|
||||
else:
|
||||
target_rows = [
|
||||
row
|
||||
for row in snapshot.get("combined_targets", [])
|
||||
if bool(row.get("tradeable")) and str(row.get("instrument", "")).startswith("perp:")
|
||||
]
|
||||
result.target_symbols = [str(row["instrument"]).split(":", 1)[1] for row in target_rows]
|
||||
if not self.config.enabled:
|
||||
return result
|
||||
|
||||
target_weights = {
|
||||
str(row["instrument"]).split(":", 1)[1]: float(row.get("weight", 0.0) or 0.0)
|
||||
for row in target_rows
|
||||
}
|
||||
desired_weights = {
|
||||
str(row["instrument"]).split(":", 1)[1]: float(row.get("desired_weight", row.get("weight", 0.0)) or 0.0)
|
||||
for row in target_rows
|
||||
}
|
||||
current_positions = self._current_positions()
|
||||
all_symbols = sorted(set(target_weights) | set(current_positions if self.config.close_orphan_positions else target_weights))
|
||||
prices = self._prices_for_symbols(all_symbols, quote_by_symbol, current_positions)
|
||||
|
||||
for base_symbol in all_symbols:
|
||||
quote_asset = quote_by_symbol.get(base_symbol, current_positions.get(base_symbol, {}).get("quote_asset", "USDT"))
|
||||
rule = self._symbol_rule(base_symbol, quote_asset)
|
||||
if rule is None:
|
||||
result.warnings.append(f"missing_symbol_rule:{base_symbol}")
|
||||
continue
|
||||
|
||||
target_weight = float(target_weights.get(base_symbol, 0.0) or 0.0)
|
||||
price = float(prices.get(base_symbol, 0.0) or 0.0)
|
||||
if price <= 0:
|
||||
result.warnings.append(f"missing_price:{base_symbol}")
|
||||
continue
|
||||
|
||||
target_notional = target_weight * result.account_equity_usd
|
||||
if abs(target_notional) < max(self.config.min_target_notional_usd, rule.min_notional):
|
||||
target_notional = 0.0
|
||||
|
||||
current_qty = float(current_positions.get(base_symbol, {}).get("qty", 0.0) or 0.0)
|
||||
current_contract = current_positions.get(base_symbol, {}).get("contract_symbol", rule.contract_symbol)
|
||||
if self.config.entry_only_refinement:
|
||||
desired_weight = float(desired_weights.get(base_symbol, target_weight) or 0.0)
|
||||
desired_notional = desired_weight * result.account_equity_usd
|
||||
current_notional = current_qty * price
|
||||
if (
|
||||
current_qty != 0.0
|
||||
and desired_notional != 0.0
|
||||
and math.copysign(1.0, current_notional) == math.copysign(1.0, desired_notional)
|
||||
and abs(target_notional) < abs(desired_notional)
|
||||
):
|
||||
target_notional = math.copysign(
|
||||
min(abs(current_notional), abs(desired_notional)),
|
||||
desired_notional,
|
||||
)
|
||||
|
||||
target_qty = self._normalize_qty(rule, target_notional / price)
|
||||
|
||||
if current_qty != 0.0 and target_qty != 0.0 and math.copysign(1.0, current_qty) != math.copysign(1.0, target_qty):
|
||||
close_order = self._submit_delta(
|
||||
contract_symbol=current_contract,
|
||||
delta_qty=-current_qty,
|
||||
price=price,
|
||||
reduce_only=True,
|
||||
)
|
||||
if close_order is not None:
|
||||
result.orders.append(close_order)
|
||||
current_qty = 0.0
|
||||
|
||||
delta_qty = target_qty - current_qty
|
||||
delta_notional = abs(delta_qty) * price
|
||||
if delta_notional < max(self.config.min_rebalance_notional_usd, rule.min_notional):
|
||||
continue
|
||||
|
||||
order = self._submit_delta(
|
||||
contract_symbol=rule.contract_symbol,
|
||||
delta_qty=delta_qty,
|
||||
price=price,
|
||||
reduce_only=False,
|
||||
)
|
||||
if order is not None:
|
||||
result.orders.append(order)
|
||||
|
||||
return result
|
||||
|
||||
def _submit_delta(
|
||||
self,
|
||||
*,
|
||||
contract_symbol: str,
|
||||
delta_qty: float,
|
||||
price: float,
|
||||
reduce_only: bool,
|
||||
) -> dict[str, Any] | None:
|
||||
qty = abs(float(delta_qty))
|
||||
if qty <= 0:
|
||||
return None
|
||||
side = "BUY" if delta_qty > 0 else "SELL"
|
||||
self._ensure_leverage(contract_symbol)
|
||||
response = self.client.place_market_order(
|
||||
symbol=contract_symbol,
|
||||
side=side,
|
||||
quantity=qty,
|
||||
reduce_only=reduce_only,
|
||||
client_order_id=f"s32-{contract_symbol.lower()}-{side.lower()}",
|
||||
)
|
||||
return {
|
||||
"symbol": contract_symbol,
|
||||
"side": side,
|
||||
"quantity": qty,
|
||||
"price_ref": price,
|
||||
"reduce_only": reduce_only,
|
||||
"response": response,
|
||||
}
|
||||
|
||||
def _ensure_leverage(self, contract_symbol: str) -> None:
|
||||
if contract_symbol in self._applied_leverage:
|
||||
return
|
||||
self.client.set_leverage(contract_symbol, self.config.leverage)
|
||||
self._applied_leverage.add(contract_symbol)
|
||||
|
||||
def _account_equity_usd(self) -> float:
|
||||
balances = self.client.get_balance()
|
||||
total = 0.0
|
||||
for row in balances:
|
||||
asset = str(row.get("asset", "")).upper()
|
||||
if asset in KNOWN_QUOTES:
|
||||
total += float(row.get("balance", 0.0) or 0.0)
|
||||
return total
|
||||
|
||||
def _current_positions(self) -> dict[str, dict[str, Any]]:
|
||||
rows = self.client.get_position_risk()
|
||||
positions: dict[str, dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
qty = float(row.get("positionAmt", 0.0) or 0.0)
|
||||
if abs(qty) <= 1e-12:
|
||||
continue
|
||||
contract_symbol = str(row.get("symbol", "")).upper()
|
||||
base_asset, quote_asset = self._split_contract_symbol(contract_symbol)
|
||||
positions[base_asset] = {
|
||||
"qty": qty,
|
||||
"quote_asset": quote_asset,
|
||||
"contract_symbol": contract_symbol,
|
||||
"mark_price": float(row.get("markPrice", 0.0) or 0.0),
|
||||
}
|
||||
return positions
|
||||
|
||||
def _prices_for_symbols(
|
||||
self,
|
||||
symbols: list[str],
|
||||
quote_by_symbol: dict[str, str],
|
||||
current_positions: dict[str, dict[str, Any]],
|
||||
) -> dict[str, float]:
|
||||
prices: dict[str, float] = {}
|
||||
for base_symbol in symbols:
|
||||
current = current_positions.get(base_symbol)
|
||||
if current is not None and float(current.get("mark_price", 0.0) or 0.0) > 0:
|
||||
prices[base_symbol] = float(current["mark_price"])
|
||||
continue
|
||||
quote_asset = quote_by_symbol.get(base_symbol, "USDT")
|
||||
contract_symbol = f"{base_symbol}{quote_asset}"
|
||||
ticker = self.client.get_ticker_price(contract_symbol)
|
||||
if isinstance(ticker, dict):
|
||||
prices[base_symbol] = float(ticker.get("price", 0.0) or 0.0)
|
||||
return prices
|
||||
|
||||
def _symbol_rule(self, base_symbol: str, quote_asset: str) -> SymbolRule | None:
|
||||
rules = self._load_rules()
|
||||
return rules.get(f"{base_symbol}{quote_asset}")
|
||||
|
||||
def _load_rules(self) -> dict[str, SymbolRule]:
|
||||
if self._rules is not None:
|
||||
return self._rules
|
||||
info = self.client.get_exchange_info()
|
||||
rules: dict[str, SymbolRule] = {}
|
||||
for row in info.get("symbols", []):
|
||||
contract_symbol = str(row.get("symbol", "")).upper()
|
||||
if not contract_symbol:
|
||||
continue
|
||||
step_size = 0.0
|
||||
min_qty = 0.0
|
||||
min_notional = 5.0
|
||||
for flt in row.get("filters", []):
|
||||
flt_type = str(flt.get("filterType", ""))
|
||||
if flt_type in {"LOT_SIZE", "MARKET_LOT_SIZE"}:
|
||||
step_size = max(step_size, float(flt.get("stepSize", 0.0) or 0.0))
|
||||
min_qty = max(min_qty, float(flt.get("minQty", 0.0) or 0.0))
|
||||
elif flt_type == "MIN_NOTIONAL":
|
||||
min_notional = max(min_notional, float(flt.get("notional", 0.0) or 0.0))
|
||||
rules[contract_symbol] = SymbolRule(
|
||||
contract_symbol=contract_symbol,
|
||||
base_asset=str(row.get("baseAsset", "")).upper(),
|
||||
quote_asset=str(row.get("quoteAsset", "")).upper(),
|
||||
step_size=step_size or 0.001,
|
||||
min_qty=min_qty or step_size or 0.001,
|
||||
min_notional=min_notional,
|
||||
)
|
||||
self._rules = rules
|
||||
return rules
|
||||
|
||||
@staticmethod
|
||||
def _normalize_qty(rule: SymbolRule, raw_qty: float) -> float:
|
||||
if abs(raw_qty) <= 0:
|
||||
return 0.0
|
||||
sign = 1.0 if raw_qty > 0 else -1.0
|
||||
step = max(rule.step_size, 1e-12)
|
||||
qty = math.floor(abs(raw_qty) / step) * step
|
||||
if qty < max(rule.min_qty, step):
|
||||
return 0.0
|
||||
return sign * qty
|
||||
|
||||
@staticmethod
|
||||
def _split_contract_symbol(contract_symbol: str) -> tuple[str, str]:
|
||||
for quote in KNOWN_QUOTES:
|
||||
if contract_symbol.endswith(quote):
|
||||
return contract_symbol[: -len(quote)], quote
|
||||
return contract_symbol, "USDT"
|
||||
129
live/notifier.py
Normal file
129
live/notifier.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotifierConfig:
|
||||
bot_token: str
|
||||
chat_id: str
|
||||
min_level: str = "INFO"
|
||||
rate_limit_per_sec: float = 5.0
|
||||
|
||||
|
||||
class Notifier:
|
||||
LEVELS = {"INFO": 10, "WARNING": 20, "CRITICAL": 30}
|
||||
|
||||
def __init__(self, config: NotifierConfig | None = None) -> None:
|
||||
self.config = config
|
||||
self.enabled = config is not None and bool(config.bot_token) and bool(config.chat_id)
|
||||
self._queue: asyncio.PriorityQueue[tuple[int, float, str, str]] = asyncio.PriorityQueue(maxsize=1000)
|
||||
self._worker: asyncio.Task | None = None
|
||||
self._stopping = False
|
||||
self._last_send_ts = 0.0
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "Notifier":
|
||||
token = (
|
||||
os.getenv("GAMMA_TELEGRAM_TOKEN", "").strip()
|
||||
or os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
|
||||
)
|
||||
chat_id = (
|
||||
os.getenv("GAMMA_TELEGRAM_CHAT_ID", "").strip()
|
||||
or os.getenv("TELEGRAM_CHAT_ID", "").strip()
|
||||
)
|
||||
min_level = (
|
||||
os.getenv("GAMMA_TELEGRAM_MIN_LEVEL", "").strip().upper()
|
||||
or os.getenv("TELEGRAM_MIN_LEVEL", "INFO").strip().upper()
|
||||
or "INFO"
|
||||
)
|
||||
if not token or not chat_id:
|
||||
return cls(config=None)
|
||||
return cls(config=NotifierConfig(bot_token=token, chat_id=chat_id, min_level=min_level))
|
||||
|
||||
async def start(self) -> None:
|
||||
if not self.enabled or self._worker is not None:
|
||||
return
|
||||
self._stopping = False
|
||||
self._worker = asyncio.create_task(self._worker_loop(), name="strategy32-telegram-notifier")
|
||||
|
||||
async def stop(self) -> None:
|
||||
self._stopping = True
|
||||
if self._worker is None:
|
||||
return
|
||||
self._worker.cancel()
|
||||
try:
|
||||
await self._worker
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
self._worker = None
|
||||
|
||||
async def send(self, level: str, message: str) -> bool:
|
||||
level = level.upper().strip()
|
||||
if not self.enabled:
|
||||
return False
|
||||
if self.LEVELS.get(level, 0) < self.LEVELS.get(self.config.min_level.upper(), 10):
|
||||
return False
|
||||
try:
|
||||
priority = 0 if level == "CRITICAL" else 1
|
||||
self._queue.put_nowait((priority, time.time(), level, _mask_sensitive(message)))
|
||||
return True
|
||||
except asyncio.QueueFull:
|
||||
return False
|
||||
|
||||
async def _worker_loop(self) -> None:
|
||||
while not self._stopping:
|
||||
_priority, _ts, level, message = await self._queue.get()
|
||||
text = f"[{level}] {message}"
|
||||
try:
|
||||
await self._send_telegram(text)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
async def _send_telegram(self, text: str) -> None:
|
||||
min_interval = 1.0 / max(1e-6, float(self.config.rate_limit_per_sec))
|
||||
elapsed = time.time() - self._last_send_ts
|
||||
if elapsed < min_interval:
|
||||
await asyncio.sleep(min_interval - elapsed)
|
||||
|
||||
encoded = urllib.parse.urlencode(
|
||||
{
|
||||
"chat_id": self.config.chat_id,
|
||||
"text": text,
|
||||
"disable_web_page_preview": "true",
|
||||
}
|
||||
).encode("utf-8")
|
||||
url = f"https://api.telegram.org/bot{self.config.bot_token}/sendMessage"
|
||||
|
||||
def _call() -> None:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=encoded,
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
raw = resp.read().decode("utf-8")
|
||||
data = json.loads(raw)
|
||||
if not isinstance(data, dict) or not bool(data.get("ok")):
|
||||
raise RuntimeError("telegram_send_failed")
|
||||
|
||||
await asyncio.to_thread(_call)
|
||||
self._last_send_ts = time.time()
|
||||
|
||||
|
||||
def _mask_sensitive(text: str) -> str:
|
||||
out = str(text)
|
||||
out = re.sub(r"0x[a-fA-F0-9]{64}", "0x***MASKED_PRIVATE_KEY***", out)
|
||||
out = re.sub(r"0x[a-fA-F0-9]{40}", "0x***MASKED_ADDRESS***", out)
|
||||
out = re.sub(r"\b\d{7,}:[A-Za-z0-9_-]{20,}\b", "***MASKED_TOKEN***", out)
|
||||
return out
|
||||
1071
live/runtime.py
Normal file
1071
live/runtime.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user