Fájl részletek

Ezen az oldalon egy konkrét fájl aktuális állapotát tudod megnézni.

Vissza a fájltérképhez Csak változott Stratégia-labor Monitor főoldal ← Előző módosult Következő módosult →
Fájl útvonala
/opt/bots/saturnus/app/tick_runner.py
Létezik most?
IGEN
Aktuális státusz
MODIFIED
Méret
72424
Módosítás ideje
1779912201.361905
Korábbi baseline időpont
1776011347.683918
SHA256 rövid

Előnézet (első 120 sor)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Saturnus tick_runner V2
- fetch last candles via Freqtrade REST API
- sync position from Freqtrade
- compute V2 levels (STANDARD / RECOVERY / PANIC + catastrophe zones)
- persist recovery arming state
- call rule_engine.decide(ctx)
- optional executor wiring
"""

from __future__ import annotations

import os
import json
import time
import base64
from datetime import datetime, timezone
from typing import Any, Tuple
from urllib.request import Request, urlopen


APP_DIR = os.path.dirname(os.path.abspath(__file__))

STATE_PATH = os.getenv("STATE_PATH", "/opt/bots/saturnus/state.json")
FT_URL = os.getenv("FT_URL", "http://127.0.0.1:8089").rstrip("/")
FT_TIMEOUT = float(os.getenv("FT_TIMEOUT", "8"))

TICK_SECONDS = float(os.getenv("TICK_SECONDS", os.getenv("SATURNUS_TICK_SECONDS", "10")))
PAIR = os.getenv("PAIR", "SOL/USDC")
TIMEFRAME = os.getenv("TIMEFRAME", "1m")

# =========================================================
# HTF (Higher Timeframe) debug-only Phase-1
# =========================================================
HTF_TIMEFRAME = os.getenv("HTF_TIMEFRAME", "1h")
HTF_ENABLED = os.getenv("HTF_ENABLED", "1").strip().lower() in ("1", "true", "yes", "on")
HTF_LIMIT = int(os.getenv("HTF_LIMIT", "120"))

LIMIT = int(os.getenv("LIMIT", "2"))
MA_SHORT_PERIOD = int(os.getenv("MA_SHORT_PERIOD", "5"))
MA_LONG_PERIOD = int(os.getenv("MA_LONG_PERIOD", "20"))

FEE_PCT = float(os.getenv("FEE_PCT", "0.1"))
SLIPPAGE_PCT = float(os.getenv("SLIPPAGE_PCT", "0.0"))

FT_USERNAME = os.getenv("FT_USERNAME", "").strip()
FT_PASSWORD = os.getenv("FT_PASSWORD", "").strip()

EXECUTION_ENABLED = os.getenv("EXECUTION_ENABLED", "0") == "1"
EXECUTION_CONFIRM = os.getenv("EXECUTION_CONFIRM", "1") == "1"
EXECUTION_LOG_ONLY = os.getenv("EXECUTION_LOG_ONLY", "1") == "1"

STD_SELL_PCT = float(os.getenv("STD_SELL_PCT", "0.01"))
RECOVERY_SELL_RETRACE_PCT = float(os.getenv("RECOVERY_SELL_RETRACE_PCT", "0.006"))
PANIC_SELL_PCT = float(os.getenv("PANIC_SELL_PCT", "0.01"))
CATASTROPHE_SELL_PCT = float(os.getenv("CATASTROPHE_SELL_PCT", "0.10"))

STD_BUY_PCT = float(os.getenv("STD_BUY_PCT", "0.01"))
RECOVERY_BUY_REBOUND_PCT = float(os.getenv("RECOVERY_BUY_REBOUND_PCT", "0.006"))
PANIC_BUY_PCT = float(os.getenv("PANIC_BUY_PCT", "0.01"))
CATASTROPHE_BUY_PCT = float(os.getenv("CATASTROPHE_BUY_PCT", "0.10"))

RECOVERY_ARM_TIMEOUT_BARS = int(os.getenv("RECOVERY_ARM_TIMEOUT_BARS", "240"))
MIN_REBOUND_CONFIRM_PCT = float(os.getenv("MIN_REBOUND_CONFIRM_PCT", "0.003"))
SPREAD_MAX_PCT = float(os.getenv("SPREAD_MAX_PCT", "0.002"))
STALENESS_MAX_MS = int(os.getenv("STALENESS_MAX_MS", "500"))
PROFIT_BUFFER_PCT = float(os.getenv("PROFIT_BUFFER_PCT", "0.001"))
COST_GUARD_ENABLED = os.getenv("COST_GUARD_ENABLED", "1").strip().lower() in ("1", "true", "yes", "on")


def _runtime_guardrail_flags() -> tuple[bool, int, float]:
    kill_switch = str(os.getenv("KILL_SWITCH", "0")).strip().lower() in ("1", "true", "yes", "on")
    max_trades = int(os.getenv("MAX_TRADES_PER_DAY", "6"))
    daily_loss_cap_pct = float(os.getenv("DAILY_LOSS_CAP_PCT", "2.0"))
    return kill_switch, max_trades, daily_loss_cap_pct


def now_utc_iso() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def log(msg: str) -> None:
    ts = now_utc_iso()
    print(f"{ts} [tick_runner] {msg}", flush=True)


def _env_bool(name: str, default: bool = False) -> bool:
    raw = os.getenv(name)
    if raw is None:
        return bool(default)
    return str(raw).strip().lower() in ("1", "true", "yes", "on")


def _env_int(name: str, default: int) -> int:
    raw = os.getenv(name)
    if raw is None:
        return int(default)
    try:
        return int(str(raw).strip())
    except Exception:
        return int(default)
def _env_float(name: str, default: float = 0.0) -> float:
    try:
        val = os.getenv(name)
        if val is None or str(val).strip() == "":
            return float(default)
        return float(val)
    except Exception:
        return float(default)

def _as_float(x) -> float | None:
    try:
        return float(x) if x is not None else None
    except Exception:
        return None


def _runtime_exec_flags() -> tuple[bool, bool, bool]:

Csak változott diff sorok

--- baseline +++ current @@ -1,107 +1,2070 @@ -# tick_runner.py -# Saturnus – tick loop + recovery state kezelés - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Saturnus tick_runner V2 +- fetch last candles via Freqtrade REST API +- sync position from Freqtrade +- compute V2 levels (STANDARD / RECOVERY / PANIC + catastrophe zones) +- persist recovery arming state +- call rule_engine.decide(ctx) +- optional executor wiring +""" + +from __future__ import annotations + +import os +import json -from typing import Dict, Any - -from rule_engine import decide - +import base64 +from datetime import datetime, timezone +from typing import Any, Tuple +from urllib.request import Request, urlopen + + +APP_DIR = os.path.dirname(os.path.abspath(__file__)) + +STATE_PATH = os.getenv("STATE_PATH", "/opt/bots/saturnus/state.json") +FT_URL = os.getenv("FT_URL", "http://127.0.0.1:8089").rstrip("/") +FT_TIMEOUT = float(os.getenv("FT_TIMEOUT", "8")) + +TICK_SECONDS = float(os.getenv("TICK_SECONDS", os.getenv("SATURNUS_TICK_SECONDS", "10"))) +PAIR = os.getenv("PAIR", "SOL/USDC") +TIMEFRAME = os.getenv("TIMEFRAME", "1m") -# State kezelés +# HTF (Higher Timeframe) debug-only Phase-1 - -def ensure_recovery_fields(state: Dict[str, Any]): - cycle = state.setdefault("cycle", {}) - - cycle.setdefault("recovery_mode", False) - cycle.setdefault("recovery_loss_pct", 0.0) - cycle.setdefault("recovery_anchor_price", None) - - -def enter_recovery(state: Dict[str, Any], entry_price: float, exit_price: float): - cycle = state["cycle"] - - loss_pct = max(0.0, (entry_price - exit_price) / entry_price) - - cycle["recovery_mode"] = True - cycle["recovery_loss_pct"] = loss_pct - cycle["recovery_anchor_price"] = entry_price - - print(f"[RECOVERY ENTER] loss={loss_pct:.5f} anchor={entry_price}") - - -def exit_recovery(state: Dict[str, Any]): - cycle = state["cycle"] +HTF_TIMEFRAME = os.getenv("HTF_TIMEFRAME", "1h") +HTF_ENABLED = os.getenv("HTF_ENABLED", "1").strip().lower() in ("1", "true", "yes", "on") +HTF_LIMIT = int(os.getenv("HTF_LIMIT", "120")) + +LIMIT = int(os.getenv("LIMIT", "2")) +MA_SHORT_PERIOD = int(os.getenv("MA_SHORT_PERIOD", "5")) +MA_LONG_PERIOD = int(os.getenv("MA_LONG_PERIOD", "20")) + +FEE_PCT = float(os.getenv("FEE_PCT", "0.1")) +SLIPPAGE_PCT = float(os.getenv("SLIPPAGE_PCT", "0.0")) + +FT_USERNAME = os.getenv("FT_USERNAME", "").strip() +FT_PASSWORD = os.getenv("FT_PASSWORD", "").strip() + +EXECUTION_ENABLED = os.getenv("EXECUTION_ENABLED", "0") == "1" +EXECUTION_CONFIRM = os.getenv("EXECUTION_CONFIRM", "1") == "1" +EXECUTION_LOG_ONLY = os.getenv("EXECUTION_LOG_ONLY", "1") == "1" + +STD_SELL_PCT = float(os.getenv("STD_SELL_PCT", "0.01")) +RECOVERY_SELL_RETRACE_PCT = float(os.getenv("RECOVERY_SELL_RETRACE_PCT", "0.006")) +PANIC_SELL_PCT = float(os.getenv("PANIC_SELL_PCT", "0.01")) +CATASTROPHE_SELL_PCT = float(os.getenv("CATASTROPHE_SELL_PCT", "0.10")) + +STD_BUY_PCT = float(os.getenv("STD_BUY_PCT", "0.01")) +RECOVERY_BUY_REBOUND_PCT = float(os.getenv("RECOVERY_BUY_REBOUND_PCT", "0.006")) +PANIC_BUY_PCT = float(os.getenv("PANIC_BUY_PCT", "0.01")) +CATASTROPHE_BUY_PCT = float(os.getenv("CATASTROPHE_BUY_PCT", "0.10")) + +RECOVERY_ARM_TIMEOUT_BARS = int(os.getenv("RECOVERY_ARM_TIMEOUT_BARS", "240")) +MIN_REBOUND_CONFIRM_PCT = float(os.getenv("MIN_REBOUND_CONFIRM_PCT", "0.003")) +SPREAD_MAX_PCT = float(os.getenv("SPREAD_MAX_PCT", "0.002")) +STALENESS_MAX_MS = int(os.getenv("STALENESS_MAX_MS", "500")) +PROFIT_BUFFER_PCT = float(os.getenv("PROFIT_BUFFER_PCT", "0.001")) +COST_GUARD_ENABLED = os.getenv("COST_GUARD_ENABLED", "1").strip().lower() in ("1", "true", "yes", "on") + + +def _runtime_guardrail_flags() -> tuple[bool, int, float]: + kill_switch = str(os.getenv("KILL_SWITCH", "0")).strip().lower() in ("1", "true", "yes", "on") + max_trades = int(os.getenv("MAX_TRADES_PER_DAY", "6")) + daily_loss_cap_pct = float(os.getenv("DAILY_LOSS_CAP_PCT", "2.0")) + return kill_switch, max_trades, daily_loss_cap_pct + + +def now_utc_iso() -> str: + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def log(msg: str) -> None: + ts = now_utc_iso() + print(f"{ts} [tick_runner] {msg}", flush=True) + + +def _env_bool(name: str, default: bool = False) -> bool: + raw = os.getenv(name) + if raw is None: + return bool(default) + return str(raw).strip().lower() in ("1", "true", "yes", "on") + + +def _env_int(name: str, default: int) -> int: + raw = os.getenv(name) + if raw is None: + return int(default) + try: + return int(str(raw).strip()) + except Exception: + return int(default) +def _env_float(name: str, default: float = 0.0) -> float: + try: + val = os.getenv(name) + if val is None or str(val).strip() == "": + return float(default) + return float(val) + except Exception: + return float(default) + +def _as_float(x) -> float | None: + try: + return float(x) if x is not None else None + except Exception: + return None + + +def _runtime_exec_flags() -> tuple[bool, bool, bool]: + enabled = _env_bool("EXECUTION_ENABLED", False) + log_only = _env_bool("EXECUTION_LOG_ONLY", True) + confirm = _env_bool("EXECUTION_CONFIRM", True) + return enabled, log_only, confirm + + +def _read_state() -> dict: + try: + with open(STATE_PATH, "r", encoding="utf-8") as f: + s = json.load(f) + return s if isinstance(s, dict) else {} + except FileNotFoundError: + return {} + except Exception: + return {} + + +def _write_state(state: dict) -> None: + tmp = STATE_PATH + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(state, f, ensure_ascii=False, indent=2, sort_keys=True) + f.write("\n") + os.replace(tmp, STATE_PATH) + + +def _auth_headers() -> dict: + if FT_USERNAME and FT_PASSWORD: + token = base64.b64encode(f"{FT_USERNAME}:{FT_PASSWORD}".encode("utf-8")).decode("ascii") + return {"Authorization": f"Basic {token}"} + return {} + + +def _http_get_json(url: str) -> Any: + headers = _auth_headers() + req = Request(url, headers=headers) + with urlopen(req, timeout=FT_TIMEOUT) as r: + data = r.read().decode("utf-8", "replace") + return json.loads(data) + + +def fetch_candles(pair: str, timeframe: str, *, limit: int = 2) -> list: + """ + Freqtrade 2025.x kompatibilis candle fetch. + A működő endpoint: /api/v1/pair_candles + Válaszforma: + {"columns": [...], "data": [[date, open, high, low, close, volume, ...], ...]}

Teljes diff

--- baseline +++ current @@ -1,107 +1,2070 @@ -# tick_runner.py -# Saturnus – tick loop + recovery state kezelés - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Saturnus tick_runner V2 +- fetch last candles via Freqtrade REST API +- sync position from Freqtrade +- compute V2 levels (STANDARD / RECOVERY / PANIC + catastrophe zones) +- persist recovery arming state +- call rule_engine.decide(ctx) +- optional executor wiring +""" + +from __future__ import annotations + +import os +import json import time -from typing import Dict, Any - -from rule_engine import decide - +import base64 +from datetime import datetime, timezone +from typing import Any, Tuple +from urllib.request import Request, urlopen + + +APP_DIR = os.path.dirname(os.path.abspath(__file__)) + +STATE_PATH = os.getenv("STATE_PATH", "/opt/bots/saturnus/state.json") +FT_URL = os.getenv("FT_URL", "http://127.0.0.1:8089").rstrip("/") +FT_TIMEOUT = float(os.getenv("FT_TIMEOUT", "8")) + +TICK_SECONDS = float(os.getenv("TICK_SECONDS", os.getenv("SATURNUS_TICK_SECONDS", "10"))) +PAIR = os.getenv("PAIR", "SOL/USDC") +TIMEFRAME = os.getenv("TIMEFRAME", "1m") # ========================================================= -# State kezelés +# HTF (Higher Timeframe) debug-only Phase-1 # ========================================================= - -def ensure_recovery_fields(state: Dict[str, Any]): - cycle = state.setdefault("cycle", {}) - - cycle.setdefault("recovery_mode", False) - cycle.setdefault("recovery_loss_pct", 0.0) - cycle.setdefault("recovery_anchor_price", None) - - -def enter_recovery(state: Dict[str, Any], entry_price: float, exit_price: float): - cycle = state["cycle"] - - loss_pct = max(0.0, (entry_price - exit_price) / entry_price) - - cycle["recovery_mode"] = True - cycle["recovery_loss_pct"] = loss_pct - cycle["recovery_anchor_price"] = entry_price - - print(f"[RECOVERY ENTER] loss={loss_pct:.5f} anchor={entry_price}") - - -def exit_recovery(state: Dict[str, Any]): - cycle = state["cycle"] +HTF_TIMEFRAME = os.getenv("HTF_TIMEFRAME", "1h") +HTF_ENABLED = os.getenv("HTF_ENABLED", "1").strip().lower() in ("1", "true", "yes", "on") +HTF_LIMIT = int(os.getenv("HTF_LIMIT", "120")) + +LIMIT = int(os.getenv("LIMIT", "2")) +MA_SHORT_PERIOD = int(os.getenv("MA_SHORT_PERIOD", "5")) +MA_LONG_PERIOD = int(os.getenv("MA_LONG_PERIOD", "20")) + +FEE_PCT = float(os.getenv("FEE_PCT", "0.1")) +SLIPPAGE_PCT = float(os.getenv("SLIPPAGE_PCT", "0.0")) + +FT_USERNAME = os.getenv("FT_USERNAME", "").strip() +FT_PASSWORD = os.getenv("FT_PASSWORD", "").strip() + +EXECUTION_ENABLED = os.getenv("EXECUTION_ENABLED", "0") == "1" +EXECUTION_CONFIRM = os.getenv("EXECUTION_CONFIRM", "1") == "1" +EXECUTION_LOG_ONLY = os.getenv("EXECUTION_LOG_ONLY", "1") == "1" + +STD_SELL_PCT = float(os.getenv("STD_SELL_PCT", "0.01")) +RECOVERY_SELL_RETRACE_PCT = float(os.getenv("RECOVERY_SELL_RETRACE_PCT", "0.006")) +PANIC_SELL_PCT = float(os.getenv("PANIC_SELL_PCT", "0.01")) +CATASTROPHE_SELL_PCT = float(os.getenv("CATASTROPHE_SELL_PCT", "0.10")) + +STD_BUY_PCT = float(os.getenv("STD_BUY_PCT", "0.01")) +RECOVERY_BUY_REBOUND_PCT = float(os.getenv("RECOVERY_BUY_REBOUND_PCT", "0.006")) +PANIC_BUY_PCT = float(os.getenv("PANIC_BUY_PCT", "0.01")) +CATASTROPHE_BUY_PCT = float(os.getenv("CATASTROPHE_BUY_PCT", "0.10")) + +RECOVERY_ARM_TIMEOUT_BARS = int(os.getenv("RECOVERY_ARM_TIMEOUT_BARS", "240")) +MIN_REBOUND_CONFIRM_PCT = float(os.getenv("MIN_REBOUND_CONFIRM_PCT", "0.003")) +SPREAD_MAX_PCT = float(os.getenv("SPREAD_MAX_PCT", "0.002")) +STALENESS_MAX_MS = int(os.getenv("STALENESS_MAX_MS", "500")) +PROFIT_BUFFER_PCT = float(os.getenv("PROFIT_BUFFER_PCT", "0.001")) +COST_GUARD_ENABLED = os.getenv("COST_GUARD_ENABLED", "1").strip().lower() in ("1", "true", "yes", "on") + + +def _runtime_guardrail_flags() -> tuple[bool, int, float]: + kill_switch = str(os.getenv("KILL_SWITCH", "0")).strip().lower() in ("1", "true", "yes", "on") + max_trades = int(os.getenv("MAX_TRADES_PER_DAY", "6")) + daily_loss_cap_pct = float(os.getenv("DAILY_LOSS_CAP_PCT", "2.0")) + return kill_switch, max_trades, daily_loss_cap_pct + + +def now_utc_iso() -> str: + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def log(msg: str) -> None: + ts = now_utc_iso() + print(f"{ts} [tick_runner] {msg}", flush=True) + + +def _env_bool(name: str, default: bool = False) -> bool: + raw = os.getenv(name) + if raw is None: + return bool(default) + return str(raw).strip().lower() in ("1", "true", "yes", "on") + + +def _env_int(name: str, default: int) -> int: + raw = os.getenv(name) + if raw is None: + return int(default) + try: + return int(str(raw).strip()) + except Exception: + return int(default) +def _env_float(name: str, default: float = 0.0) -> float: + try: + val = os.getenv(name) + if val is None or str(val).strip() == "": + return float(default) + return float(val) + except Exception: + return float(default) + +def _as_float(x) -> float | None: + try: + return float(x) if x is not None else None + except Exception: + return None + + +def _runtime_exec_flags() -> tuple[bool, bool, bool]: + enabled = _env_bool("EXECUTION_ENABLED", False) + log_only = _env_bool("EXECUTION_LOG_ONLY", True) + confirm = _env_bool("EXECUTION_CONFIRM", True) + return enabled, log_only, confirm + + +def _read_state() -> dict: + try: + with open(STATE_PATH, "r", encoding="utf-8") as f: + s = json.load(f) + return s if isinstance(s, dict) else {} + except FileNotFoundError: + return {} + except Exception: + return {} + + +def _write_state(state: dict) -> None: + tmp = STATE_PATH + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(state, f, ensure_ascii=False, indent=2, sort_keys=True) + f.write("\n") + os.replace(tmp, STATE_PATH) + + +def _auth_headers() -> dict: + if FT_USERNAME and FT_PASSWORD: + token = base64.b64encode(f"{FT_USERNAME}:{FT_PASSWORD}".encode("utf-8")).decode("ascii") + return {"Authorization": f"Basic {token}"} + return {} + + +def _http_get_json(url: str) -> Any: + headers = _auth_headers() + req = Request(url, headers=headers) + with urlopen(req, timeout=FT_TIMEOUT) as r: + data = r.read().decode("utf-8", "replace") + return json.loads(data) + + +def fetch_candles(pair: str, timeframe: str, *, limit: int = 2) -> list: + """ + Freqtrade 2025.x kompatibilis candle fetch. + A működő endpoint: /api/v1/pair_candles + Válaszforma: + {"columns": [...], "data": [[date, open, high, low, close, volume, ...], ...]} ... [DIFF LEVÁGVA] további sorok: 1964