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
