|
21 | 21 | from application.cycle_result import coerce_strategy_cycle_result |
22 | 22 | from application.runtime_broker_adapters import ( |
23 | 23 | IBKRGatewayUnavailableError, |
| 24 | + IBKRTradingPermissionError, |
24 | 25 | build_runtime_broker_adapters, |
25 | 26 | ) |
26 | 27 | from application.runtime_composer import build_runtime_composer |
|
74 | 75 | max_workers_from_env, |
75 | 76 | timeout_seconds_from_env, |
76 | 77 | ) |
77 | | -from application.ibkr_portfolio import fetch_portfolio_snapshot |
| 78 | +from application.ibkr_portfolio import ( |
| 79 | + IBKRPortfolioSnapshotUnavailableError, |
| 80 | + fetch_portfolio_snapshot, |
| 81 | +) |
78 | 82 | from application.execution_service import ( |
79 | 83 | check_order_submitted as application_check_order_submitted, |
80 | 84 | execute_rebalance as application_execute_rebalance, |
@@ -1292,28 +1296,53 @@ def _handle_request( |
1292 | 1296 | ) |
1293 | 1297 | if notification_delivery_log: |
1294 | 1298 | report_summary["notification_delivery_log"] = notification_delivery_log |
| 1299 | + execution_status = str(report_summary.get("execution_status") or "").strip().lower() |
| 1300 | + execution_blocked = execution_status in {"blocked", "error", "failed", "failure"} |
| 1301 | + if execution_blocked: |
| 1302 | + append_runtime_report_error( |
| 1303 | + report, |
| 1304 | + stage="strategy_execution", |
| 1305 | + message=( |
| 1306 | + f"Strategy execution status is {execution_status}; " |
| 1307 | + f"reason={report_summary.get('no_op_reason') or 'unspecified'}" |
| 1308 | + ), |
| 1309 | + error_type="StrategyExecutionBlocked", |
| 1310 | + failure_category="strategy_execution_blocked", |
| 1311 | + ) |
| 1312 | + report_diagnostics = { |
| 1313 | + "result": cycle_result.result, |
| 1314 | + "price_source_mode": execution_summary.get("price_source_mode") or reconciliation_record.get("price_source_mode"), |
| 1315 | + "snapshot_price_fallback_symbols": execution_summary.get("snapshot_price_fallback_symbols") |
| 1316 | + or reconciliation_record.get("snapshot_price_fallback_symbols") |
| 1317 | + or [], |
| 1318 | + **({"signal_snapshot": signal_snapshot} if has_signal_snapshot else {}), |
| 1319 | + } |
| 1320 | + if execution_blocked: |
| 1321 | + report_diagnostics["failure_category"] = "strategy_execution_blocked" |
1295 | 1322 | finalize_runtime_report( |
1296 | 1323 | report, |
1297 | | - status="ok", |
| 1324 | + status="error" if execution_blocked else "ok", |
1298 | 1325 | summary=report_summary, |
1299 | | - diagnostics={ |
1300 | | - "result": cycle_result.result, |
1301 | | - "price_source_mode": execution_summary.get("price_source_mode") or reconciliation_record.get("price_source_mode"), |
1302 | | - "snapshot_price_fallback_symbols": execution_summary.get("snapshot_price_fallback_symbols") |
1303 | | - or reconciliation_record.get("snapshot_price_fallback_symbols") |
1304 | | - or [], |
1305 | | - **({"signal_snapshot": signal_snapshot} if has_signal_snapshot else {}), |
1306 | | - }, |
| 1326 | + diagnostics=report_diagnostics, |
1307 | 1327 | artifacts={ |
1308 | 1328 | "reconciliation_record_path": cycle_result.reconciliation_record_path, |
1309 | 1329 | }, |
1310 | 1330 | ) |
1311 | 1331 | log_runtime_event( |
1312 | 1332 | log_context, |
1313 | | - "strategy_cycle_completed", |
1314 | | - message="Strategy dry-run completed" if dry_run_only_override else "Strategy execution completed", |
| 1333 | + "strategy_cycle_blocked" if execution_blocked else "strategy_cycle_completed", |
| 1334 | + message=( |
| 1335 | + "Strategy execution blocked" |
| 1336 | + if execution_blocked |
| 1337 | + else "Strategy dry-run completed" |
| 1338 | + if dry_run_only_override |
| 1339 | + else "Strategy execution completed" |
| 1340 | + ), |
| 1341 | + severity="ERROR" if execution_blocked else "INFO", |
1315 | 1342 | execution_window=execution_window, |
1316 | 1343 | result=cycle_result.result, |
| 1344 | + execution_status=report_summary.get("execution_status"), |
| 1345 | + no_op_reason=report_summary.get("no_op_reason"), |
1317 | 1346 | ) |
1318 | 1347 | return (response_body if dry_run_only_override else cycle_result.result), 200 |
1319 | 1348 | except RuntimeDeadlineExceeded as exc: |
@@ -1362,6 +1391,40 @@ def _handle_request( |
1362 | 1391 | exc=exc, |
1363 | 1392 | ) |
1364 | 1393 | return "Error", 503 |
| 1394 | + except (IBKRTradingPermissionError, IBKRPortfolioSnapshotUnavailableError) as exc: |
| 1395 | + failure_category = ( |
| 1396 | + "ibkr_trading_permission" |
| 1397 | + if isinstance(exc, IBKRTradingPermissionError) |
| 1398 | + else "ibkr_portfolio_snapshot" |
| 1399 | + ) |
| 1400 | + append_runtime_report_error( |
| 1401 | + report, |
| 1402 | + stage=failure_category, |
| 1403 | + message=str(exc), |
| 1404 | + error_type=type(exc).__name__, |
| 1405 | + failure_category=failure_category, |
| 1406 | + ) |
| 1407 | + finalize_runtime_report( |
| 1408 | + report, |
| 1409 | + status="error", |
| 1410 | + diagnostics={"failure_category": failure_category}, |
| 1411 | + ) |
| 1412 | + log_runtime_event( |
| 1413 | + log_context, |
| 1414 | + f"{failure_category}_failed", |
| 1415 | + message="IBKR execution readiness validation failed", |
| 1416 | + severity="ERROR", |
| 1417 | + error_type=type(exc).__name__, |
| 1418 | + error_message=str(exc), |
| 1419 | + failure_category=failure_category, |
| 1420 | + ) |
| 1421 | + error_msg = f"🚨 【IBKR 执行条件异常】\n{str(exc)}" |
| 1422 | + _publish_runtime_failure_notification( |
| 1423 | + detailed_text=error_msg, |
| 1424 | + compact_text=error_msg, |
| 1425 | + exc=exc, |
| 1426 | + ) |
| 1427 | + return "Error", 503 |
1365 | 1428 | except Exception as exc: |
1366 | 1429 | append_runtime_report_error( |
1367 | 1430 | report, |
|
0 commit comments