36 lines
16 KiB
Plaintext
36 lines
16 KiB
Plaintext
CHAT_OUTPUT_BEGIN
|
|
COMMAND_ID=SIGNALBOT-260921-ORDER-NORMALIZATION-TEST-RCA-052FIXRCA1
|
|
STATUS=OK
|
|
RC=0
|
|
HOST=pve01
|
|
MODE=read-only
|
|
COMPONENT=signalbot-order-normalization-test-rca
|
|
REFERENCE_REGISTER_CHECK=OK
|
|
REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66
|
|
ERROR_REGISTER_CHECK=OK
|
|
ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0
|
|
COMMAND_SHA256=bb6b4fbaa3c7a735965adc6a9e217dc7242ed3097f157c63cb7d11fcba77a0fa
|
|
DUPLICATE_FAILED_COMMAND_BLOCKED=false
|
|
EXECUTION_STARTED=true
|
|
CHANGE_DECLARED=false
|
|
RESULT_CONTRACT_VALID=true
|
|
RESULT_CONTRACT_STATUS=NOT_APPLICABLE
|
|
RESULT_CONTRACT_ERROR=NONE
|
|
COMMAND_RC=0
|
|
CHANGES_MADE=false
|
|
ROLLBACK_STARTED=false
|
|
ROLLBACK_RESTORED=null
|
|
MUTATION_OUTCOME=NO_MUTATION
|
|
SANITIZED=yes
|
|
SECRETS_INCLUDED=no
|
|
PRIVATE_ADDRESSES_INCLUDED=no
|
|
RAW_EVIDENCE_SHA256=6f146b4f6887dfacff94c79715d1e60e5cdb633c76d4b25a3f28feec05a11eab
|
|
SANITIZED_OUTPUT_SHA256=6f146b4f6887dfacff94c79715d1e60e5cdb633c76d4b25a3f28feec05a11eab
|
|
OUTPUT_BEGIN
|
|
SIGNALBOT052FIXRCA1_RESULT={"branch":"signalbot-260917-reconnect-reconciliation","clean":true,"diff_empty":true,"head":"749073ae0f894debc06fb0ffbf7a84abf9da531f","package_identity":"9dd3a15343a59007aa1c0484f9a52b5987c67478687f1a5d3e3c91c7b95a9073","restored_exact":true,"source_lines_146_215":"146:\n147: def fetch_trade_window(\n148: self,\n149: symbol: str,\n150: *,\n151: start_mts: datetime,\n152: end_mts: datetime,\n153: ) -> BitfinexTradeHistoryBatch:\n154: if not isinstance(symbol, str) or not symbol.startswith(\"t\") or len(symbol) < 2:\n155: raise DemonovError(\"Bitfinex trade history symbol invalid\")\n156: start_mts = _utc(start_mts, \"start_mts\")\n157: end_mts = _utc(end_mts, \"end_mts\")\n158: if end_mts < start_mts:\n159: raise DemonovError(\"Bitfinex trade history interval invalid\")\n160: start_ms = int(start_mts.timestamp() * 1000)\n161: end_ms = int(end_mts.timestamp() * 1000)\n162: query = urlencode({\"start\": start_ms, \"end\": end_ms, \"sort\": 1, \"limit\": BITFINEX_PUBLIC_HISTORY_LIMIT})\n163: url = f\"{BITFINEX_PUBLIC_TRADES_BASE}/{quote(symbol, safe='')}/hist?{query}\"\n164: try:\n165: payload = self._fetch_json(\n166: url,\n167: timeout=self.timeout,\n168: max_bytes=self.max_bytes,\n169: )\n170: except DemonovError:\n171: raise\n172: except Exception as exc:\n173: raise DemonovError(\n174: \"Bitfinex public trade history request failed\"\n175: ) from exc\n176: if not isinstance(payload, list):\n177: raise DemonovError(\"Bitfinex public trade history payload invalid\")\n178:\n179: normalized: list[BitfinexCanonicalTradeV1] = []\n180: raw_rows: list[list[object]] = []\n181: seen: dict[str, str] = {}\n182: order_keys: list[tuple[datetime, int]] = []\n183: for raw in payload:\n184: if not isinstance(raw, list):\n185: raise DemonovError(\"Bitfinex REST trade row malformed\")\n186: trade = normalize_bitfinex_rest_trade(raw)\n187: raw_rows.append(list(raw[:4]))\n188: prior = seen.get(trade.trade_identity)\n189: if prior is not None:\n190: if prior != trade.semantic_hash:\n191: raise DemonovError(\"Bitfinex REST trade semantic conflict\")\n192: continue\n193: seen[trade.trade_identity] = trade.semantic_hash\n194: normalized.append(trade)\n195: order_keys.append((trade.mts, trade.trade_id))\n196: if order_keys != sorted(order_keys):\n197: raise DemonovError(\"Bitfinex public trade history not ascending\")\n198:\n199: evidence_sha256 = canonical_hash(\n200: {\n201: \"schema\": \"CR0116_BITFINEX_RECONCILIATION_REST_EVIDENCE_V1\",\n202: \"symbol\": symbol,\n203: \"start_mts\": start_mts,\n204: \"end_mts\": end_mts,\n205: \"raw_row_count\": len(payload),\n206: \"rows\": raw_rows,\n207: }\n208: )\n209: return BitfinexTradeHistoryBatch(\n210: symbol=symbol,\n211: start_mts=start_mts,\n212: end_mts=end_mts,\n213: raw_row_count=len(payload),\n214: trades=tuple(normalized),\n215: evidence_sha256=evidence_sha256,","source_sha256":"023ce0a041b50b866a48d9f752bd80479a34789d82f73c81d18246ed8871ef8e","test_hits":["126: journal.close()\n127:\n128:\n129:def test_public_history_fetch_is_ascending_inclusive_and_deduplicates_exact_rows():\n130: a, b, c = _trade(2001, 1), _trade(2002, 2), _trade(2003, 3)\n131: calls = []\n132: def fetch(url, *, timeout, max_bytes):\n133: calls.append((url, timeout, max_bytes))","133: calls.append((url, timeout, max_bytes))\n134: return [_rest_row(a), _rest_row(b), _rest_row(b), _rest_row(c)]\n135: client = BitfinexPublicTradeHistoryClient(fetch_json=fetch)\n136: batch = client.fetch_trade_window(\"tBTCUSD\", start_mts=a.mts, end_mts=c.mts)\n137: assert [t.trade_id for t in batch.trades] == [2001, 2002, 2003]\n138: assert batch.raw_row_count == 4\n139: assert \"sort=1\" in calls[0][0] and \"limit=10000\" in calls[0][0]\n140: assert f\"start={int(a.mts.timestamp()*1000)}\" in calls[0][0]","145: a, b = _trade(2051, 1), _trade(2052, 2)\n146: left = BitfinexPublicTradeHistoryClient(\n147: fetch_json=lambda *args, **kwargs: [_rest_row(a), _rest_row(a), _rest_row(b)]\n148: ).fetch_trade_window(\"tBTCUSD\", start_mts=a.mts, end_mts=b.mts)\n149: right = BitfinexPublicTradeHistoryClient(\n150: fetch_json=lambda *args, **kwargs: [_rest_row(a), _rest_row(b), _rest_row(b)]\n151: ).fetch_trade_window(\"tBTCUSD\", start_mts=a.mts, end_mts=b.mts)\n152: assert [t.trade_id for t in left.trades] == [2051, 2052]","148: ).fetch_trade_window(\"tBTCUSD\", start_mts=a.mts, end_mts=b.mts)\n149: right = BitfinexPublicTradeHistoryClient(\n150: fetch_json=lambda *args, **kwargs: [_rest_row(a), _rest_row(b), _rest_row(b)]\n151: ).fetch_trade_window(\"tBTCUSD\", start_mts=a.mts, end_mts=b.mts)\n152: assert [t.trade_id for t in left.trades] == [2051, 2052]\n153: assert [t.trade_id for t in right.trades] == [2051, 2052]\n154: assert left.raw_row_count == right.raw_row_count == 3\n155: assert left.evidence_sha256 != right.evidence_sha256","164: ]\n165: for client in bad_clients:\n166: with pytest.raises(DemonovError):\n167: client.fetch_trade_window(\"tBTCUSD\", start_mts=a.mts, end_mts=b.mts)\n168:\n169:\n170:def test_successful_reconciliation_recovers_middle_trade_and_is_idempotent(tmp_path):\n171: journal, window, previous, next_trade = _journal_with_window(tmp_path)","175: calls.append(url)\n176: return [_rest_row(previous), _rest_row(middle), _rest_row(next_trade)]\n177: client = BitfinexPublicTradeHistoryClient(fetch_json=fetch)\n178: resolved = client.reconcile_window(\n179: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n180: )\n181: repeated = client.reconcile_window(\n182: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=21)","178: resolved = client.reconcile_window(\n179: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n180: )\n181: repeated = client.reconcile_window(\n182: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=21)\n183: )\n184: assert resolved.status is ReconciliationStatus.RECONCILED\n185: assert resolved.recovered_row_count == 1","189: journal.close()\n190:\n191:\n192:def test_http_or_validation_failure_marks_window_unresolved(tmp_path):\n193: journal, window, _, _ = _journal_with_window(tmp_path)\n194: client = BitfinexPublicTradeHistoryClient(\n195: fetch_json=lambda *a, **k: (_ for _ in ()).throw(TimeoutError(\"simulated\"))\n196: )","194: client = BitfinexPublicTradeHistoryClient(\n195: fetch_json=lambda *a, **k: (_ for _ in ()).throw(TimeoutError(\"simulated\"))\n196: )\n197: resolved = client.reconcile_window(\n198: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n199: )\n200: assert resolved.status is ReconciliationStatus.UNRESOLVED\n201: assert resolved.failure_reason == \"REST_FETCH_OR_VALIDATION_FAILED\"","198: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n199: )\n200: assert resolved.status is ReconciliationStatus.UNRESOLVED\n201: assert resolved.failure_reason == \"REST_FETCH_OR_VALIDATION_FAILED\"\n202: journal.close()\n203:\n204:\n205:def test_exact_row_limit_marks_window_unresolved(tmp_path):","212: rows[-1] = _rest_row(next_trade)\n213: rows.sort(key=lambda r: (r[1], r[0]))\n214: client = BitfinexPublicTradeHistoryClient(fetch_json=lambda *a, **k: rows)\n215: resolved = client.reconcile_window(\n216: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n217: )\n218: assert resolved.status is ReconciliationStatus.UNRESOLVED\n219: assert resolved.failure_reason == \"REST_ROW_LIMIT_REACHED\"","226: client = BitfinexPublicTradeHistoryClient(\n227: fetch_json=lambda *a, **k: [_rest_row(previous), _rest_row(middle)]\n228: )\n229: resolved = client.reconcile_window(\n230: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n231: )\n232: assert resolved.status is ReconciliationStatus.UNRESOLVED\n233: assert resolved.failure_reason == \"NEXT_BOUNDARY_MISSING\"","243: client = BitfinexPublicTradeHistoryClient(\n244: fetch_json=lambda *a, **k: [conflicting, _rest_row(next_trade)]\n245: )\n246: resolved = client.reconcile_window(\n247: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n248: )\n249: after = journal._db.execute(\n250: \"SELECT COUNT(*) AS n FROM trade_revisions WHERE trade_identity=?\", (previous.trade_identity,)"],"test_lines_150_280":"150: fetch_json=lambda *args, **kwargs: [_rest_row(a), _rest_row(b), _rest_row(b)]\n151: ).fetch_trade_window(\"tBTCUSD\", start_mts=a.mts, end_mts=b.mts)\n152: assert [t.trade_id for t in left.trades] == [2051, 2052]\n153: assert [t.trade_id for t in right.trades] == [2051, 2052]\n154: assert left.raw_row_count == right.raw_row_count == 3\n155: assert left.evidence_sha256 != right.evidence_sha256\n156:\n157:\n158:def test_public_history_rejects_malformed_unordered_and_conflicting_duplicate_rows():\n159: a, b = _trade(2101, 1), _trade(2102, 2)\n160: bad_clients = [\n161: BitfinexPublicTradeHistoryClient(fetch_json=lambda *a, **k: [[1, 2]]),\n162: BitfinexPublicTradeHistoryClient(fetch_json=lambda *a, **k: [_rest_row(b), _rest_row(a)]),\n163: BitfinexPublicTradeHistoryClient(fetch_json=lambda *a, **k: [_rest_row(a), [a.trade_id, int(a.mts.timestamp()*1000), \"0.5\", \"101\"]]),\n164: ]\n165: for client in bad_clients:\n166: with pytest.raises(DemonovError):\n167: client.fetch_trade_window(\"tBTCUSD\", start_mts=a.mts, end_mts=b.mts)\n168:\n169:\n170:def test_successful_reconciliation_recovers_middle_trade_and_is_idempotent(tmp_path):\n171: journal, window, previous, next_trade = _journal_with_window(tmp_path)\n172: middle = _trade(1002, 7)\n173: calls = []\n174: def fetch(url, *, timeout, max_bytes):\n175: calls.append(url)\n176: return [_rest_row(previous), _rest_row(middle), _rest_row(next_trade)]\n177: client = BitfinexPublicTradeHistoryClient(fetch_json=fetch)\n178: resolved = client.reconcile_window(\n179: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n180: )\n181: repeated = client.reconcile_window(\n182: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=21)\n183: )\n184: assert resolved.status is ReconciliationStatus.RECONCILED\n185: assert resolved.recovered_row_count == 1\n186: assert journal.has_trade_identity(middle.trade_identity)\n187: assert repeated == resolved\n188: assert len(calls) == 1\n189: journal.close()\n190:\n191:\n192:def test_http_or_validation_failure_marks_window_unresolved(tmp_path):\n193: journal, window, _, _ = _journal_with_window(tmp_path)\n194: client = BitfinexPublicTradeHistoryClient(\n195: fetch_json=lambda *a, **k: (_ for _ in ()).throw(TimeoutError(\"simulated\"))\n196: )\n197: resolved = client.reconcile_window(\n198: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n199: )\n200: assert resolved.status is ReconciliationStatus.UNRESOLVED\n201: assert resolved.failure_reason == \"REST_FETCH_OR_VALIDATION_FAILED\"\n202: journal.close()\n203:\n204:\n205:def test_exact_row_limit_marks_window_unresolved(tmp_path):\n206: journal, window, previous, next_trade = _journal_with_window(tmp_path)\n207: rows = []\n208: for i in range(BITFINEX_PUBLIC_HISTORY_LIMIT):\n209: trade_id = 300000 + i\n210: rows.append([trade_id, int((previous.mts + timedelta(milliseconds=i)).timestamp()*1000), \"0.5\", \"100\"])\n211: rows[0] = _rest_row(previous)\n212: rows[-1] = _rest_row(next_trade)\n213: rows.sort(key=lambda r: (r[1], r[0]))\n214: client = BitfinexPublicTradeHistoryClient(fetch_json=lambda *a, **k: rows)\n215: resolved = client.reconcile_window(\n216: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n217: )\n218: assert resolved.status is ReconciliationStatus.UNRESOLVED\n219: assert resolved.failure_reason == \"REST_ROW_LIMIT_REACHED\"\n220: journal.close()\n221:\n222:\n223:def test_missing_boundary_marks_window_unresolved(tmp_path):\n224: journal, window, previous, _ = _journal_with_window(tmp_path, persist_next=False)\n225: middle = _trade(1002, 7)\n226: client = BitfinexPublicTradeHistoryClient(\n227: fetch_json=lambda *a, **k: [_rest_row(previous), _rest_row(middle)]\n228: )\n229: resolved = client.reconcile_window(\n230: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n231: )\n232: assert resolved.status is ReconciliationStatus.UNRESOLVED\n233: assert resolved.failure_reason == \"NEXT_BOUNDARY_MISSING\"\n234: journal.close()\n235:\n236:\n237:def test_semantic_conflict_marks_window_unresolved_without_new_revision(tmp_path):\n238: journal, window, previous, next_trade = _journal_with_window(tmp_path)\n239: conflicting = [previous.trade_id, int(previous.mts.timestamp()*1000), \"0.5\", \"101\"]\n240: before = journal._db.execute(\n241: \"SELECT COUNT(*) AS n FROM trade_revisions WHERE trade_identity=?\", (previous.trade_identity,)\n242: ).fetchone()[\"n\"]\n243: client = BitfinexPublicTradeHistoryClient(\n244: fetch_json=lambda *a, **k: [conflicting, _rest_row(next_trade)]\n245: )\n246: resolved = client.reconcile_window(\n247: journal, window.window_id, symbol=\"tBTCUSD\", resolved_at=T0 + timedelta(seconds=20)\n248: )\n249: after = journal._db.execute(\n250: \"SELECT COUNT(*) AS n FROM trade_revisions WHERE trade_identity=?\", (previous.trade_identity,)\n251: ).fetchone()[\"n\"]\n252: assert resolved.status is ReconciliationStatus.UNRESOLVED\n253: assert resolved.failure_reason == \"REST_SEMANTIC_CONFLICT\"\n254: assert after == before\n255: journal.close()","test_sha256":"bebdd9c2875d2c63a0ac27d89eefa7f1c36c83c067aa8b472c0616ac470dd2a2"}
|
|
TASK_RESULT=PASS_SIGNALBOT_052FIXRCA1_ROLLBACK_AND_TEST_SEMANTICS_CAPTURED
|
|
HOMELAB_RESULT_CONTRACT={"version":1,"command_id":"SIGNALBOT-260921-ORDER-NORMALIZATION-TEST-RCA-052FIXRCA1","status":"OK","changes_made":false,"rollback_started":false,"rollback_restored":null}
|
|
|
|
OUTPUT_END
|
|
CHAT_OUTPUT_END
|