Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 647e3e565f | |||
| a5a39a6d27 | |||
| 839285d75b |
@@ -42,6 +42,9 @@ x-bite-env: &bite-env
|
|||||||
CERBERO_BITE_MCP_HYPERLIQUID_URL: ${CERBERO_BITE_MCP_HYPERLIQUID_URL:-http://cerbero-mcp:9000/mcp-hyperliquid}
|
CERBERO_BITE_MCP_HYPERLIQUID_URL: ${CERBERO_BITE_MCP_HYPERLIQUID_URL:-http://cerbero-mcp:9000/mcp-hyperliquid}
|
||||||
CERBERO_BITE_MCP_MACRO_URL: ${CERBERO_BITE_MCP_MACRO_URL:-http://cerbero-mcp:9000/mcp-macro}
|
CERBERO_BITE_MCP_MACRO_URL: ${CERBERO_BITE_MCP_MACRO_URL:-http://cerbero-mcp:9000/mcp-macro}
|
||||||
CERBERO_BITE_MCP_SENTIMENT_URL: ${CERBERO_BITE_MCP_SENTIMENT_URL:-http://cerbero-mcp:9000/mcp-sentiment}
|
CERBERO_BITE_MCP_SENTIMENT_URL: ${CERBERO_BITE_MCP_SENTIMENT_URL:-http://cerbero-mcp:9000/mcp-sentiment}
|
||||||
|
# MCP V2 unified interface (/mcp): get_instruments, get_historical,
|
||||||
|
# get_indicators — removed from the per-exchange routers.
|
||||||
|
CERBERO_BITE_MCP_UNIFIED_URL: ${CERBERO_BITE_MCP_UNIFIED_URL:-http://cerbero-mcp:9000/mcp}
|
||||||
|
|
||||||
services:
|
services:
|
||||||
cerbero-bite:
|
cerbero-bite:
|
||||||
|
|||||||
@@ -120,12 +120,29 @@ def _to_decimal(value: Any) -> Decimal | None:
|
|||||||
class DeribitClient:
|
class DeribitClient:
|
||||||
SERVICE = "deribit"
|
SERVICE = "deribit"
|
||||||
|
|
||||||
def __init__(self, http: HttpToolClient) -> None:
|
def __init__(
|
||||||
|
self, http: HttpToolClient, unified: HttpToolClient | None = None
|
||||||
|
) -> None:
|
||||||
if http.service != self.SERVICE:
|
if http.service != self.SERVICE:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"DeribitClient requires service '{self.SERVICE}', got '{http.service}'"
|
f"DeribitClient requires service '{self.SERVICE}', got '{http.service}'"
|
||||||
)
|
)
|
||||||
self._http = http
|
self._http = http
|
||||||
|
# Cerbero MCP V2 moved the data tools (get_instruments,
|
||||||
|
# get_historical, get_indicators) to the unified ``/mcp`` router;
|
||||||
|
# they are no longer on ``/mcp-deribit``. This second client points
|
||||||
|
# at ``/mcp`` and is only needed by those three methods — diagnostic
|
||||||
|
# paths (e.g. ``environment_info`` for ping) leave it ``None``.
|
||||||
|
self._unified = unified
|
||||||
|
|
||||||
|
def _unified_client(self, tool: str) -> HttpToolClient:
|
||||||
|
if self._unified is None:
|
||||||
|
raise McpDataAnomalyError(
|
||||||
|
f"unified MCP client not configured; '{tool}' lives on /mcp",
|
||||||
|
service=self.SERVICE,
|
||||||
|
tool=tool,
|
||||||
|
)
|
||||||
|
return self._unified
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Environment / health
|
# Environment / health
|
||||||
@@ -206,7 +223,16 @@ class DeribitClient:
|
|||||||
limit: int = 500,
|
limit: int = 500,
|
||||||
) -> list[InstrumentMeta]:
|
) -> list[InstrumentMeta]:
|
||||||
"""Return option instruments matching the filters as typed metadata."""
|
"""Return option instruments matching the filters as typed metadata."""
|
||||||
body: dict[str, Any] = {"currency": currency, "kind": "option", "limit": limit}
|
# MCP V2: get_instruments is unified-only (/mcp). The venue is
|
||||||
|
# selected via ``exchange`` and each row is normalized — the native
|
||||||
|
# symbol is ``symbol`` and Deribit-specific fields live under
|
||||||
|
# ``native``.
|
||||||
|
body: dict[str, Any] = {
|
||||||
|
"exchange": self.SERVICE,
|
||||||
|
"currency": currency,
|
||||||
|
"kind": "option",
|
||||||
|
"limit": limit,
|
||||||
|
}
|
||||||
if expiry_from is not None:
|
if expiry_from is not None:
|
||||||
body["expiry_from"] = expiry_from.date().isoformat()
|
body["expiry_from"] = expiry_from.date().isoformat()
|
||||||
if expiry_to is not None:
|
if expiry_to is not None:
|
||||||
@@ -214,28 +240,31 @@ class DeribitClient:
|
|||||||
if min_open_interest is not None:
|
if min_open_interest is not None:
|
||||||
body["min_open_interest"] = min_open_interest
|
body["min_open_interest"] = min_open_interest
|
||||||
|
|
||||||
raw = await self._http.call("get_instruments", body)
|
raw = await self._unified_client("get_instruments").call(
|
||||||
|
"get_instruments", body
|
||||||
|
)
|
||||||
instruments = raw.get("instruments") or []
|
instruments = raw.get("instruments") or []
|
||||||
out: list[InstrumentMeta] = []
|
out: list[InstrumentMeta] = []
|
||||||
for entry in instruments:
|
for entry in instruments:
|
||||||
if not isinstance(entry, dict):
|
if not isinstance(entry, dict):
|
||||||
continue
|
continue
|
||||||
name = entry.get("name")
|
name = entry.get("symbol")
|
||||||
if not isinstance(name, str):
|
if not isinstance(name, str):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
strike, expiry, option_type = _parse_instrument(name)
|
strike, expiry, option_type = _parse_instrument(name)
|
||||||
except McpDataAnomalyError:
|
except McpDataAnomalyError:
|
||||||
continue
|
continue
|
||||||
|
native = entry.get("native") if isinstance(entry.get("native"), dict) else {}
|
||||||
out.append(
|
out.append(
|
||||||
InstrumentMeta(
|
InstrumentMeta(
|
||||||
name=name,
|
name=name,
|
||||||
strike=strike,
|
strike=strike,
|
||||||
expiry=expiry,
|
expiry=expiry,
|
||||||
option_type=option_type,
|
option_type=option_type,
|
||||||
open_interest=_to_decimal(entry.get("open_interest")),
|
open_interest=_to_decimal(native.get("open_interest")),
|
||||||
tick_size=_to_decimal(entry.get("tick_size")),
|
tick_size=_to_decimal(entry.get("tick_size")),
|
||||||
min_trade_amount=_to_decimal(entry.get("min_trade_amount")),
|
min_trade_amount=_to_decimal(native.get("min_trade_amount")),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
@@ -291,13 +320,17 @@ class DeribitClient:
|
|||||||
in :func:`compute_bias`. Returns ``None`` when the chain has no
|
in :func:`compute_bias`. Returns ``None`` when the chain has no
|
||||||
data in the window.
|
data in the window.
|
||||||
"""
|
"""
|
||||||
raw = await self._http.call(
|
# MCP V2: get_historical is unified-only (/mcp); it takes
|
||||||
|
# ``exchange`` + lowercase ``interval`` (e.g. "1d", "1h") instead of
|
||||||
|
# the old per-exchange ``resolution``.
|
||||||
|
raw = await self._unified_client("get_historical").call(
|
||||||
"get_historical",
|
"get_historical",
|
||||||
{
|
{
|
||||||
|
"exchange": self.SERVICE,
|
||||||
"instrument": instrument,
|
"instrument": instrument,
|
||||||
"start_date": start.date().isoformat(),
|
"start_date": start.date().isoformat(),
|
||||||
"end_date": end.date().isoformat(),
|
"end_date": end.date().isoformat(),
|
||||||
"resolution": resolution,
|
"interval": resolution.lower(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
candles = (raw or {}).get("candles") or []
|
candles = (raw or {}).get("candles") or []
|
||||||
@@ -422,28 +455,34 @@ class DeribitClient:
|
|||||||
resolution: str = "1h",
|
resolution: str = "1h",
|
||||||
) -> Decimal | None:
|
) -> Decimal | None:
|
||||||
"""Return the most recent ADX(14) value, or ``None`` when missing."""
|
"""Return the most recent ADX(14) value, or ``None`` when missing."""
|
||||||
raw = await self._http.call(
|
# MCP V2: indicators are computed by the unified ``get_indicators``
|
||||||
"get_technical_indicators",
|
# tool (/mcp), which replaced the per-exchange
|
||||||
|
# ``get_technical_indicators``. Body takes ``exchange`` + lowercase
|
||||||
|
# ``interval``; the response nests each indicator under
|
||||||
|
# ``indicators`` (ADX as ``{"adx": <float>, "+di":…, "-di":…}``).
|
||||||
|
raw = await self._unified_client("get_indicators").call(
|
||||||
|
"get_indicators",
|
||||||
{
|
{
|
||||||
|
"exchange": self.SERVICE,
|
||||||
"instrument": instrument,
|
"instrument": instrument,
|
||||||
"indicators": ["adx"],
|
"indicators": ["adx"],
|
||||||
"start_date": start.date().isoformat(),
|
"start_date": start.date().isoformat(),
|
||||||
"end_date": end.date().isoformat(),
|
"end_date": end.date().isoformat(),
|
||||||
"resolution": resolution,
|
"interval": resolution.lower(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
return None
|
return None
|
||||||
# The MCP server returns either a top-level dict with the
|
indicators = raw.get("indicators")
|
||||||
# indicator keyed by name, or a list of points. Be tolerant.
|
if not isinstance(indicators, dict):
|
||||||
adx_payload = raw.get("adx") or raw.get("ADX") or raw.get("indicators", {})
|
return None
|
||||||
if isinstance(adx_payload, list) and adx_payload:
|
adx_block = indicators.get("adx")
|
||||||
tail = adx_payload[-1]
|
if isinstance(adx_block, dict):
|
||||||
value = tail.get("value") if isinstance(tail, dict) else tail
|
value = adx_block.get("adx")
|
||||||
return None if value is None else Decimal(str(value))
|
|
||||||
if isinstance(adx_payload, dict):
|
|
||||||
value = adx_payload.get("latest") or adx_payload.get("value")
|
|
||||||
return None if value is None else Decimal(str(value))
|
return None if value is None else Decimal(str(value))
|
||||||
|
# Tolerate a flattened scalar shape just in case.
|
||||||
|
if adx_block is not None and not isinstance(adx_block, list):
|
||||||
|
return Decimal(str(adx_block))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_account_summary(self, currency: str = "USDC") -> dict[str, Any]:
|
async def get_account_summary(self, currency: str = "USDC") -> dict[str, Any]:
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ DEFAULT_ENDPOINTS: dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Cerbero MCP V2 unified interface (``/mcp``). The data tools
|
||||||
|
# ``get_instruments`` / ``get_historical`` / ``get_indicators`` live here
|
||||||
|
# ONLY — they were removed from the per-exchange routers in MCP V2. The
|
||||||
|
# caller passes ``exchange="deribit"`` in the body to target one venue.
|
||||||
|
_UNIFIED_ENV = "CERBERO_BITE_MCP_UNIFIED_URL"
|
||||||
|
_DEFAULT_UNIFIED_URL = "http://cerbero-mcp:9000/mcp"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class McpEndpoints:
|
class McpEndpoints:
|
||||||
"""Resolved per-service URLs."""
|
"""Resolved per-service URLs."""
|
||||||
@@ -70,6 +78,7 @@ class McpEndpoints:
|
|||||||
hyperliquid: str
|
hyperliquid: str
|
||||||
macro: str
|
macro: str
|
||||||
sentiment: str
|
sentiment: str
|
||||||
|
unified: str = _DEFAULT_UNIFIED_URL
|
||||||
|
|
||||||
def for_service(self, name: str) -> str:
|
def for_service(self, name: str) -> str:
|
||||||
try:
|
try:
|
||||||
@@ -85,6 +94,10 @@ def load_endpoints(env: dict[str, str] | None = None) -> McpEndpoints:
|
|||||||
for name, (host, port, env_var) in MCP_SERVICES.items():
|
for name, (host, port, env_var) in MCP_SERVICES.items():
|
||||||
override = e.get(env_var)
|
override = e.get(env_var)
|
||||||
resolved[name] = override.rstrip("/") if override else _default_url(host, port)
|
resolved[name] = override.rstrip("/") if override else _default_url(host, port)
|
||||||
|
unified_override = e.get(_UNIFIED_ENV)
|
||||||
|
resolved["unified"] = (
|
||||||
|
unified_override.rstrip("/") if unified_override else _DEFAULT_UNIFIED_URL
|
||||||
|
)
|
||||||
return McpEndpoints(**resolved)
|
return McpEndpoints(**resolved)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ class EntryConfig(BaseModel):
|
|||||||
no_position_concurrent: bool = True
|
no_position_concurrent: bool = True
|
||||||
exclude_macro_severity: list[str] = Field(default_factory=lambda: ["high"])
|
exclude_macro_severity: list[str] = Field(default_factory=lambda: ["high"])
|
||||||
exclude_macro_countries: list[str] = Field(default_factory=lambda: ["US", "EU"])
|
exclude_macro_countries: list[str] = Field(default_factory=lambda: ["US", "EU"])
|
||||||
|
# Finestra (giorni) entro cui un evento macro high-severity blocca
|
||||||
|
# l'entry. Disaccoppiata da `structure.dte_target`: il filtro macro
|
||||||
|
# è una protezione di rischio evento, indipendente dalla scadenza
|
||||||
|
# scelta per le opzioni. Default 18 = comportamento storico.
|
||||||
|
exclude_macro_within_days: int = 18
|
||||||
|
|
||||||
# directional bias (§3.1)
|
# directional bias (§3.1)
|
||||||
trend_window_days: int = 30
|
trend_window_days: int = 30
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ def validate_entry(ctx: EntryContext, cfg: StrategyConfig) -> EntryDecision:
|
|||||||
"""
|
"""
|
||||||
reasons: list[str] = []
|
reasons: list[str] = []
|
||||||
entry_cfg = cfg.entry
|
entry_cfg = cfg.entry
|
||||||
structure_cfg = cfg.structure
|
|
||||||
|
|
||||||
if ctx.has_open_position:
|
if ctx.has_open_position:
|
||||||
reasons.append("open position already exists")
|
reasons.append("open position already exists")
|
||||||
@@ -118,7 +117,7 @@ def validate_entry(ctx: EntryContext, cfg: StrategyConfig) -> EntryDecision:
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
ctx.next_macro_event_in_days is not None
|
ctx.next_macro_event_in_days is not None
|
||||||
and ctx.next_macro_event_in_days <= structure_cfg.dte_target
|
and ctx.next_macro_event_in_days <= entry_cfg.exclude_macro_within_days
|
||||||
):
|
):
|
||||||
reasons.append(
|
reasons.append(
|
||||||
f"macro event within DTE window ({ctx.next_macro_event_in_days} days)"
|
f"macro event within DTE window ({ctx.next_macro_event_in_days} days)"
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ async def _fetch_balances_async(*, timeout_s: float = 8.0) -> BalancesSnapshot:
|
|||||||
client=http_client,
|
client=http_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
deribit = DeribitClient(_client("deribit"))
|
deribit = DeribitClient(_client("deribit"), _client("unified"))
|
||||||
hl = HyperliquidClient(_client("hyperliquid"))
|
hl = HyperliquidClient(_client("hyperliquid"))
|
||||||
macro = MacroClient(_client("macro"))
|
macro = MacroClient(_client("macro"))
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ def build_runtime(
|
|||||||
telegram=telegram, audit_log=audit_log, kill_switch=kill_switch
|
telegram=telegram, audit_log=audit_log, kill_switch=kill_switch
|
||||||
)
|
)
|
||||||
|
|
||||||
deribit = DeribitClient(_client("deribit"))
|
deribit = DeribitClient(_client("deribit"), _client("unified"))
|
||||||
macro = MacroClient(_client("macro"))
|
macro = MacroClient(_client("macro"))
|
||||||
sentiment = SentimentClient(_client("sentiment"))
|
sentiment = SentimentClient(_client("sentiment"))
|
||||||
hyperliquid = HyperliquidClient(_client("hyperliquid"))
|
hyperliquid = HyperliquidClient(_client("hyperliquid"))
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ async def _gather_snapshot(
|
|||||||
)
|
)
|
||||||
macro_t: asyncio.Task[int | None] = asyncio.create_task(
|
macro_t: asyncio.Task[int | None] = asyncio.create_task(
|
||||||
macro.next_high_severity_within(
|
macro.next_high_severity_within(
|
||||||
days=cfg.structure.dte_target,
|
days=cfg.entry.exclude_macro_within_days,
|
||||||
countries=list(cfg.entry.exclude_macro_countries),
|
countries=list(cfg.entry.exclude_macro_countries),
|
||||||
now=now,
|
now=now,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ from cerbero_bite.runtime.option_chain_snapshot_cycle import (
|
|||||||
from cerbero_bite.runtime.monitor_cycle import MonitorCycleResult, run_monitor_cycle
|
from cerbero_bite.runtime.monitor_cycle import MonitorCycleResult, run_monitor_cycle
|
||||||
from cerbero_bite.runtime.recovery import recover_state
|
from cerbero_bite.runtime.recovery import recover_state
|
||||||
from cerbero_bite.runtime.scheduler import JobSpec, build_scheduler
|
from cerbero_bite.runtime.scheduler import JobSpec, build_scheduler
|
||||||
|
from cerbero_bite.safety.audit_log import tail_continues_from
|
||||||
from cerbero_bite.state import connect as connect_state
|
from cerbero_bite.state import connect as connect_state
|
||||||
|
from cerbero_bite.state import transaction
|
||||||
|
|
||||||
__all__ = ["Orchestrator"]
|
__all__ = ["Orchestrator"]
|
||||||
|
|
||||||
@@ -50,8 +52,8 @@ _log = logging.getLogger("cerbero_bite.runtime.orchestrator")
|
|||||||
Environment = Literal["testnet", "mainnet"]
|
Environment = Literal["testnet", "mainnet"]
|
||||||
|
|
||||||
# Default cron schedule (matches docs/06-operational-flow.md table).
|
# Default cron schedule (matches docs/06-operational-flow.md table).
|
||||||
_CRON_ENTRY = "0 14 * * *" # crypto 24/7: candidatura giornaliera; i gate decidono se entrare
|
_CRON_ENTRY = "0 */2 * * *" # crypto 24/7: valutazione ogni 2h; i gate decidono se entrare
|
||||||
_CRON_MONITOR = "0 2,14 * * *"
|
_CRON_MONITOR = "0 * * * *" # stop/take-profit check ogni ora
|
||||||
_CRON_HEALTH = "*/5 * * * *"
|
_CRON_HEALTH = "*/5 * * * *"
|
||||||
_CRON_BACKUP = "0 * * * *"
|
_CRON_BACKUP = "0 * * * *"
|
||||||
_CRON_MANUAL_ACTIONS = "*/1 * * * *"
|
_CRON_MANUAL_ACTIONS = "*/1 * * * *"
|
||||||
@@ -158,7 +160,30 @@ class Orchestrator:
|
|||||||
if state is None or state.last_audit_hash is None:
|
if state is None or state.last_audit_hash is None:
|
||||||
return # first boot, nothing to compare against
|
return # first boot, nothing to compare against
|
||||||
actual_tail = self._ctx.audit_log.last_hash
|
actual_tail = self._ctx.audit_log.last_hash
|
||||||
if actual_tail != state.last_audit_hash:
|
if actual_tail == state.last_audit_hash:
|
||||||
|
return
|
||||||
|
# The anchor is persisted best-effort (see build_runtime): under
|
||||||
|
# SQLite write contention the mirror can fall behind the log while
|
||||||
|
# the log itself keeps growing forward, intact. Treat that benign
|
||||||
|
# lag — anchor is a valid ancestor of the current tail — as a
|
||||||
|
# re-sync, not tampering. Only a missing anchor or a broken
|
||||||
|
# post-anchor chain (truncation/tampering) arms the kill switch.
|
||||||
|
if tail_continues_from(self._ctx.audit_log.path, state.last_audit_hash):
|
||||||
|
conn = connect_state(self._ctx.db_path)
|
||||||
|
try:
|
||||||
|
with transaction(conn):
|
||||||
|
self._ctx.repository.set_last_audit_hash(
|
||||||
|
conn, hex_hash=actual_tail
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
_log.warning(
|
||||||
|
"audit anchor lagged behind tail; re-synced "
|
||||||
|
"(anchor=%s…, tail=%s…)",
|
||||||
|
state.last_audit_hash[:12],
|
||||||
|
actual_tail[:12],
|
||||||
|
)
|
||||||
|
return
|
||||||
await self._ctx.alert_manager.critical(
|
await self._ctx.alert_manager.critical(
|
||||||
source="orchestrator.boot",
|
source="orchestrator.boot",
|
||||||
message=(
|
message=(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ __all__ = [
|
|||||||
"AuditChainError",
|
"AuditChainError",
|
||||||
"AuditEntry",
|
"AuditEntry",
|
||||||
"AuditLog",
|
"AuditLog",
|
||||||
|
"tail_continues_from",
|
||||||
"verify_chain",
|
"verify_chain",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -157,6 +158,55 @@ def verify_chain(path: str | Path) -> int:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def tail_continues_from(path: str | Path, anchor_hash: str) -> bool:
|
||||||
|
"""Return ``True`` when *anchor_hash* is a genuine ancestor of the tail.
|
||||||
|
|
||||||
|
That is: *anchor_hash* is the ``hash`` of some line in the log AND every
|
||||||
|
line after it forms a valid forward chain to EOF. This distinguishes a
|
||||||
|
**benign anchor lag** — the best-effort anchor persisted in
|
||||||
|
:func:`cerbero_bite.runtime.dependencies.build_runtime` fell behind the
|
||||||
|
file under SQLite write contention, yet the log grew forward
|
||||||
|
legitimately — from real **truncation/tampering** (anchor absent, or the
|
||||||
|
post-anchor chain broken).
|
||||||
|
|
||||||
|
Returns ``False`` if the file is missing/empty, the anchor is not found,
|
||||||
|
or the chain from the anchor to the tail does not verify. The single-
|
||||||
|
writer invariant of :class:`AuditLog` still holds; this only makes the
|
||||||
|
boot-time anchor check tolerant of the durability gap it documents.
|
||||||
|
"""
|
||||||
|
p = Path(path)
|
||||||
|
if not p.exists() or p.stat().st_size == 0:
|
||||||
|
return False
|
||||||
|
seen = False
|
||||||
|
expected_prev = ""
|
||||||
|
with p.open("r", encoding="utf-8") as fh:
|
||||||
|
for line in fh:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
entry = _parse_line(line)
|
||||||
|
except AuditChainError:
|
||||||
|
if seen:
|
||||||
|
return False
|
||||||
|
continue
|
||||||
|
if seen:
|
||||||
|
if entry.prev_hash != expected_prev:
|
||||||
|
return False
|
||||||
|
recomputed = _compute_hash(
|
||||||
|
entry.timestamp.isoformat(),
|
||||||
|
entry.event,
|
||||||
|
_canonical_payload(entry.payload),
|
||||||
|
entry.prev_hash,
|
||||||
|
)
|
||||||
|
if recomputed != entry.hash:
|
||||||
|
return False
|
||||||
|
expected_prev = entry.hash
|
||||||
|
elif entry.hash == anchor_hash:
|
||||||
|
seen = True
|
||||||
|
expected_prev = entry.hash
|
||||||
|
return seen
|
||||||
|
|
||||||
|
|
||||||
def iter_entries(path: str | Path) -> Iterator[AuditEntry]:
|
def iter_entries(path: str | Path) -> Iterator[AuditEntry]:
|
||||||
"""Yield each :class:`AuditEntry` from *path* without verifying."""
|
"""Yield each :class:`AuditEntry` from *path* without verifying."""
|
||||||
p = Path(path)
|
p = Path(path)
|
||||||
|
|||||||
+18
-9
@@ -6,9 +6,9 @@
|
|||||||
# config hash), and lands as a separate commit with the motivation in
|
# config hash), and lands as a separate commit with the motivation in
|
||||||
# the commit message.
|
# the commit message.
|
||||||
|
|
||||||
config_version: "1.4.0"
|
config_version: "1.6.0"
|
||||||
config_hash: "22182814216190331e0b69b3bc99493e6d69cc813f7ed937394986eecc1f5d11"
|
config_hash: "67df85f84ce6148b88f7eb9d96346145c1b9892a7250f5fc42619da3178dac86"
|
||||||
last_review: "2026-04-26"
|
last_review: "2026-05-29"
|
||||||
last_reviewer: "Adriano"
|
last_reviewer: "Adriano"
|
||||||
|
|
||||||
asset:
|
asset:
|
||||||
@@ -16,7 +16,7 @@ asset:
|
|||||||
exchange: "deribit"
|
exchange: "deribit"
|
||||||
|
|
||||||
entry:
|
entry:
|
||||||
cron: "0 14 * * *"
|
cron: "0 */2 * * *"
|
||||||
skip_holidays_country: "IT"
|
skip_holidays_country: "IT"
|
||||||
|
|
||||||
capital_min_usd: "720"
|
capital_min_usd: "720"
|
||||||
@@ -27,12 +27,15 @@ entry:
|
|||||||
no_position_concurrent: true
|
no_position_concurrent: true
|
||||||
exclude_macro_severity: ["high"]
|
exclude_macro_severity: ["high"]
|
||||||
exclude_macro_countries: ["US", "EU"]
|
exclude_macro_countries: ["US", "EU"]
|
||||||
|
# Finestra evento macro ridotta a 1 giorno: blocca l'entry solo se un
|
||||||
|
# evento high-severity cade entro 24h, invece dei 18gg (dte_target).
|
||||||
|
exclude_macro_within_days: 1
|
||||||
|
|
||||||
trend_window_days: 30
|
trend_window_days: 30
|
||||||
trend_bull_threshold_pct: "0.05"
|
trend_bull_threshold_pct: "0.05"
|
||||||
trend_bear_threshold_pct: "-0.05"
|
trend_bear_threshold_pct: "-0.05"
|
||||||
funding_bull_threshold_annualized: "0.20"
|
funding_bull_threshold_annualized: "0.10"
|
||||||
funding_bear_threshold_annualized: "-0.20"
|
funding_bear_threshold_annualized: "-0.10"
|
||||||
iron_condor_dvol_min: "55"
|
iron_condor_dvol_min: "55"
|
||||||
iron_condor_adx_max: "20"
|
iron_condor_adx_max: "20"
|
||||||
iron_condor_trend_neutral_band_pct: "0.05"
|
iron_condor_trend_neutral_band_pct: "0.05"
|
||||||
@@ -43,11 +46,17 @@ entry:
|
|||||||
# per vendere credit spread. Soglia conservativa, da rifinire dopo
|
# per vendere credit spread. Soglia conservativa, da rifinire dopo
|
||||||
# paper trading.
|
# paper trading.
|
||||||
dealer_gamma_min: "0"
|
dealer_gamma_min: "0"
|
||||||
dealer_gamma_filter_enabled: true
|
dealer_gamma_filter_enabled: false
|
||||||
liquidation_filter_enabled: true
|
liquidation_filter_enabled: true
|
||||||
# IV richness gate (§2.9). Disabilitato di default.
|
# IV richness gate (§2.9). Disabilitato di default.
|
||||||
iv_minus_rv_min: "0"
|
iv_minus_rv_min: "0"
|
||||||
iv_minus_rv_filter_enabled: false
|
iv_minus_rv_filter_enabled: true
|
||||||
|
|
||||||
|
# IV richness gate adattivo — soglia P25 rolling su 60 giorni
|
||||||
|
iv_minus_rv_adaptive_enabled: true
|
||||||
|
iv_minus_rv_percentile: "0.25"
|
||||||
|
iv_minus_rv_window_target_days: 60
|
||||||
|
iv_minus_rv_window_min_days: 30
|
||||||
|
|
||||||
|
|
||||||
structure:
|
structure:
|
||||||
@@ -107,7 +116,7 @@ exit:
|
|||||||
# atomica. Pipeline runtime non ancora attiva (hook futuro).
|
# atomica. Pipeline runtime non ancora attiva (hook futuro).
|
||||||
profit_take_partial_levels: []
|
profit_take_partial_levels: []
|
||||||
|
|
||||||
monitor_cron: "0 2,14 * * *"
|
monitor_cron: "0 * * * *"
|
||||||
user_confirmation_timeout_min: 30
|
user_confirmation_timeout_min: 30
|
||||||
|
|
||||||
escalate_on_timeout:
|
escalate_on_timeout:
|
||||||
|
|||||||
Reference in New Issue
Block a user