CH3-003 · ניהול מצב פקודת הביצוע מול הברוקר
גרסה 1.0 · טיוטא 8.8.2026 · P1 · Draft for Architect Review
הכרעת Architect: Locked & Approved — Version 1.0 — ננעל ב-8.8.2026.
גרסה 1.0 ננעלה ואושרה לביצוע (8.8.2026). כל הערות הסקירה מסבבי 0.2/0.21/0.22 נסגרו ללא סתירות פתוחות: הפרדת ACK (SUBMIT_ACCEPTED/ORDER_ACTIVE) עם שני זמנים, מירוץ Fill/ביטול (מילוי חלקי ב-CANCEL_PENDING אינו מבטל את הביטול), מסלולי ביטול מתוך Reconciliation, שחרור יתרות בכל Fill, אטומיות קליטת Fill בארבעה שלבים, Corrective Trade / ExecutionAdjustment לאירוע מאוחר סותר טרמינלי (ה-Trade המקורי נשאר CANCELLED/EXPIRED — CH3-002 אינו נשבר), Unique Constraint חד-משמעי, ו-cancel_timeout_sec=20 קנוני. השלב הבא: מימוש לפי סעיף 19 + הגשת Ch3ExecutionReport003. הפעלת Live Broker — החלטת Live-Go נפרדת; המימוש והבדיקות ב-Paper Adapter בלבד עד אז.
| הערה | נושא | תיקון |
|---|---|---|
| #1 (0.21) | כפילות במשמעות ACK | SUBMIT_ACCEPTED (SUBMITTING→SENT: התקבל broker_order_id) נפרד מ-ORDER_ACTIVE (SENT→WORKING: אושרה כפעילה). נוספו שני זמנים: submit_started_at ו-broker_accepted_at. |
| #2 (0.21) | Fill בזמן CANCEL_PENDING | מילוי חלקי אינו מבטל את הביטול — נשארים ב-CANCEL_PENDING ומעדכנים כמות. מילוי מלא → CONFIRMED. ביטול מאושר: filled=0 → CANCELLED; filled>0 → CONFIRMED חלקי. דחייה/Timeout בביטול → queryStatus. נוספו cancel_request_id ו-cancel_timeout_sec. |
| #3 (0.21) | מסלולי ביטול מתוך Reconciliation | נוספו RECONCILIATION_REQUIRED → CANCEL_PENDING (הביטול עדיין ממתין) ו-→ CANCELLED (הביטול הושלם ללא מילוי). |
| #4 (0.21) | שחרור יתרות בכל Fill | היתרות השמורות מופחתות בכל Fill (קנייה: reserved_cash_usd; מכירה: reserved_shares). בסיום משוחררת רק היתרה שנותרה — מונע נעילת כסף/מניות ב-GTC חלקי. |
| #5 (0.21) | אטומיות קליטת Fill | ארבע פעולות באותה יחידה אטומית/בר-שחזור: כתיבת Inbox, עדכון filled_quantity+מכונה, portfolioLedger, Outbox. בדיקת קריסה בין כל שני שלבים — אין Fill ביומן שלא בתיק ולהפך. |
| #6 (0.21) | אירוע מאוחר סותר טרמינלי | התראה בלבד אינה מספיקה: פתיחת Reconciliation Case, חסימת פעולה נוספת בפקודה, ולאחר אימות — תיקון חד-פעני ב-portfolioLedger וב-Audit והתאמת Trade.status מול החשיפה. אין חובה לפתוח מחדש אוטומטית את הטרמינלי. |
| #7 (0.21) | Unique Constraint חד-משמעי | Fill = broker_type + broker_account_id + fill_id; אירוע אחר = broker_type + broker_account_id + broker_event_id. הניסוח הקודם (broker_order_id + fill_id / event_seq) אינו חד-משמעי למימוש. |
| #8 (0.22) | אירוע מאוחר סותר טרמינלי — שבירת CH3-002 | אין לשנות את ה-Trade המקורי (אינו פותח מחדש את execution_state ואינו מתאם Trade.status לחשיפה, כי CH3-002 מאפשר executed רק כש-execution_state=CONFIRMED). במקום זאת — נוצר Corrective Trade / ExecutionAdjustment מקושר (corrective_for_trade_id + original_terminal_state) הרושם את הביצוע המאוחר ומעדכן את התיק וה-Audit. ה-Trade המקורי נשאר CANCELLED/EXPIRED וההיסטוריה נשמרת. |
| #9 (0.22) | חסר ערך ברירת מחדל ל-cancel_timeout_sec | נקבע cancel_timeout_sec=20 קנוני, בדומה ל-response_timeout_sec, ניתן לשינוי ב-SystemSettings. |
CH3-002 נעל את החוזה העסקי: approved→executed מחייב execution_state=CONFIRMED בלבד — אך מצב פקודת הביצוע עצמו אינו מנוהל. כיום המערכת כותבת CONFIRMED ישירות בתוך tradeExecution ללא שליחה אמיתית, ללא מעקב אירועי ברוקר, ללא טיפול במילוי חלקי, Timeout, Reconciliation, ביטול או מירוץ בין Fill לביטול. כל חיבור עתידי לברוקר חי ידרוש בנייה מחדש של מנגנון הביצוע.
חוזה ביצוע היברידי ואחיד, התומך ב-Paper וב-Live דרך ממשק Broker Adapter אחיד. המצב הנוכחי מפעיל Paper Broker Adapter בלבד; הפעלת Live תדרוש Live-Go נפרד ואישור. כל שינוי execution_state עובר דרך מכונת מצבים ייעודית עם idempotency, Inbox (BrokerEventJournal) ו-Outbox, Audit ומניעת שליחה כפולה — גם ב-Paper אסור לשנות execution_state ישירות במסד הנתונים.
| מאפיין | תוכן |
|---|---|
| מצב פקודת הביצוע מול הברוקר | Trade.execution_state — שדה נפרד מ-Trade.status (תחום CH3-003) |
| ערכים | NOT_SENT → SUBMITTING → SENT → WORKING / PARTIALLY_FILLED → CONFIRMED / FAILED / EXPIRED / CANCELLED (עם CANCEL_PENDING ו-RECONCILIATION_REQUIRED ביניים) |
| חוזה סיום ל-Trade.status | רק אירוע סופי מאומת מ-CH3-003 רשאי להעביר Trade.status מ-approved: CONFIRMED→executed, FAILED→rejected, EXPIRED/CANCELLED(filled=0)→expired. SUBMITTING / WORKING / PARTIALLY_FILLED / CANCEL_PENDING / RECONCILIATION_REQUIRED אינם משנים Trade.status. |
Trade.status ו-execution_state הם שני מישורים נפרדים. CH3-003 אינו נוגע למישור 2 אלא דרך המפות הקנוניות בלבד, דרך transitionTradeStatus של CH3-002.
| מצב | סוג | תיאור | טרמינלי? |
|---|---|---|---|
| NOT_SENT | קיים | לא נשלחה הוראה לברוקר. מצב התחלתי. מאפשר את כל בדיקות טרום-השליחה של CH3-002 (שנחסמות כאשר execution_state != NOT_SENT). | לא |
| SUBMITTING | חדש (0.2) | רשומת ExecutionCommand אטומית נכתבה עם client_order_id קבוע לפני הקריאה ל-submitOrder. submit_started_at נקבע כאן (תחילת Response Timeout). חלון הקריסה הקריטי: אם התשובה אבדה/המערכת קרסה → queryStatus לפי client_order_id, לא שליחה כפולה. | לא |
| SENT | קיים (מורחב) | התקבל broker_order_id בחזרה מ-submitOrder (SUBMIT_ACCEPTED). broker_accepted_at נקבע כאן. ממתינים ל-ORDER_ACTIVE / מילוי / דחייה. Trade.status נשאר approved. | לא |
| WORKING | חדש | הברוקר אישר שהפקודה פעילה בספר הפקודות (ORDER_ACTIVE) וממתינה למילוי (Limit/Stop מסוג DAY). טרם מולאה כלל. | לא |
| PARTIALLY_FILLED | חדש | מולאה כמות חלקית; יתרה פתוחה אצל הברוקר. התיק והיתרה מתעדכנים חד-פעני לפי fill_id; היתרות השמורות מופחתות בכל Fill. Trade.status נשאר approved. | לא |
| CANCEL_PENDING | חדש (0.2) | בקשת ביטול נשלחה לברוקר (עם cancel_request_id) וממתינה לאישורו. הפקודה עדיין פעילה — עלולה להתמלא חלקית/מלא בינתיים; מילוי חלקי אינו מבטל את הביטול (0.21). | לא |
| CONFIRMED | קיים (מוגדר) | מילוי סופי מאומת (מלא, או יתרה בוטלה/פגה ואומתה עם filled_quantity>0). מאפשר את המעבר היחיד ל-Trade.status=executed (עם executed_quantity בפועל). | כן |
| FAILED | קיים (מוגדר) | דחייה מפורשת מהברוקר, או אימות שהפקודה אינה קיימת אצלו. Timeout לבדו אינו FAILED. מעביר את Trade.status ל-rejected. | כן |
| RECONCILIATION_REQUIRED | קיים (מורחב) | התוצאה אינה ודאית. אינו משנה Trade.status — העסקה נשארת approved. ניתן לחזור ממנו ל-CANCEL_PENDING/WORKING/PARTIALLY_FILLED/CONFIRMED/FAILED/EXPIRED/CANCELLED לפי תשובת הברוקר בבירור (0.21: גם ל-CANCEL_PENDING ו-CANCELLED). | לא |
| EXPIRED | חדש | תוקף הפקודה הסתיים (EOD לפי לוח הבורסה) עם filled_quantity=0. אינו FAILED. מעביר את Trade.status ל-expired (ניתן לאישור מחדש כ-Trade חדש + Message — CH3-002). | כן |
| CANCELLED | חדש (0.2) | הברוקר אישר ביטול מלא עם filled_quantity=0. טרמינלי; מעביר את Trade.status ל-expired. אם בוטלה עם filled_quantity>0 — המעבר הוא ל-CONFIRMED (חלקי), לא ל-CANCELLED. | כן |
| מ- | אל | שחקן מפעיל | טריגר |
|---|---|---|---|
| NOT_SENT | SUBMITTING | EXECUTION_ENGINE | כתיבת ExecutionCommand אטומית עם client_order_id קבוע לפני submitOrder. submit_started_at נקבע (תחילת Response Timeout). |
| SUBMITTING | SENT | BROKER_EVENT (SUBMIT_ACCEPTED) | submitOrder החזיר broker_order_id. broker_accepted_at נקבע. idempotency_key ננעל לפי client_order_id. (0.21: ACK מופרד מ-ORDER_ACTIVE.) |
| SUBMITTING | RECONCILIATION_REQUIRED | EXECUTION_ENGINE (crash/timeout) | התשובה אבדה/המערכת קרסה בחלון הקריטי. לא שולחים שוב — queryStatus לפי client_order_id. |
| SUBMITTING | FAILED | BROKER_EVENT (reject) | הברוקר דחה את הפקודה בטרם נקלטה (למשל פרמטרים לא-תקינים). |
| SENT | WORKING | BROKER_EVENT (ORDER_ACTIVE) | הברוקר אישר שהפקודה פעילה בספר הפקודות. (0.21: ACK מופרד מ-SUBMIT_ACCEPTED.) Market אינה עוברת ל-WORKING אלא למילוי. |
| SENT | PARTIALLY_FILLED | BROKER_EVENT (fill) | מילוי חלקי ראשון התקבל לפני ORDER_ACTIVE. נרשם כאירוע Fill עם fill_id ייחודי. |
| SENT | CONFIRMED | BROKER_EVENT (fill) | מילוי מלא מאומת עבור Market (או מילוי מיידי מלא). |
| SENT | FAILED | BROKER_EVENT (reject) | דחייה מפורשת או אימות אי-קיום. Timeout לבדו אינו גורם לכך. |
| SENT | RECONCILIATION_REQUIRED | EXECUTION_ENGINE (timeout) | Response Timeout (response_timeout_sec מ-submit_started_at) חלף ללא מענה ודאי. |
| WORKING | PARTIALLY_FILLED | BROKER_EVENT (fill) | מילוי חלקי התקבל. צבירה דטרמיניסטית; requested_quantity אינו משתנה; יתרות שמורות מופחתות. |
| WORKING | CONFIRMED | BROKER_EVENT (fill) | מילוי מלא מאומת. |
| WORKING | EXPIRED | EXECUTION_ENGINE (EOD) | סגירת הבורסה (לוח בורסה) ללא מילוי (filled_quantity=0). נדרש אימות שאין יתרה פעילה. |
| WORKING | CANCEL_PENDING | USER/CANCEL | בקשת ביטול נשלחה לברוקר עם cancel_request_id. |
| WORKING | RECONCILIATION_REQUIRED | EXECUTION_ENGINE | תוצאה לא ודאית (למשל איבוד קשר עם הברוקר בעוד הפקודה פעילה). |
| PARTIALLY_FILLED | PARTIALLY_FILLED | BROKER_EVENT (fill) | מילוי נוסף. צבירה; כל Fill בעל fill_id ייחודי; יתרות שמורות מופחתות בכל Fill. |
| PARTIALLY_FILLED | CONFIRMED | BROKER_EVENT (fill/cancel/EOD) | מילוי מלא, או ביטול/תפוגה מאומתים של היתרה כאשר filled_quantity>0. מקבע executed_quantity חלקית. |
| PARTIALLY_FILLED | EXPIRED | EXECUTION_ENGINE | אסור — אם filled_quantity>0 היתרה חייבת להיבדק עד CONFIRMED/RECONCILIATION. (מעבר חסום.) |
| PARTIALLY_FILLED | CANCEL_PENDING | USER/CANCEL | בקשת ביטול יתרה פתוחה עם cancel_request_id. |
| PARTIALLY_FILLED | RECONCILIATION_REQUIRED | EXECUTION_ENGINE | אי-ודאות לגבי גורל היתרה אצל הברוקר. |
| CANCEL_PENDING | CANCELLED | BROKER_EVENT (cancel-ack) | הברוקר אישר ביטול מלא כאשר filled_quantity=0. |
| CANCEL_PENDING | CONFIRMED | BROKER_EVENT (cancel-ack / fill) | אישור ביטול היתרה עם filled_quantity>0, או מילוי מלא בזמן הביטול → CONFIRMED (חלקי/מלא). |
| CANCEL_PENDING | CANCEL_PENDING | BROKER_EVENT (partial fill) | 0.21: מילוי חלקי בזמן CANCEL_PENDING אינו מבטל את הביטול — נשארים ב-CANCEL_PENDING ומעדכנים את הכמות והיתרות השמורות. |
| CANCEL_PENDING | RECONCILIATION_REQUIRED | EXECUTION_ENGINE (cancel timeout/reject) | cancel_timeout_sec חלף או הברוקר דחה את הביטול. אין חזרה עיוורת — queryStatus לפי cancel_request_id/broker_order_id. |
| RECONCILIATION_REQUIRED | CANCEL_PENDING | RECONCILIATION | 0.21: שאילתה החזירה שביטול עדיין ממתין אצל הברוקר (אי-הוודאות החלה בזמן CANCEL_PENDING). |
| RECONCILIATION_REQUIRED | CANCELLED | RECONCILIATION | 0.21: בירור הסתיים באישור ביטול מלא ללא מילוי. |
| RECONCILIATION_REQUIRED | WORKING | RECONCILIATION | שאילתת הברוקר החזירה: הפקודה קיימת ועדיין פעילה. |
| RECONCILIATION_REQUIRED | PARTIALLY_FILLED | RECONCILIATION / BROKER_EVENT (late fill) | שאילתה החזירה מילוי חלקי, או הגיע Fill מאוחר בזמן הבירור. |
| RECONCILIATION_REQUIRED | CONFIRMED | RECONCILIATION | בירור הסתיים באישור תוצאה סופית ומאומתת. |
| RECONCILIATION_REQUIRED | FAILED | RECONCILIATION | בירור הסתיים בדחייה מפורשת / אי-קיום מאומת. |
| RECONCILIATION_REQUIRED | EXPIRED | RECONCILIATION | בירור הסתיים בתפוגה מאומתת עם filled_quantity=0. |
מעברים אסורים:
CONFIRMED / FAILED / EXPIRED / CANCELLED הם טרמינליים ב-execution_state — אין מעבר החוצה ישירות.
אירוע ברוקר מאוחר הסותר מצב טרמינלי אינו משנה את execution_state ואינו פותח מחדש את ה-Trade המקורי — נפתח Reconciliation Case, נחסמת פעולה נוספת באותה פקודה, ולאחר אימות נוצר Corrective Trade / ExecutionAdjustment מקושר שרושם את הביצוע המאוחר ומעדכן את התיק וה-Audit (0.22). ה-Trade המקורי נשאר CANCELLED/EXPIRED — CH3-002 אינו נשבר.
NOT_SENT → SENT חסום — חובת SUBMITTING תחילה (חלון הקריסה חייב להיות מכוסה).
NOT_SENT → CONFIRMED / WORKING / PARTIALLY_FILLED חסום — אין "מילוי" ללא שליחה מתועדת.
PARTIALLY_FILLED → EXPIRED חסום — אם filled_quantity>0, גורל היתרה חייב להיבדק עד CONFIRMED (חלקי) או RECONCILIATION_REQUIRED.
CANCEL_PENDING → PARTIALLY_FILLED חסום כחזרה עיוורת — מילוי חלקי בזמן ביטול אינו מבטל את הביטול; נשארים ב-CANCEL_PENDING ומעדכנים כמות (0.21).
נסיגה מ-WORKING/PARTIALLY_FILLED ל-SUBMITTING/NOT_SENT חסומה — אין "ביטול שליחה"; הפקדה מחייבת פעולת ביטול מפורשת מול הברוקר.
Timeout לבדו אינו יכול להעביר ל-FAILED — רק דחייה מפורשת או אימות אי-קיום. Timeout ללא ודאות → RECONCILIATION_REQUIRED.
אסור לכתוב execution_state ישירות במסד הנתונים — גם ב-Paper. כל שינוי דרך transitionExecutionState המקבל אירוע מ-Adapter או תוצאת בירור.
אין כתיבה ישירה של Trade.status מ-CH3-003 למעט המפות הקנוניות, ורק דרך transitionTradeStatus של CH3-002.
transitionExecutionState(tradeId, expectedFrom, expectedVersion, to, brokerEvent, idempotencyKey) היא הדרך היחידה לשנות execution_state. אף קוד אינו רשאי לכתוב Trade.update({ execution_state }) ישירות (אכיפת grep, כמו CH3-002).
גם ב-Paper אסור לשנות execution_state ישירות — ה-Paper Broker Adapter מחזיר אירועי ברוקר דרך אותו ממשק שבו ישתמש ה-Live Adapter, והמכונה משנה את המצב.
אטומיות שליחה: לפני כל submitOrder נכתבת רשומת ExecutionCommand אטומית עם client_order_id קבוע. המעבר NOT_SENT → SUBMITTING מתבצע לפני הקריאה וקובע submit_started_at; SENT רק לאחר קבלת broker_order_id (SUBMIT_ACCEPTED) וקובע broker_accepted_at. קריסה בחלון זה → queryStatus לפי client_order_id, לעולם לא שליחה כפולה עיוורת.
הפרדת ACK (0.21): SUBMIT_ACCEPTED = התקבל broker_order_id (SUBMITTING → SENT). ORDER_ACTIVE = הפקודה אושרה כפעילה בספר הפקודות (SENT → WORKING). שני אירועים נפרדים, שני זמנים נפרדים: submit_started_at ו-broker_accepted_at.
idempotency: שליחה חוזרת עם אותו client_order_id/idempotency_key מחזירה את אותה תוצאה — ללא שליחה כפולה לברוקר וללא מעבר נוסף. הוראה עסקית חדשה → client_order_id חדש.
מניעת שליחה כפולה: כל עוד execution_state ב-{SUBMITTING, SENT, WORKING, PARTIALLY_FILLED, CANCEL_PENDING} וקיים client_order_id/broker_order_id — אסור לשלוח הוראה נוספת על אותו Trade.
מירוץ Fill/ביטול (0.21): מילוי חלקי בזמן CANCEL_PENDING אינו מבטל את הביטול — נשארים ב-CANCEL_PENDING ומעדכנים כמות/יתרות. מילוי מלא ב-CANCEL_PENDING → CONFIRMED. אישור ביטול: filled_quantity=0 → CANCELLED; filled_quantity>0 → CONFIRMED חלקי. דחיית ביטול או cancel_timeout_sec → queryStatus (לא חזרה עיוורת). ביטול דורש cancel_request_id משלו.
אטומיות קליטת Fill (0.21): ארבע הפעולות הבאות מתבצעות באותה יחידה אטומית (או בתהליך בר-שחזור): (1) כתיבת האירוע ל-BrokerEventJournal; (2) עדכון filled_quantity + מכונת המצבים; (3) עדכון portfolioLedger; (4) כתיבת האירוע היוצא ל-Outbox. בדיקת קריסה בין כל שני שלבים — אין Fill שנרשם ביומן אך לא בתיק, ולהפך.
עדכון תיק חד-פעני: כל Fill מעדכן את התיק, המזומן והחשיפה פעם אחת בלבד, עם מפתח ייחודי המבוסס על fill_id. CONFIRMED רק מסיים את הפקודה ומשחרר את יתרת היתרות השמורות — אינו רושם שוב כמויות שכבר נרשמו.
שחרור יתרות בכל Fill (0.21): היתרות השמורות מופחתות כבר בכל Fill — קנייה: הפחתת reserved_cash_usd בחלק שנוצל; מכירה: הפחתת reserved_shares בכמות שמולאה. בסיום משוחררת רק היתרה שנותרה שמורה (אחרת GTC חלקי משאיר כסף/מניות נעולים שלא לצורך).
מילוי חלקי: requested_quantity אינו משתנה. מנוהלים בנפרד filled_quantity (מצטבר), remaining_quantity, average_fill_price, last_fill_at. כל Fill בעל fill_id ייחודי; קליטה כפולה נחסמת ב-Inbox.
גשר Trade.status: המעבר היחיד מ-approved ל-executed מתרחש רק כש-execution_state=CONFIRMED. FAILED→rejected. EXPIRED/CANCELLED (filled_quantity=0)→expired. כל המעברים דרך transitionTradeStatus של CH3-002.
SUBMITTING / WORKING / PARTIALLY_FILLED / CANCEL_PENDING / RECONCILIATION_REQUIRED אינם משנים את Trade.status — העסקה נשארת approved וחיה למשתמש.
Inbox לעומת Outbox: BrokerEventJournal (Inbox) הוא מקור האמת לאירועים שהתקבלו מהברוקר, עם Unique Constraint חד-משמעי (0.21: Fill = broker_type + broker_account_id + fill_id; אירוע אחר = broker_type + broker_account_id + broker_event_id). Outbox לפרסום יוצא בלבד. last_fill_id לבדו אינו מונע קליטה חוזרת של Fill ישן שהגיע שלא לפי הסדר.
אירוע מאוחר אחרי מצב טרמינלי (0.22): אם מתקבל Fill אמיתי ומאומת לאחר CANCELLED/EXPIRED/CONFIRMED/FAILED שסותר את המצב — ה-Trade המקורי אינו נפתח מחדש ואינו משנה execution_state או Trade.status (כדי לא לשבור את CH3-002: executed מחייב execution_state=CONFIRMED). במקום זאת — נפתח Reconciliation Case, נחסמת פעולה נוספת באותה פקודה עד הבירור, ולאחר אימות נוצר Corrective Trade / ExecutionAdjustment מקושר (corrective_for_trade_id + original_terminal_state) הרושם את הביצוע המאוחר ומעדכן את התיק וה-Audit. ה-Trade המקורי נשאר CANCELLED/EXPIRED והיסטוריית האירועים נשמרת.
הגנות CH3-002 נשמרות: Global Freeze, Approval TTL ו-Trade Validity אינן פועלות לאחר שליחה (execution_state != NOT_SENT) — רק חוזה הסיום של CH3-003 רשאי למעבר.
EOD לפי לוח הבורסה: סגירת יום המסחר נקבעת לפי MarketRegion של הבורסה (אזור זמן, חגים, סגירה מוקדמת) — לא לפי סוף היום בשרת.
דטרמיניסטיות: כל ערכי execution_state ו-reason_code נשמרים ב-UPPERCASE. ה-Paper Adapter קובע תוצאות דטרמיניסטיות לבדיקות קבלה יציבות.
| רכיב | תיאור |
|---|---|
| ממשק Broker Adapter | ממשק אחיד: submitOrder({client_order_id, order}) → { broker_order_id }, cancelOrder({broker_order_id, cancel_request_id}), queryStatus(client_order_id|broker_order_id) → BrokerEvent. אירועים: SUBMIT_ACCEPTED, ORDER_ACTIVE, fill, reject, timeout, cancel-ack, cancel-reject, EOD. |
| Paper Adapter (כעת) | מדמה אירועי ברוקר באופן דטרמיניסטי; תומך בהזרקת מילוי חלקי/דחייה/Timeout/ביטול/Fill-בזמן-ביטול לבדיקות. כל אירוע עובר דרך המכונה — אין כתיבה ישירה. |
| Live Adapter (עתיד) | קריאת API אמיתית לברוקר (IB/Alpaca וכו'); מיפוי תשובות הברוקר לאותם BrokerEvents. הפעלה רק לאחר Live-Go נפרד + אישור. |
| שיתוף מנגנונים | שני ה-Adapters חולקים: client_order_id/idempotency_key, ExecutionCommand, Inbox+Outbox, מניעת שליחה כפולה, מכונת מצבים, Reconciliation, גשר Trade.status, Audit והגנות CH3-002. ההבדל היחיד הוא מקור תשובת הביצוע. |
ההבדל היחיד בין Paper ל-Live הוא מקור תשובת הביצוע. כל יתר המנגנונים משותפים וזהים. הפעלת Live דורשת Live-Go + אישור.
| נושא | כלל |
|---|---|
| Response Timeout (קנוני) | response_timeout_sec = 20 שניות, נמדד מ-submit_started_at (0.21). אחיד לכל סוגי הפקודות. אין תשובה בזמן זה → RECONCILIATION_REQUIRED (לא FAILED). |
| Cancel Timeout (קנוני, 0.22) | cancel_timeout_sec = 20 שניות (קנוני, בדומה ל-response_timeout_sec; ניתן לשינוי ב-SystemSettings). חלף → queryStatus לפי cancel_request_id/broker_order_id (לא חזרה עיוורת למצב קודם). |
| Order Validity — Market | market_fill_timeout_sec = 90 שניות מ-broker_accepted_at. Market שלא התקבל עבורו מילוי מאומת בזמן זה → RECONCILIATION_REQUIRED. |
| Order Validity — Limit/Stop (DAY) | עד סגירת הבורסה לפי לוח המסחר (MarketRegion: אזור זמן, חגים, סגירה מוקדמת). אושרה אצל הברוקר → WORKING עד מילוי/ביטול/EOD. |
| Order Validity — GTC | רק אם הוגדרה ואושרה במפורש מראש; ברירת מחדל DAY. אין GTC סתמי. |
| FAILED | רק לאחר דחייה מפורשת מהברוקר או אימות אי-קיום. לעולם לא בגלל Timeout בלבד. |
| סמנטיקת השעונים | השעות הן משך חיי הפקודה — לא Timeout של התקשורת. גם פקודת Limit שממתינה שעות צריכה לקבל SUBMIT_ACCEPTED בתוך response_timeout_sec. |
requested_quantity אינו משתנה לעולם — הכמות המקורית נשארת קבועה.
filled_quantity — מצטבר הכמות שמולאה בפועל (מתעדכן בכל אירוע Fill, חד-פעני לפי fill_id).
remaining_quantity = requested_quantity − filled_quantity (נגזר, נשמר לנוחות שאילתה).
average_fill_price — מחיר מילוי ממוצע משוקלל בכמות על כל אירועי ה-Fill. היסטורי: רק מראיה קיימת; אחרת null + historical_fill_price_unknown.
last_fill_id / last_fill_at — ה-Fill האחרון (Cache; מקור האמת ב-Inbox).
כל מילוי נרשם כאירוע Broker Fill נפרד עם fill_id ייחודי ב-Inbox; קליטה כפולה נחסמת גם אם הגיע שלא לפי הסדר.
העסקה אינה עוברת ל-executed עד CONFIRMED. יתרת הפקודה ממשיכה להמתין אצל הברוקר עד מילוי מלא, ביטול או תפוגה.
סגירת סתירת EOD: filled_quantity=0 ופגה → EXPIRED; filled_quantity>0 והיתרה פגה/בוטלה ואומתה → CONFIRMED חלקי; גורל יתרה לא-ודאי → RECONCILIATION_REQUIRED. PARTIALLY_FILLED→EXPIRED חסום.
שחרור יתרות בכל Fill (0.21): בקנייה מופחת reserved_cash_usd בחלק שנוצל; במכירה מופחת reserved_shares בכמות שמולאה. בסיום משוחררת רק היתרה שנותרה שמורה.
BrokerEventJournal (Inbox) — מקור האמת לאירועים נכנסים, עם Unique Constraint חד-משמעי (0.21): Fill = broker_type + broker_account_id + fill_id; אירוע אחר = broker_type + broker_account_id + broker_event_id. שדות: { trade_id, client_order_id, broker_order_id, broker_type, broker_account_id, fill_id / broker_event_id, event_type, fill_quantity, fill_price, ts }. Outbox — אירועים יוצאים שהמערכת מפרסמת לאחר שינוי מצב. קליטת Fill חוזרת (אפילו שלא בסדר) נחסמת ב-Inbox, לא רק על בסיס last_fill_id.
ביטול דורש מסלול מצבים מלא: CANCEL_PENDING (הבקשה נשלחה עם cancel_request_id) → CANCELLED (אישור, filled_quantity=0) או → CONFIRMED (אישור ביטול יתרה עם filled_quantity>0, או מילוי מלא בזמן הביטול).
0.21: מילוי חלקי בזמן CANCEL_PENDING אינו מבטל את הביטול — נשארים ב-CANCEL_PENDING ומעדכנים כמות/יתרות. (החזרה העיוורת ל-PARTIALLY_FILLED בוטלה.)
דחיית ביטול או cancel_timeout_sec → queryStatus לפי cancel_request_id/broker_order_id; אין חזרה עיוורת למצב קודם.
CANCELLED (filled_quantity=0) → Trade.status=expired (טרמינלי; ניתן לאישור מחדש כ-Trade חדש).
מתוך Reconciliation (0.21): RECONCILIATION_REQUIRED → CANCEL_PENDING (הביטול עדיין ממתין) ו-→ CANCELLED (הביטול הושלם ללא מילוי).
אין cancelOrder בממשק ללא מסלול מצבים — כל ביטול עובר דרך transitionExecutionState.
RECONCILIATION_REQUIRED אינו משנה Trade.status — העסקה נשארת approved וחיה; מסומנת לבירור.
פתרון הבירור יכול להוביל לכל אחד מ-CANCEL_PENDING / WORKING / PARTIALLY_FILLED / CONFIRMED / FAILED / EXPIRED / CANCELLED לפי תשובת הברוקר (0.21: גם למצבי הביטול).
Fill מאוחר המגיע בזמן הבירור מטופל כאירוע נכנס רגיל (נכתב ל-Inbox) ומעביר ל-PARTIALLY_FILLED/CONFIRMED לפי הכמות.
כל פעולת בירור מתועדת ב-Outbox עם actor, reason_code ומזהה בירור. פתרון ידני דורש משתמש מורשה בלבד.
אירוע מאוחר אחרי מצב טרמינלי (0.22): פתיחת Reconciliation Case, חסימת פעולה נוספת באותה פקודה, ולאחר אימות — יצירת Corrective Trade / ExecutionAdjustment מקושר (corrective_for_trade_id + original_terminal_state) הרושם את הביצוע המאוחר ומעדכן את התיק וה-Audit. ה-Trade המקורי נשאר CANCELLED/EXPIRED; אינו נפתח מחדש ואינו שובר את CH3-002.
| execution_state סופי | Trade.status (דרך CH3-002) |
|---|---|
| CONFIRMED | approved → executed (עם executed_quantity) |
| FAILED | approved → rejected |
| EXPIRED | approved → expired (filled_quantity=0) |
| CANCELLED | approved → expired (filled_quantity=0) |
| SUBMITTING / WORKING / PARTIALLY_FILLED / CANCEL_PENDING / RECONCILIATION_REQUIRED | ללא שינוי — העסקה נשארת approved |
| Actor | מעברים מותרים | תנאי/הערה |
|---|---|---|
| EXECUTION_ENGINE | NOT_SENT → SUBMITTING; SUBMITTING → RECONCILIATION_REQUIRED (crash/timeout); SENT → RECONCILIATION_REQUIRED (timeout); WORKING → EXPIRED (EOD); CANCEL_PENDING → RECONCILIATION_REQUIRED (cancel timeout) | שליחה, גילוי Timeout/קריסה, תפוגה, timeout ביטול — דטרמיניסטי |
| BROKER_EVENT | SUBMITTING → SENT (SUBMIT_ACCEPTED)/FAILED; SENT → WORKING (ORDER_ACTIVE)/CONFIRMED/FAILED; WORKING → PARTIALLY_FILLED/CONFIRMED; PARTIALLY_FILLED → PARTIALLY_FILLED/CONFIRMED; CANCEL_PENDING → CANCELLED/CONFIRMED/CANCEL_PENDING (partial fill) | אירועים מה-Adapter בלבד |
| USER/CANCEL | WORKING → CANCEL_PENDING; PARTIALLY_FILLED → CANCEL_PENDING | בקשת ביטול ממשתמש מורשה עם cancel_request_id |
| RECONCILIATION (ידני/אוטומטי) | RECONCILIATION_REQUIRED → CANCEL_PENDING/CANCELLED/WORKING/PARTIALLY_FILLED/CONFIRMED/FAILED/EXPIRED | פתרון בירור — ידני דורש משתמש מורשה |
ערכי reason_code קנוניים:
| שדה | סוג | תיאור |
|---|---|---|
| execution_state (enum) | string | מורחב: NOT_SENT, SUBMITTING, SENT, WORKING, PARTIALLY_FILLED, CANCEL_PENDING, CONFIRMED, FAILED, RECONCILIATION_REQUIRED, EXPIRED, CANCELLED. כולם UPPERCASE. (⚠ דורש אישור Architect) |
| execution_version | number | מונה אופטימי (CAS) ל-execution_state. מתעדכן ב-$inc רק אם תנאי הגרסה התקיים. |
| client_order_id | string | null | מזהה פקודה קבוע שנשלח לברוקר בכל Retry. נקבע ב-SUBMITTING ואינו משתנה. מאפשר queryStatus גם ללא broker_order_id. |
| cancel_request_id | string | null | 0.21: מזהה ייחודי לבקשת הביטול (Idempotency משלו). מאפשר queryStatus של הביטול גם לאחר Timeout. |
| broker_order_id | string | null | מזהה הפקודה אצל הברוקר (מוחזר ב-SUBMIT_ACCEPTED). |
| broker_type | string | null | איזה Adapter טיפל (PAPER / IB / ALPACA...). חלק מה-Unique Constraint של ה-Inbox (0.21). |
| broker_account_id | string | null | מזהה חשבון הברוקר. חלק מה-Unique Constraint של ה-Inbox (0.21). |
| broker_environment | string | null | PAPER | LIVE. כרגע PAPER בלבד; LIVE דורש Live-Go. |
| requested_quantity | number | הכמות המקורית. אינה משתנה לאחר שליחה. |
| filled_quantity | number | מצטבר הכמות שמולאה בפועל. ברירת מחדל 0. |
| executed_quantity | number | הכמות הסופית שבוצעה — נקבעת רק ב-CONFIRMED (מלאה או חלקית מאומתת). |
| average_fill_price | number | null | מחיר מילוי ממוצע משוקלל. null כשאין ראיה למחיר בפועל. |
| historical_fill_price_unknown | boolean | true עבור רשומות executed ישנות ללא ראיית מחיר ביצוע אמיתית. |
| last_fill_id | string | null | מזהה אירוע ה-Fill האחרון (Cache; מקור האמת ב-Inbox). |
| last_fill_at | date-time | null | מועד המילוי האחרון. |
| submit_started_at | date-time | null | 0.21: מועד המעבר ל-SUBMITTING — תחילת Response Timeout. |
| broker_accepted_at | date-time | null | 0.21: מועד קבלת broker_order_id (SUBMIT_ACCEPTED). |
| reserved_cash_usd | number | null | מזומן שמור לפקודת קנייה פתוחה; מופחת בכל Fill והיתרה משוחררת בסיום (0.21). |
| reserved_shares | number | null | כמות מניות שמורה לפקודת מכירה פתוחה; מופחתת בכל Fill והיתרה משוחררת בסיום (0.21). |
| order_validity | string | null | time_in_force קנוני: DAY (ברירת מחדל) | GTC (רק באישור מפורש). |
| corrective_for_trade_id | string | null | 0.22: מציין ש-Trade זה הוא Corrective Trade / ExecutionAdjustment ל-Trade מקורי שהגיע למצב טרמינלי (CANCELLED/EXPIRED) ואחריו התקבל Fill מאוחר מאומת. ה-Trade המקורי אינו נפתח מחדש. |
| original_terminal_state | string | null | 0.22: מצב טרמינלי של ה-Trade המקורי שה-Corrective Trade מתקן (CANCELLED/EXPIRED/CONFIRMED/FAILED). רק ב-Corrective Trade. |
| reconciliation_case_id | string | null | 0.22: מזהה Reconciliation Case שהוליד את ה-Corrective Trade (אם נוצר מאירוע מאוחר סותר טרמינלי). |
אינם נשמרים ב-Trade:
expectedFrom / expectedVersion — פרמטרי קריאה (חלק מ-CAS), אינם נשמרים ב-Trade.
broker_event (גלם) — נכתב ל-Inbox (BrokerEventJournal); ב-Trade נשמרות רק נגזרות (broker_order_id, last_fill_id).
execution_version (CAS) מקביל ל-status_version של CH3-002. רשומת ExecutionCommand אטומית מונעת שליחה כפולה בקריסה. BrokerEventJournal (Inbox) הוא מקור האמת לאירועים נכנסים עם Unique Constraint חד-משמעי; עיבוד Fill אטומי בארבעה שלבים (Inbox → מכונה+filled_quantity → portfolioLedger → Outbox) בר-שחזור מקריסה בין כל שני שלבים. Outbox לפרסום יוצא. ב-Trade נשמרים client_order_id, broker_order_id ו-last_fill_id כ-Cache. Retry עם אותו client_order_id/fill_id מחזיר את אותה תוצאה — ללא שליחה/קליטה כפולה.
רשומות Trade קיימות עם execution_state=NOT_SENT: נשארות כשתפקידן (המצב ההתחלתי החוקי).
רשומות executed ישנות: execution_state=CONFIRMED; executed_quantity=Trade.quantity; filled_quantity=Trade.quantity. average_fill_price — יש לחפש מחיר ביצוע קיים ב-tradeExecution/יומן התיק; ללא ראיה → null + historical_fill_price_unknown=true. אסור להניח average_fill_price=Trade.price עיוורת.
רשומות approved/wait פתוחות: execution_state נשאר NOT_SENT; שדות Fill/יתרות/זמנים ריקים.
כל ערכי execution_state/reason_code נשמרים UPPERCASE; Migration ממיר ערכים ישנים lowercase ל-UPPERCASE.
Migration חד-פעני, הפיך — סקריפט rollback מוכן לפני הרצה.
| שלב | פעולה |
|---|---|
| executeTrade / refreshPortfolioPrices (CH3-001/002) | מקדמים Trade ל-approved; אינם שולחים לברוקר — מעבירים ל-Execution Engine. |
| Execution Engine (CH3-003) | כותב ExecutionCommand אטומית עם client_order_id → NOT_SENT→SUBMITTING (submit_started_at); מפקיד יתרות (reserved); קורא ל-Broker Adapter; על SUBMIT_ACCEPTED → SENT + broker_order_id + broker_accepted_at. |
| Broker Event ingestion (Inbox, אטומי) | אירועי SUBMIT_ACCEPTED/ORDER_ACTIVE/fill/reject/timeout/cancel-ack/cancel-reject/EOD נכתבים ל-BrokerEventJournal (Unique Constraint) ומוזנים ל-transitionExecutionState ביחידה אטומית עם portfolioLedger ו-Outbox (0.21). |
| גשר Trade.status (חוזה סיום) | CONFIRMED→executed; FAILED→rejected; EXPIRED/CANCELLED(filled=0)→expired. רק דרך CH3-002. |
| עדכון תיק חד-פעני + שחרור יתרות | כל Fill מעדכן תיק+מזומן+חשיפה לפי fill_id ומפחית יתרות שמורות; CONFIRMED משחרר רק את היתרה הנותרת. |
base44/entities/Trade.jsonc — הרחבת enum של execution_state + שדות Fill/Timeout/Adapter/יתרות + historical_fill_price_unknown + submit_started_at/broker_accepted_at/cancel_request_id + שדות Corrective Trade (corrective_for_trade_id, original_terminal_state, reconciliation_case_id) (⚠ דורש אישור Architect).
base44/entities/SystemSettings.jsonc — פרמטרי Timeout קנוניים (response_timeout_sec=20, market_fill_timeout_sec=90, cancel_timeout_sec=20) + ברירת מחדל order_validity=DAY. (⚠ דורש אישור Architect)
base44/shared/executionStateMachine.ts (חדש) — transitionExecutionState() + CAS (execution_version) + טבלת מעברים מורחבת + חסימות (PARTIALLY_FILLED→EXPIRED, CANCEL_PENDING→PARTIALLY_FILLED עיוורת, מתוך טרמינלי) + גשר ל-transitionTradeStatus (CH3-002).
base44/shared/brokerEventJournal.ts (חדש) — Inbox לאירועים נכנסים עם Unique Constraint חד-משמעי (0.21) + קליטה חד-פענית גם לסדר הגעה שגוי.
base44/shared/fillProcessor.ts (חדש, 0.21) — עיבוד Fill אטומי בארבעה שלבים (Inbox → מכונה+filled_quantity → portfolioLedger → Outbox) בר-שחזור מקריסה בין כל שני שלבים.
base44/shared/portfolioLedger.ts (חדש) — עדכון תיק/מזומן/חשיפה חד-פעני לפי fill_id; הפקדה/הפחתה/שחרור יתרות בכל Fill; רישום Corrective Trade מאירוע מאוחר מאומת (מקושר ל-Trade טרמינלי, ללא פתיחתו מחדש).
base44/shared/brokerAdapter.ts (חדש) — ממשק Broker Adapter + Paper Adapter (דטרמיניסטי, תומך בביטול/Timeout/Fill-בזמן-ביטול). עמדת Live Adapter ריקה + LIVE_GO_PENDING.
base44/shared/tradeExecution.ts — הפרדת "אישור עסקי" מ"שליחה לברוקר"; קריאה ל-Execution Engine; אין כתיבת CONFIRMED ישירות; עדכון תיק חד-פעני דרך portfolioLedger.
base44/functions/executeTrade/entry.ts — מעביר approved ל-Execution Engine; אינו כותב execution_state.
base44/functions/refreshPortfolioPrices/entry.ts — חסימת שינוי Trade.status כש-execution_state != NOT_SENT; EOD לפי לוח בורסה.
src/components/invest/Badges.jsx — תצוגה ל-execution_state המורחב (SUBMITTING/WORKING/PARTIALLY_FILLED/CANCEL_PENDING/CANCELLED/EXPIRED).
base44/functions/runCh3AcceptanceTests/entry.ts — הרחבה לבדיקות קריסה לפני/אחרי submitOrder, אירועים מחוץ לסדר, Retry אחרי Restart, Fill בזמן Reconciliation, Fill בזמן CANCEL_PENDING, עדכון תיק חד-פעני, ואירוע מאוחר סותר טרמינלי.
כל שינוי execution_state עובר דרך transitionExecutionState — grep מאפס כתיבות ישירות של Trade.update({ execution_state }).
גם ב-Paper אין כתיבה ישירה של execution_state — ה-Adapter מחזיר אירועים דרך הממשק, והמכונה משנה את המצב.
לפני כל submitOrder נכתבת ExecutionCommand אטומית עם client_order_id; המעבר NOT_SENT→SUBMITTING לפני הקריאה וקובע submit_started_at; SENT רק לאחר broker_order_id (SUBMIT_ACCEPTED) וקובע broker_accepted_at.
0.21: ACK מופרד — SUBMIT_ACCEPTED (SUBMITTING→SENT) שונה מ-ORDER_ACTIVE (SENT→WORKING); שני זמנים נפרדים נשמרים.
קריסה בחלון SUBMITTING/SENT → המערכת שואלת את הברוקר לפי client_order_id ואינה שולחת שוב עיוורת; בדיקת קבלה לקריסה לפני ואחרי submitOrder.
שליחה חוזרת עם אותו client_order_id/idempotency_key אינה שולחת שוב ומחזירה את אותה תוצאה — ללא מעבר נוסף.
מניעת שליחה כפולה: כשקיים client_order_id/broker_order_id ו-execution_state אינו טרמינלי — אסור לשלוח הוראה נוספת.
Response Timeout (response_timeout_sec מ-submit_started_at) ללא מענה ודאי → RECONCILIATION_REQUIRED, לא FAILED.
FAILED נקבע רק לאחר דחייה מפורשת או אימות אי-קיום — לעולם לא מ-Timeout בלבד.
Limit/Stop שאושרה → WORKING עד מילוי/ביטול/EOD. Market שלא מולאה ב-market_fill_timeout_sec → RECONCILIATION_REQUIRED.
EOD לפי לוח הבורסה (אזור זמן/חגים/סגירה מוקדמת) — לא סוף יום בשרת.
filled_quantity=0 ופגה → EXPIRED; filled_quantity>0 והיתרה פגה/בוטלה ואומתה → CONFIRMED חלקי; גורל לא-ודאי → RECONCILIATION_REQUIRED.
PARTIALLY_FILLED → EXPIRED חסום (אסור לפוגע פקודה שמולאה חלקית).
מילוי חלקי: requested_quantity אינו משתנה; filled_quantity מצטבר; כל Fill בעל fill_id ייחודי ב-Inbox; קליטה כפולה (גם מחוץ לסדר) נחסמת.
0.21: מילוי חלקי בזמן CANCEL_PENDING נשאר ב-CANCEL_PENDING ומעדכן כמות/יתרות (אינו מבטל את הביטול); מילוי מלא ב-CANCEL_PENDING → CONFIRMED; CANCEL_PENDING→PARTIALLY_FILLED עיוורת חסומה.
0.21: אטומיות קליטת Fill — ארבע פעולות (Inbox, מכונה+filled_quantity, portfolioLedger, Outbox) ביחידה אטומית/בר-שחזור; בדיקת קריסה בין כל שני שלבים — אין Fill ביומן שלא בתיק ולהפך.
עדכון תיק חד-פעני: כל Fill מעדכן תיק+מזומן+חשיפה פעם אחת לפי fill_id; CONFIRMED רק משחרר יתרת יתרות ואינו רושם שוב. בדיקת קבלה: אין רישום כפול.
0.21: שחרור יתרות בכל Fill — קנייה מפחיתה reserved_cash_usd בחלק שנוצל; מכירה מפחיתה reserved_shares בכמות שמולאה; בסיום משוחררת רק היתרה שנותרה. GTC חלקי אינו משאיר כסף/מניות נעולים.
הפקדת יתרות: קנייה פתוחה שומרת reserved_cash_usd; מכירה פתוחה שומרת reserved_shares; שחרור/הפחתה בכל Fill ובסיום.
ביטול: CANCEL_PENDING→CANCELLED (filled=0) או →CONFIRMED (filled>0/מילוי מלא); דחייה/cancel_timeout_sec → queryStatus לפי cancel_request_id (לא חזרה עיוורת).
0.21: מתוך Reconciliation — RECONCILIATION_REQUIRED→CANCEL_PENDING (הביטול עדיין ממתין) ו-→CANCELLED (הביטול הושלם ללא מילוי).
RECONCILIATION_REQUIRED יכול לחזור ל-WORKING/PARTIALLY_FILLED — לא רק למצבים טרמינליים. Fill מאוחר בבירור מטופל כאירוע נכנס.
0.22: אירוע מאוחר הסותר מצב טרמינלי — אינו פותח מחדש את ה-Trade המקורי ואינו משנה את execution_state/Trade.status (שמירת CH3-002). נפתח Reconciliation Case, נחסמת פעולה נוספת בפקודה, ולאחר אימות נוצר Corrective Trade / ExecutionAdjustment מקושר (corrective_for_trade_id + original_terminal_state + reconciliation_case_id) הרושם את הביצוע המאוחר ומעדכן את התיק וה-Audit. ה-Trade המקורי נשאר CANCELLED/EXPIRED.
CONFIRMED → Trade.status=executed; FAILED → rejected; EXPIRED/CANCELLED(filled=0) → expired. מפות קנוניות בלבד.
SUBMITTING/WORKING/PARTIALLY_FILLED/CANCEL_PENDING/RECONCILIATION_REQUIRED אינם משנים Trade.status — העסקה נשארת approved.
Live Adapter אינו מופעל במצב paper; ניסיון לשלוח דרכו נחסם עם reason_code=LIVE_GO_PENDING.
Migration: executed_quantity/filled_quantity מאותחלים לרשומות executed ישנות; average_fill_price=null כשאין ראיה + historical_fill_price_unknown=true; ערכים ישנים מומרים ל-UPPERCASE; rollback מוכן.
CAS: שתי בקשות מקבילות על אותו execution_state → רק אחת מצליחה (execution_version).
כל מעבר execution_state מתועד ב-Message עם from→to, actor, reason_code, trade_id, client_order_id, broker_order_id, fill_id (כשרלוונטי).
0.21: Unique Constraint חד-משמעי ב-Inbox — Fill = broker_type + broker_account_id + fill_id; אירוע אחר = broker_type + broker_account_id + broker_event_id.
כל ערכי execution_state ו-reason_code נשמרים ב-UPPERCASE.
1. base44/shared/executionStateMachine.ts (חדש): transitionExecutionState() + טבלת מעברים מורחבת + CAS (execution_version) + חסימות (PARTIALLY_FILLED→EXPIRED, CANCEL_PENDING→PARTIALLY_FILLED עיוורת, מתוך טרמינלי) + גשר ל-transitionTradeStatus (CH3-002).
2. base44/shared/brokerEventJournal.ts (חדש): Inbox עם Unique Constraint חד-משמעי (0.21) וקליטה חד-פענית גם לסדר הגעה שגוי.
3. base44/shared/fillProcessor.ts (חדש, 0.21): עיבוד Fill אטומי בארבעה שלבים בר-שחזור מקריסה בין כל שני שלבים.
4. base44/shared/portfolioLedger.ts (חדש): עדכון תיק/מזומן/חשיפה חד-פעני לפי fill_id + הפקדה/הפחתה/שחרור יתרות בכל Fill + תיקון חד-פעני לאחר Reconciliation Case.
5. base44/shared/brokerAdapter.ts (חדש): ממשק Broker Adapter + Paper Adapter (דטרמיניסטי, כולל ביטול/Timeout/Fill-בזמן-ביטול). עמדת Live ריקה + LIVE_GO_PENDING.
6. base44/entities/Trade.jsonc + SystemSettings.jsonc: הרחבת execution_state + שדות Fill/Timeout/Adapter/יתרות/זמנים/cancel_request_id + Timeout קנוני (⚠ דורש אישור Architect).
7. רפקטור tradeExecution.ts ו-executeTrade/refreshPortfolioPrices: הפרדת אישור עסקי משליחה; מעבר דרך Execution Engine; עדכון תיק חד-פעני.
8. הזנת אירועי ברוקר מה-Paper Adapter ל-Inbox ולמכונה; טיפול ב-SUBMIT_ACCEPTED/ORDER_ACTIVE/fill/reject/timeout/cancel-ack/cancel-reject/EOD; מירוץ Fill/ביטול; מילוי חלקי בצבירה עם שחרור יתרות.
9. גשר Trade.status: CONFIRMED→executed, FAILED→rejected, EXPIRED/CANCELLED(filled=0)→expired דרך CH3-002.
10. Reconciliation + ביטול: מעברים חזרה ל-CANCEL_PENDING/WORKING/PARTIALLY_FILLED/CANCELLED; ביטול מלא; אירועים מאוחרים סותרי טרמינלי → Reconciliation Case + תיקון חד-פעני.
11. Migration (ראיית Audit בלבד, הפיך): executed_quantity/filled_quantity; average_fill_price=null ללא ראיה + historical_fill_price_unknown; המרה ל-UPPERCASE.
12. UI: Badges.jsx — תצוגת המצבים החדשים.
13. בדיקות קבלה מתועדות (runCh3AcceptanceTests מורחב): קריסה לפני/אחרי submitOrder, אירועים מחוץ לסדר, Retry אחרי Restart, Fill בזמן Reconciliation, Fill בזמן CANCEL_PENDING, עדכון תיק חד-פעני, אירוע מאוחר סותר טרמינלי. + דוח Ch3ExecutionReport003.