Your bot can repeat a trade after a restart even when the first request succeeded. My offline restart check created 1 order intent, then 0 when the same signal came back. The useful part was the second result: the code held the signal for reconciliation instead of preparing another buy.
The starting question was a real stock-platform brief: finish the existing software so it can execute trades reliably. Before adding another strategy rule, I would look at the gap between sending a request and saving its result. That gap can survive a perfectly good backtest.
The missing record tells you less than you think
Imagine your program sends an order. The broker accepts it, but the connection drops before your program receives the answer. Or the answer arrives and the process stops before the local write finishes. After restart, your database contains no completed order. That does not tell you whether the broker accepted it.
A retry can now produce another trade. The dangerous assumption is that no local success record means no external action. A manual position and an automated order whose answer was lost also need different treatment. I would keep an unknown state separate from both, with a clear route to investigate it.
Save the chosen identity first
My example uses SQLite and a stable signal ID. It saves the ID before returning an order intent. The ID is the table's primary key, so the database rejects a second reservation even after the process closes and reopens.
Here is the actual preparation method from the checked example:
def prepare(self, order_id, symbol, quantity):
if not order_id or symbol not in {"AAPL", "MSFT"} or type(quantity) is not int or quantity <= 0:
return "rejected", None
try:
with self.db:
self.db.execute("INSERT INTO orders VALUES (?,?,?,?)", (order_id, symbol, quantity, "pending_reconciliation"))
except sqlite3.IntegrityError:
return "held_for_reconciliation", None
return "prepared", {"id": order_id, "symbol": symbol, "quantity": quantity, "side": "buy", "mode": "offline_only"}
The surrounding class creates the table with id TEXT PRIMARY KEY and commits that setup. It validates the selected symbol and positive integer quantity before reserving an ID. An invalid request therefore does not consume an identity that a corrected request still needs.
What the four checks showed
The first check prepared an AAPL buy intent. The second closed the database connection, reopened the same file and presented the same signal again. It returned held_for_reconciliation and no intent. The third rejected a zero quantity. The fourth corrected that rejected request and prepared it successfully. All four checks passed.
That last pair matters. Reserving an ID before checking the quantity can leave a valid correction blocked by a request that never should have reached the send step. Validation belongs before the reservation. The reservation belongs before an external request can leave the process.
A database hold still needs a recovery path
This small example deliberately stops at preparation. A real broker integration needs to query the original client order ID, order status and fills. It also needs a policy for pending signals that remain unresolved. Generating a new random ID after each timeout would defeat the duplicate hold even though the database still works correctly.
I would never turn an unknown acknowledgement into an automatic resend. I would first recover what happened to the original order. If the broker gives a definitive rejection, the next permitted state can follow that result. If the answer remains uncertain, the hold needs to remain visible rather than disappear when the process restarts.
In five minutes, compare the signal ID before a restart with the ID used afterwards, then check whether a repeated pending ID creates another order intent. Use your program's offline path and inspect the actual returned state. A fresh ID on every retry is a reason to investigate before connecting that retry to an account.
If your trading bot has a similar state problem, the free bug check starts with the relevant source and the expected result.
These four local checks cover chosen inputs and SQLite persistence, without broker submission, fills or a live account.

Top comments (1)
The ordering you chose (validate, then reserve, then send) is right, and it leaves one case the four checks can't show: a crash after the INSERT commits but before the request leaves the process. The row says pending_reconciliation, the broker has never heard of the order, and the hold on restart then blocks a trade that was never sent. That is the safe failure for a duplicate, but it is a silent miss, so reconciliation needs a "broker has no order with this client ID" branch that is allowed to release or resend.
That is also where the stable ID pays twice. If you pass the same ID to the broker as its client order ID, a resend after a lost acknowledgement is deduplicated on their side, and you can look the order up by that ID instead of guessing.
Two smaller things from reading prepare() (not run):
type(quantity) is not intrejects numpy ints coming out of a position-sizing calculation, and if the signal ID is derived from symbol plus date, a legitimate second signal the same day lands in held_for_reconciliation. Would a regression check for the second case belong in the four?