feat: split audit logs into live/dryrun subdirs, add catlog --dry-run, list all kline intervals
- Write live trades to logs/live/ and dry-run trades to logs/dryrun/ - Add -d/--dry-run flag to catlog to read dry-run audit logs - List all 16 Binance kline interval options in --help and docs Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -22,30 +22,38 @@ def _resolve_audit_dir(paths: RuntimePaths) -> Path:
|
||||
return _audit_dir_cache[key]
|
||||
|
||||
|
||||
def _audit_path(paths: RuntimePaths | None = None) -> Path:
|
||||
def _audit_path(paths: RuntimePaths | None = None, *, dry_run: bool = False) -> Path:
|
||||
paths = ensure_runtime_dirs(paths or get_runtime_paths())
|
||||
logs_dir = _resolve_audit_dir(paths)
|
||||
logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
return logs_dir / f"audit_{datetime.now(timezone.utc).strftime('%Y%m%d')}.jsonl"
|
||||
subdir = logs_dir / ("dryrun" if dry_run else "live")
|
||||
subdir.mkdir(parents=True, exist_ok=True)
|
||||
return subdir / f"audit_{datetime.now(timezone.utc).strftime('%Y%m%d')}.jsonl"
|
||||
|
||||
|
||||
def audit_event(event: str, payload: dict[str, Any], paths: RuntimePaths | None = None) -> dict[str, Any]:
|
||||
def audit_event(
|
||||
event: str, payload: dict[str, Any], paths: RuntimePaths | None = None, *, dry_run: bool = False
|
||||
) -> dict[str, Any]:
|
||||
entry = {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"event": event,
|
||||
**payload,
|
||||
}
|
||||
with _audit_path(paths).open("a", encoding="utf-8") as handle:
|
||||
with _audit_path(paths, dry_run=dry_run).open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(entry, ensure_ascii=False, default=json_default) + "\n")
|
||||
return entry
|
||||
|
||||
|
||||
def read_audit_log(paths: RuntimePaths | None = None, limit: int = 10, offset: int = 0) -> list[dict[str, Any]]:
|
||||
def read_audit_log(
|
||||
paths: RuntimePaths | None = None, limit: int = 10, offset: int = 0, *, dry_run: bool = False
|
||||
) -> list[dict[str, Any]]:
|
||||
paths = ensure_runtime_dirs(paths or get_runtime_paths())
|
||||
logs_dir = _resolve_audit_dir(paths)
|
||||
if not logs_dir.exists():
|
||||
return []
|
||||
audit_files = sorted(logs_dir.glob("audit_*.jsonl"), reverse=True)
|
||||
subdir = logs_dir / ("dryrun" if dry_run else "live")
|
||||
if not subdir.exists():
|
||||
return []
|
||||
audit_files = sorted(subdir.glob("audit_*.jsonl"), reverse=True)
|
||||
needed = offset + limit
|
||||
chunks: list[list[dict[str, Any]]] = []
|
||||
total = 0
|
||||
|
||||
@@ -191,7 +191,7 @@ JSON Output:
|
||||
}
|
||||
|
||||
Fields:
|
||||
interval – candle interval (enum: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w)
|
||||
interval – candle interval (enum: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M)
|
||||
limit – number of candles requested (int)
|
||||
symbol – normalized trading pair
|
||||
open_time – candle open timestamp in ms (int)
|
||||
@@ -212,7 +212,7 @@ JSON Output:
|
||||
}
|
||||
|
||||
Fields:
|
||||
interval – candle interval (enum: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w)
|
||||
interval – candle interval (enum: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M)
|
||||
limit – number of candles requested (int)
|
||||
symbol – normalized trading pair
|
||||
open_time – candle open timestamp in ms (int)
|
||||
@@ -730,7 +730,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
description="Fetch OHLCV candlestick data for one or more symbols.",
|
||||
)
|
||||
klines_parser.add_argument("symbols", nargs="+", metavar="SYM", help="Symbols to query")
|
||||
klines_parser.add_argument("-i", "--interval", default="1h", help="Kline interval (default: 1h)")
|
||||
klines_parser.add_argument(
|
||||
"-i", "--interval", default="1h",
|
||||
help="Kline interval: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M (default: 1h)",
|
||||
)
|
||||
klines_parser.add_argument("-l", "--limit", type=int, default=100, help="Number of candles (default: 100)")
|
||||
_add_global_flags(klines_parser)
|
||||
|
||||
@@ -784,6 +787,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
catlog_parser.add_argument(
|
||||
"-o", "--offset", type=int, default=0, help="Skip the most recent N entries (default: 0)"
|
||||
)
|
||||
catlog_parser.add_argument("-d", "--dry-run", action="store_true", help="Read dry-run audit logs")
|
||||
_add_global_flags(catlog_parser)
|
||||
|
||||
completion_parser = subparsers.add_parser(
|
||||
@@ -1057,9 +1061,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
if args.command == "catlog":
|
||||
with with_spinner("Reading audit logs...", enabled=not args.agent):
|
||||
entries = read_audit_log(limit=args.limit, offset=args.offset)
|
||||
entries = read_audit_log(limit=args.limit, offset=args.offset, dry_run=args.dry_run)
|
||||
print_output(
|
||||
{"entries": entries, "limit": args.limit, "offset": args.offset, "total": len(entries)},
|
||||
{"entries": entries, "limit": args.limit, "offset": args.offset, "total": len(entries), "dry_run": args.dry_run},
|
||||
agent=args.agent,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -112,7 +112,7 @@ def execute_spot_trade(
|
||||
dry_run=is_dry_run,
|
||||
)
|
||||
|
||||
audit_event("trade_submitted", _trade_log_payload(intent, payload, status="submitted"))
|
||||
audit_event("trade_submitted", _trade_log_payload(intent, payload, status="submitted"), dry_run=intent.dry_run)
|
||||
if is_dry_run:
|
||||
response = {"dry_run": True, "status": "DRY_RUN", "request": payload}
|
||||
result = asdict(
|
||||
@@ -128,14 +128,14 @@ def execute_spot_trade(
|
||||
)
|
||||
)
|
||||
audit_event(
|
||||
"trade_filled", {**_trade_log_payload(intent, payload, status="DRY_RUN"), "response_payload": response}
|
||||
"trade_filled", {**_trade_log_payload(intent, payload, status="DRY_RUN"), "response_payload": response}, dry_run=intent.dry_run
|
||||
)
|
||||
return {"trade": result}
|
||||
|
||||
try:
|
||||
response = spot_client.new_order(**payload)
|
||||
except Exception as exc:
|
||||
audit_event("trade_failed", _trade_log_payload(intent, payload, status="failed", error=str(exc)))
|
||||
audit_event("trade_failed", _trade_log_payload(intent, payload, status="failed", error=str(exc)), dry_run=intent.dry_run)
|
||||
raise RuntimeError(f"Spot order failed: {exc}") from exc
|
||||
|
||||
result = asdict(
|
||||
@@ -151,6 +151,7 @@ def execute_spot_trade(
|
||||
)
|
||||
)
|
||||
audit_event(
|
||||
"trade_filled", {**_trade_log_payload(intent, payload, status=result["status"]), "response_payload": response}
|
||||
"trade_filled", {**_trade_log_payload(intent, payload, status=result["status"]), "response_payload": response},
|
||||
dry_run=intent.dry_run,
|
||||
)
|
||||
return {"trade": result}
|
||||
|
||||
Reference in New Issue
Block a user