CH3-003 · ניהול מצב פקודת הביצוע מול הברוקר

גרסה 1.0 · טיוטא 8.8.2026 · P1 · Draft for Architect Review

Locked & Approved — Version 1.0

הכרעת 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 בלבד עד אז.

0 · מפת תיקוני סקירה (0.21)
הערהנושאתיקון
#1 (0.21)כפילות במשמעות ACKSUBMIT_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.
1 · בעיה ויעד (השורש)

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 ישירות במסד הנתונים.

2 · מישור 3 — מצב פקודת הביצוע (נפרד מ-Trade.status)
מאפייןתוכן
מצב פקודת הביצוע מול הברוקר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.

3 · ערכי execution_state קנוניים (UPPERCASE)
מצבסוגתיאורטרמינלי?
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.כן
4 · מטריצת מעברים חוקיים — execution_state
מ-אלשחקן מפעילטריגר
NOT_SENTSUBMITTINGEXECUTION_ENGINEכתיבת ExecutionCommand אטומית עם client_order_id קבוע לפני submitOrder. submit_started_at נקבע (תחילת Response Timeout).
SUBMITTINGSENTBROKER_EVENT (SUBMIT_ACCEPTED)submitOrder החזיר broker_order_id. broker_accepted_at נקבע. idempotency_key ננעל לפי client_order_id. (0.21: ACK מופרד מ-ORDER_ACTIVE.)
SUBMITTINGRECONCILIATION_REQUIREDEXECUTION_ENGINE (crash/timeout)התשובה אבדה/המערכת קרסה בחלון הקריטי. לא שולחים שוב — queryStatus לפי client_order_id.
SUBMITTINGFAILEDBROKER_EVENT (reject)הברוקר דחה את הפקודה בטרם נקלטה (למשל פרמטרים לא-תקינים).
SENTWORKINGBROKER_EVENT (ORDER_ACTIVE)הברוקר אישר שהפקודה פעילה בספר הפקודות. (0.21: ACK מופרד מ-SUBMIT_ACCEPTED.) Market אינה עוברת ל-WORKING אלא למילוי.
SENTPARTIALLY_FILLEDBROKER_EVENT (fill)מילוי חלקי ראשון התקבל לפני ORDER_ACTIVE. נרשם כאירוע Fill עם fill_id ייחודי.
SENTCONFIRMEDBROKER_EVENT (fill)מילוי מלא מאומת עבור Market (או מילוי מיידי מלא).
SENTFAILEDBROKER_EVENT (reject)דחייה מפורשת או אימות אי-קיום. Timeout לבדו אינו גורם לכך.
SENTRECONCILIATION_REQUIREDEXECUTION_ENGINE (timeout)Response Timeout (response_timeout_sec מ-submit_started_at) חלף ללא מענה ודאי.
WORKINGPARTIALLY_FILLEDBROKER_EVENT (fill)מילוי חלקי התקבל. צבירה דטרמיניסטית; requested_quantity אינו משתנה; יתרות שמורות מופחתות.
WORKINGCONFIRMEDBROKER_EVENT (fill)מילוי מלא מאומת.
WORKINGEXPIREDEXECUTION_ENGINE (EOD)סגירת הבורסה (לוח בורסה) ללא מילוי (filled_quantity=0). נדרש אימות שאין יתרה פעילה.
WORKINGCANCEL_PENDINGUSER/CANCELבקשת ביטול נשלחה לברוקר עם cancel_request_id.
WORKINGRECONCILIATION_REQUIREDEXECUTION_ENGINEתוצאה לא ודאית (למשל איבוד קשר עם הברוקר בעוד הפקודה פעילה).
PARTIALLY_FILLEDPARTIALLY_FILLEDBROKER_EVENT (fill)מילוי נוסף. צבירה; כל Fill בעל fill_id ייחודי; יתרות שמורות מופחתות בכל Fill.
PARTIALLY_FILLEDCONFIRMEDBROKER_EVENT (fill/cancel/EOD)מילוי מלא, או ביטול/תפוגה מאומתים של היתרה כאשר filled_quantity>0. מקבע executed_quantity חלקית.
PARTIALLY_FILLEDEXPIREDEXECUTION_ENGINEאסור — אם filled_quantity>0 היתרה חייבת להיבדק עד CONFIRMED/RECONCILIATION. (מעבר חסום.)
PARTIALLY_FILLEDCANCEL_PENDINGUSER/CANCELבקשת ביטול יתרה פתוחה עם cancel_request_id.
PARTIALLY_FILLEDRECONCILIATION_REQUIREDEXECUTION_ENGINEאי-ודאות לגבי גורל היתרה אצל הברוקר.
CANCEL_PENDINGCANCELLEDBROKER_EVENT (cancel-ack)הברוקר אישר ביטול מלא כאשר filled_quantity=0.
CANCEL_PENDINGCONFIRMEDBROKER_EVENT (cancel-ack / fill)אישור ביטול היתרה עם filled_quantity>0, או מילוי מלא בזמן הביטול → CONFIRMED (חלקי/מלא).
CANCEL_PENDINGCANCEL_PENDINGBROKER_EVENT (partial fill)0.21: מילוי חלקי בזמן CANCEL_PENDING אינו מבטל את הביטול — נשארים ב-CANCEL_PENDING ומעדכנים את הכמות והיתרות השמורות.
CANCEL_PENDINGRECONCILIATION_REQUIREDEXECUTION_ENGINE (cancel timeout/reject)cancel_timeout_sec חלף או הברוקר דחה את הביטול. אין חזרה עיוורת — queryStatus לפי cancel_request_id/broker_order_id.
RECONCILIATION_REQUIREDCANCEL_PENDINGRECONCILIATION0.21: שאילתה החזירה שביטול עדיין ממתין אצל הברוקר (אי-הוודאות החלה בזמן CANCEL_PENDING).
RECONCILIATION_REQUIREDCANCELLEDRECONCILIATION0.21: בירור הסתיים באישור ביטול מלא ללא מילוי.
RECONCILIATION_REQUIREDWORKINGRECONCILIATIONשאילתת הברוקר החזירה: הפקודה קיימת ועדיין פעילה.
RECONCILIATION_REQUIREDPARTIALLY_FILLEDRECONCILIATION / BROKER_EVENT (late fill)שאילתה החזירה מילוי חלקי, או הגיע Fill מאוחר בזמן הבירור.
RECONCILIATION_REQUIREDCONFIRMEDRECONCILIATIONבירור הסתיים באישור תוצאה סופית ומאומתת.
RECONCILIATION_REQUIREDFAILEDRECONCILIATIONבירור הסתיים בדחייה מפורשת / אי-קיום מאומת.
RECONCILIATION_REQUIREDEXPIREDRECONCILIATIONבירור הסתיים בתפוגה מאומתת עם 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.

5 · כללים מחייבים

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 קובע תוצאות דטרמיניסטיות לבדיקות קבלה יציבות.

6 · Broker Adapter — מודל היברידי (Paper כעת, Live בעתיד)
רכיבתיאור
ממשק 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 + אישור.

7 · מדיניות Timeout — שני שעונים קנוניים (Response + Order Validity)
נושאכלל
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 — Marketmarket_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.
8 · מילוי חלקי — צבירה דטרמיניסטית + שחרור יתרות בכל Fill

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.

9 · ביטול פקודה — מסלול מצבים מלא + מירוץ Fill/ביטול (0.21)

ביטול דורש מסלול מצבים מלא: 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.

10 · Reconciliation

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.

11 · גשר Trade.status (חוזה סיום ל-CH3-002)
execution_state סופיTrade.status (דרך CH3-002)
CONFIRMEDapproved → executed (עם executed_quantity)
FAILEDapproved → rejected
EXPIREDapproved → expired (filled_quantity=0)
CANCELLEDapproved → expired (filled_quantity=0)
SUBMITTING / WORKING / PARTIALLY_FILLED / CANCEL_PENDING / RECONCILIATION_REQUIREDללא שינוי — העסקה נשארת approved
12 · Actor ו-reason_code קנוניים (UPPERCASE)
Actorמעברים מותריםתנאי/הערה
EXECUTION_ENGINENOT_SENT → SUBMITTING; SUBMITTING → RECONCILIATION_REQUIRED (crash/timeout); SENT → RECONCILIATION_REQUIRED (timeout); WORKING → EXPIRED (EOD); CANCEL_PENDING → RECONCILIATION_REQUIRED (cancel timeout)שליחה, גילוי Timeout/קריסה, תפוגה, timeout ביטול — דטרמיניסטי
BROKER_EVENTSUBMITTING → 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/CANCELWORKING → CANCEL_PENDING; PARTIALLY_FILLED → CANCEL_PENDINGבקשת ביטול ממשתמש מורשה עם cancel_request_id
RECONCILIATION (ידני/אוטומטי)RECONCILIATION_REQUIRED → CANCEL_PENDING/CANCELLED/WORKING/PARTIALLY_FILLED/CONFIRMED/FAILED/EXPIREDפתרון בירור — ידני דורש משתמש מורשה

ערכי reason_code קנוניים:

SUBMIT_STARTEDSUBMIT_ACCEPTEDORDER_ACTIVEBROKER_FILLBROKER_REJECTBROKER_ORDER_NOT_FOUNDRESPONSE_TIMEOUTCRASH_DURING_SUBMITORDER_EXPIRED_EODPARTIAL_FILL_ACCUMULATEDFILL_DURING_CANCEL_PARTIALFILL_DURING_CANCEL_FULLFILL_FINAL_CONFIRMEDFILL_CANCEL_VERIFIEDCANCEL_REQUESTEDCANCEL_CONFIRMED_EMPTYCANCEL_CONFIRMED_PARTIALCANCEL_REJECTEDCANCEL_TIMEOUTRECONCILIATION_OPENEDRECONCILIATION_CASE_OPENED_LATE_EVENTRECONCILIATION_RESOLVED_CANCEL_PENDINGRECONCILIATION_RESOLVED_CANCELLEDRECONCILIATION_RESOLVED_WORKINGRECONCILIATION_RESOLVED_PARTIALRECONCILIATION_RESOLVED_CONFIRMEDRECONCILIATION_RESOLVED_FAILEDRECONCILIATION_RESOLVED_EXPIREDLATE_FILL_VERIFIED_CORRECTIONDUPLICATE_SUBMIT_BLOCKEDPORTFOLIO_UPDATE_DEDUP_BY_FILL_IDRESERVES_RELEASED_FINALLIVE_GO_PENDING
13 · שדות סכמה ב-Trade (⚠ דורש אישור Architect)
שדהסוגתיאור
execution_state (enum)stringמורחב: NOT_SENT, SUBMITTING, SENT, WORKING, PARTIALLY_FILLED, CANCEL_PENDING, CONFIRMED, FAILED, RECONCILIATION_REQUIRED, EXPIRED, CANCELLED. כולם UPPERCASE. (⚠ דורש אישור Architect)
execution_versionnumberמונה אופטימי (CAS) ל-execution_state. מתעדכן ב-$inc רק אם תנאי הגרסה התקיים.
client_order_idstring | nullמזהה פקודה קבוע שנשלח לברוקר בכל Retry. נקבע ב-SUBMITTING ואינו משתנה. מאפשר queryStatus גם ללא broker_order_id.
cancel_request_idstring | null0.21: מזהה ייחודי לבקשת הביטול (Idempotency משלו). מאפשר queryStatus של הביטול גם לאחר Timeout.
broker_order_idstring | nullמזהה הפקודה אצל הברוקר (מוחזר ב-SUBMIT_ACCEPTED).
broker_typestring | nullאיזה Adapter טיפל (PAPER / IB / ALPACA...). חלק מה-Unique Constraint של ה-Inbox (0.21).
broker_account_idstring | nullמזהה חשבון הברוקר. חלק מה-Unique Constraint של ה-Inbox (0.21).
broker_environmentstring | nullPAPER | LIVE. כרגע PAPER בלבד; LIVE דורש Live-Go.
requested_quantitynumberהכמות המקורית. אינה משתנה לאחר שליחה.
filled_quantitynumberמצטבר הכמות שמולאה בפועל. ברירת מחדל 0.
executed_quantitynumberהכמות הסופית שבוצעה — נקבעת רק ב-CONFIRMED (מלאה או חלקית מאומתת).
average_fill_pricenumber | nullמחיר מילוי ממוצע משוקלל. null כשאין ראיה למחיר בפועל.
historical_fill_price_unknownbooleantrue עבור רשומות executed ישנות ללא ראיית מחיר ביצוע אמיתית.
last_fill_idstring | nullמזהה אירוע ה-Fill האחרון (Cache; מקור האמת ב-Inbox).
last_fill_atdate-time | nullמועד המילוי האחרון.
submit_started_atdate-time | null0.21: מועד המעבר ל-SUBMITTING — תחילת Response Timeout.
broker_accepted_atdate-time | null0.21: מועד קבלת broker_order_id (SUBMIT_ACCEPTED).
reserved_cash_usdnumber | nullמזומן שמור לפקודת קנייה פתוחה; מופחת בכל Fill והיתרה משוחררת בסיום (0.21).
reserved_sharesnumber | nullכמות מניות שמורה לפקודת מכירה פתוחה; מופחתת בכל Fill והיתרה משוחררת בסיום (0.21).
order_validitystring | nulltime_in_force קנוני: DAY (ברירת מחדל) | GTC (רק באישור מפורש).
corrective_for_trade_idstring | null0.22: מציין ש-Trade זה הוא Corrective Trade / ExecutionAdjustment ל-Trade מקורי שהגיע למצב טרמינלי (CANCELLED/EXPIRED) ואחריו התקבל Fill מאוחר מאומת. ה-Trade המקורי אינו נפתח מחדש.
original_terminal_statestring | null0.22: מצב טרמינלי של ה-Trade המקורי שה-Corrective Trade מתקן (CANCELLED/EXPIRED/CONFIRMED/FAILED). רק ב-Corrective Trade.
reconciliation_case_idstring | null0.22: מזהה Reconciliation Case שהוליד את ה-Corrective Trade (אם נוצר מאירוע מאוחר סותר טרמינלי).

אינם נשמרים ב-Trade:

expectedFrom / expectedVersion — פרמטרי קריאה (חלק מ-CAS), אינם נשמרים ב-Trade.

broker_event (גלם) — נכתב ל-Inbox (BrokerEventJournal); ב-Trade נשמרות רק נגזרות (broker_order_id, last_fill_id).

14 · מקביליות, אטומיות ו-Inbox/Outbox

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 מחזיר את אותה תוצאה — ללא שליחה/קליטה כפולה.

15 · Migration בטוח (חד-פעני, הפיך)

רשומות 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 מוכן לפני הרצה.

16 · אינטגרציה עם צינור CH3-001/002
שלבפעולה
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 משחרר רק את היתרה הנותרת.
17 · קבצים מושפעים

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, עדכון תיק חד-פעני, ואירוע מאוחר סותר טרמינלי.

18 · קריטריוני קבלה

כל שינוי 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.

19 · סדר ביצוע

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.

Locked & Approved — Version 1.0 · צא למימושהמפרט ננעל כ-Version 1.0 ב-8.8.2026. יש לצאת למימוש לפי סדר הפעולות בסעיף 19 (Paper Adapter בלבד; Live-Go נפרד), ולהגיש Ch3ExecutionReport003 עם ראיות לכל קריטריוני הקבלה. כל שינוי עתידי — גרסה חדשה + ADR.