Fájl részletek
Ezen az oldalon egy konkrét fájl aktuális állapotát tudod megnézni.
Fájl útvonala
/opt/bots/saturnus/app/freqtrade_executor.py
Aktuális státusz
MODIFIED
Módosítás ideje
1777296984.9445407
Korábbi baseline időpont
1772820662.920038
Előnézet (első 120 sor)
import os
import time
import json
import urllib.request
import urllib.error
from dataclasses import dataclass
from typing import Optional, Dict, Any, Tuple
@dataclass
class ExecResult:
ok: bool
action: str
detail: str
http_status: Optional[int] = None
response: Optional[Any] = None
class FreqtradeExecutor:
"""
Safe-by-default executor:
- execution disabled unless EXECUTION_ENABLED=1
- provides API calls + confirmation helpers
- does NOT write state.json
- HARD GUARD: FORCEEXIT is blocked if the open trade is not profitable after fee buffer
"""
def __init__(self):
self.enabled = os.getenv("EXECUTION_ENABLED", "0") == "1"
self.ft_base_url = os.getenv("FT_URL", "http://127.0.0.1:8089").rstrip("/")
self.username = os.getenv("FT_USERNAME", "")
self.password = os.getenv("FT_PASSWORD", "")
self.pair = os.getenv("PAIR", "")
self.timeout_sec = int(os.getenv("EXECUTION_TIMEOUT_SEC", "8"))
self.confirm_wait_sec = float(os.getenv("EXECUTION_CONFIRM_WAIT_SEC", "0.8"))
# Binance fee kb. 0.075% oldalanként; round-trip védelem alapból 0.15%
self.forceexit_min_profit_ratio = float(os.getenv("FORCEEXIT_MIN_PROFIT_RATIO", "0.0015"))
def _basic_auth_header(self) -> Dict[str, str]:
if not self.username and not self.password:
return {}
import base64
token = base64.b64encode(f"{self.username}:{self.password}".encode("utf-8")).decode("ascii")
return {"Authorization": f"Basic {token}"}
def _request_json(self, method: str, path: str, payload: Optional[Dict[str, Any]] = None) -> Tuple[int, Any]:
url = f"{self.ft_base_url}{path}"
data = None
headers = {"Content-Type": "application/json"}
headers.update(self._basic_auth_header())
if payload is not None:
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=self.timeout_sec) as resp:
raw_bytes = resp.read()
raw = raw_bytes.decode("utf-8", errors="replace") if raw_bytes else ""
try:
body = json.loads(raw) if raw else {}
except Exception:
body = {"raw": raw}
return resp.status, body
except urllib.error.HTTPError as e:
raw = ""
try:
raw_bytes = e.read()
raw = raw_bytes.decode("utf-8", errors="replace") if raw_bytes else ""
except Exception:
raw = ""
try:
body = json.loads(raw) if raw else {}
except Exception:
body = {"raw": raw}
return e.code, body
except Exception as e:
return 0, {"error": str(e)}
def get_status(self) -> ExecResult:
code, body = self._request_json("GET", "/api/v1/status", None)
ok = (code == 200)
return ExecResult(ok=ok, action="STATUS", detail="ok" if ok else "status_failed", http_status=code, response=body)
def get_trades(self) -> ExecResult:
code, body = self._request_json("GET", "/api/v1/trades", None)
ok = (code == 200)
return ExecResult(ok=ok, action="TRADES", detail="ok" if ok else "trades_failed", http_status=code, response=body)
def _get_open_trade_from_status(self, pair: Optional[str] = None) -> ExecResult:
pair = pair or self.pair
r = self.get_status()
if not r.ok:
return ExecResult(False, "OPEN_TRADE_LOOKUP", "status_failed", http_status=r.http_status, response=r.response)
raw = r.response
if not isinstance(raw, list):
return ExecResult(False, "OPEN_TRADE_LOOKUP", "unexpected_status_schema", http_status=r.http_status, response=raw)
open_trades = [x for x in raw if isinstance(x, dict) and x.get("is_open")]
if not open_trades:
return ExecResult(False, "OPEN_TRADE_LOOKUP", "no_open_trade", http_status=r.http_status, response=raw)
if pair:
for t in open_trades:
if str(t.get("pair")) == str(pair):
return ExecResult(True, "OPEN_TRADE_LOOKUP", "ok", http_status=r.http_status, response=t)
return ExecResult(False, "OPEN_TRADE_LOOKUP", "open_trade_for_pair_not_found", http_status=r.http_status, response=raw)
return ExecResult(True, "OPEN_TRADE_LOOKUP", "ok", http_status=r.http_status, response=open_trades[0])
def _to_float(self, value, default=None):
try:
if value is None or value == "":
return default
return float(value)
Csak változott diff sorok
--- baseline
+++ current
@@ -4,7 +4,7 @@
-from typing import Optional, Dict, Any, Tuple, Union
+from typing import Optional, Dict, Any, Tuple
@@ -21,11 +21,8 @@
- - does NOT write state.json (tick_runner/state manager will do it)
-
- IMPORTANT:
- - /api/v1/status is authoritative for OPEN positions (returns LIST)
- - /api/v1/trades is NOT reliable for open positions in this setup (history)
+ - does NOT write state.json
+ - HARD GUARD: FORCEEXIT is blocked if the open trade is not profitable after fee buffer
@@ -36,6 +33,9 @@
+
+ # Binance fee kb. 0.075% oldalanként; round-trip védelem alapból 0.15%
+ self.forceexit_min_profit_ratio = float(os.getenv("FORCEEXIT_MIN_PROFIT_RATIO", "0.0015"))
@@ -91,10 +91,6 @@
- """
- Authoritative open-trade lookup from /api/v1/status.
- Returns first matching open trade for the configured pair (or any open trade if pair is empty).
- """
@@ -116,6 +112,44 @@
+
+ def _to_float(self, value, default=None):
+ try:
+ if value is None or value == "":
+ return default
+ return float(value)
+ except Exception:
+ return default
+
+ def _trade_profit_ratio(self, trade: Dict[str, Any]) -> Optional[float]:
+ """
+ Freqtrade status többféle kulcsot adhat vissza.
+ Elsőként a profit_ratio mezőket használjuk, ha vannak.
+ Ha nincs, open_rate/current_rate alapján számolunk.
+ """
+ for key in (
+ "profit_ratio",
+ "current_profit",
+ "current_profit_ratio",
+ "close_profit",
+ ):
+ val = self._to_float(trade.get(key), None)
+ if val is not None:
+ return val
+
+ open_rate = self._to_float(trade.get("open_rate"), None)
+ current_rate = self._to_float(
+ trade.get("current_rate")
+ or trade.get("current_price")
+ or trade.get("close_rate")
+ or trade.get("rate"),
+ None,
+ )
+
+ if open_rate and current_rate:
+ return (current_rate - open_rate) / open_rate
+
+ return None
@@ -156,6 +190,26 @@
+ http_status=trade_lookup.http_status,
+ response=trade,
+ )
+
+ profit_ratio = self._trade_profit_ratio(trade)
+
+ if profit_ratio is None:
+ return ExecResult(
+ False,
+ "FORCEEXIT",
+ "blocked_profit_unknown",
+ http_status=trade_lookup.http_status,
+ response=trade,
+ )
+
+ if profit_ratio < self.forceexit_min_profit_ratio:
+ return ExecResult(
+ False,
+ "FORCEEXIT",
+ f"blocked_not_profitable_after_fee:profit_ratio={profit_ratio:.8f},min={self.forceexit_min_profit_ratio:.8f}",
@@ -172,10 +226,6 @@
- """
- CONFIRM MUST use /api/v1/status (LIST of open positions).
- Returns response normalized to dict: {"open_trades": <int>, "raw": <original>}
- """
Teljes diff
--- baseline
+++ current
@@ -4,7 +4,7 @@
import urllib.request
import urllib.error
from dataclasses import dataclass
-from typing import Optional, Dict, Any, Tuple, Union
+from typing import Optional, Dict, Any, Tuple
@dataclass
@@ -21,11 +21,8 @@
Safe-by-default executor:
- execution disabled unless EXECUTION_ENABLED=1
- provides API calls + confirmation helpers
- - does NOT write state.json (tick_runner/state manager will do it)
-
- IMPORTANT:
- - /api/v1/status is authoritative for OPEN positions (returns LIST)
- - /api/v1/trades is NOT reliable for open positions in this setup (history)
+ - does NOT write state.json
+ - HARD GUARD: FORCEEXIT is blocked if the open trade is not profitable after fee buffer
"""
def __init__(self):
@@ -36,6 +33,9 @@
self.pair = os.getenv("PAIR", "")
self.timeout_sec = int(os.getenv("EXECUTION_TIMEOUT_SEC", "8"))
self.confirm_wait_sec = float(os.getenv("EXECUTION_CONFIRM_WAIT_SEC", "0.8"))
+
+ # Binance fee kb. 0.075% oldalanként; round-trip védelem alapból 0.15%
+ self.forceexit_min_profit_ratio = float(os.getenv("FORCEEXIT_MIN_PROFIT_RATIO", "0.0015"))
def _basic_auth_header(self) -> Dict[str, str]:
if not self.username and not self.password:
@@ -91,10 +91,6 @@
return ExecResult(ok=ok, action="TRADES", detail="ok" if ok else "trades_failed", http_status=code, response=body)
def _get_open_trade_from_status(self, pair: Optional[str] = None) -> ExecResult:
- """
- Authoritative open-trade lookup from /api/v1/status.
- Returns first matching open trade for the configured pair (or any open trade if pair is empty).
- """
pair = pair or self.pair
r = self.get_status()
@@ -116,6 +112,44 @@
return ExecResult(False, "OPEN_TRADE_LOOKUP", "open_trade_for_pair_not_found", http_status=r.http_status, response=raw)
return ExecResult(True, "OPEN_TRADE_LOOKUP", "ok", http_status=r.http_status, response=open_trades[0])
+
+ def _to_float(self, value, default=None):
+ try:
+ if value is None or value == "":
+ return default
+ return float(value)
+ except Exception:
+ return default
+
+ def _trade_profit_ratio(self, trade: Dict[str, Any]) -> Optional[float]:
+ """
+ Freqtrade status többféle kulcsot adhat vissza.
+ Elsőként a profit_ratio mezőket használjuk, ha vannak.
+ Ha nincs, open_rate/current_rate alapján számolunk.
+ """
+ for key in (
+ "profit_ratio",
+ "current_profit",
+ "current_profit_ratio",
+ "close_profit",
+ ):
+ val = self._to_float(trade.get(key), None)
+ if val is not None:
+ return val
+
+ open_rate = self._to_float(trade.get("open_rate"), None)
+ current_rate = self._to_float(
+ trade.get("current_rate")
+ or trade.get("current_price")
+ or trade.get("close_rate")
+ or trade.get("rate"),
+ None,
+ )
+
+ if open_rate and current_rate:
+ return (current_rate - open_rate) / open_rate
+
+ return None
def force_enter(self, pair: Optional[str] = None) -> ExecResult:
pair = pair or self.pair
@@ -156,6 +190,26 @@
False,
"FORCEEXIT",
"tradeid_missing_or_invalid",
+ http_status=trade_lookup.http_status,
+ response=trade,
+ )
+
+ profit_ratio = self._trade_profit_ratio(trade)
+
+ if profit_ratio is None:
+ return ExecResult(
+ False,
+ "FORCEEXIT",
+ "blocked_profit_unknown",
+ http_status=trade_lookup.http_status,
+ response=trade,
+ )
+
+ if profit_ratio < self.forceexit_min_profit_ratio:
+ return ExecResult(
+ False,
+ "FORCEEXIT",
+ f"blocked_not_profitable_after_fee:profit_ratio={profit_ratio:.8f},min={self.forceexit_min_profit_ratio:.8f}",
http_status=trade_lookup.http_status,
response=trade,
)
@@ -172,10 +226,6 @@
)
def confirm_open_trades(self) -> ExecResult:
- """
- CONFIRM MUST use /api/v1/status (LIST of open positions).
- Returns response normalized to dict: {"open_trades": <int>, "raw": <original>}
- """
time.sleep(self.confirm_wait_sec)
r = self.get_status()