TradeStation Webhook: Send Strategy Signals to a Trade Copier
Read this before you run any of the code below
This guide places live orders. Trading futures and other leveraged products carries substantial risk of loss and is not suitable for every investor. Past performance does not indicate future results.
Run it on a demo or evaluation account first, and keep the test offset described in step 4 switched on until you have watched a signal travel end to end. A file-watching bridge will happily send a wrong order as fast as a right one.
TradeStation has no built-in webhook. There is no "send a JSON alert to this URL" box the way there is in TradingView, and EasyLanguage has no HTTP function in the standard language. Search for a way to do it and you mostly find people asking the same question, or being pointed at the TradeStation REST API, which is for talking to TradeStation's own brokerage rather than for getting a signal out to something else.
That is a real gap, and it matters more than it used to. A TradeStation strategy that used to route straight to a TradeStation account may now need to reach a funded futures account at Topstep, Apex or anywhere else, and those accounts live on other platforms entirely.
This guide covers a route that works and has carried several thousand live trades. It was contributed by a Trade Dispensary customer who built it for his own trading, and it is published here with his agreement because the method is more useful shared than kept. The code below is his approach with a handful of changes, each one explained and each one the result of something measurable rather than a matter of taste.
Take the files rather than copying them
Both listings appear in full further down, because you should read code before you run it. If you would rather not carry several hundred lines out of a browser by hand, these are the same two files, unchanged:
Watch-Signals.ps1– the PowerShell watcher that posts each signalTradeStation-Signal-Writer.easylanguage.txt– the EasyLanguage that writes the signal file
Hand-copying is worth avoiding for a concrete reason: the version of this script that prompted the guide had a web address broken by an invisible line wrap picked up in transit, and it posted to nothing while still looking healthy. The watcher prints its own version on startup, so a support question can always be answered against a known build.
How the bridge works
Three pieces, and only the middle one is unusual:
- EasyLanguage writes a file. Your strategy builds a small JSON object
describing the trade and writes it to disk.
FileAppendis a standard EasyLanguage function, so nothing exotic is required. - PowerShell watches that folder and POSTs each new signal to Trade Dispensary's webhook endpoint on the same machine.
- Trade Dispensary copies it to whatever destination accounts you have configured, applying your own sizing and risk rules on the way.
The file is the join. It exists because EasyLanguage can write files but cannot make HTTP
requests, and PowerShell can make HTTP requests and is already on every Windows machine. Nothing
leaves your computer: the POST goes to 127.0.0.1.
On latency, the local hop is not the slow part. Measured on a normal Windows desktop, a POST from the watcher to a local listener completes in about 2 to 3 milliseconds once the first call has warmed up. The first call after starting the script takes noticeably longer, around 400 ms, because .NET initialises on first use. That is worth knowing only so you do not read it as a fault.
Step 1: create a source account in Trade Dispensary
Add a webhook source and name it something you will recognise, for example
TradeStation. The name becomes part of the URL:
http://127.0.0.1:5000/webhook/tradingview/TradeStation
Trade Dispensary accepts
/webhook/<platform>/<source_account_name>, and the generic
tradingview platform handler is the right one to use here, because the payload shape is
the same. You are not pretending to be TradingView; you are using the same webhook format.
If you run Trade Dispensary on a non-default port, that port has to match. This is the single most common way to end up with a bridge that looks perfectly healthy and delivers nothing.
Step 2: the EasyLanguage signal writer
Put your own entry logic where Condition1 is set. Everything after that builds the
JSON and writes it.
{ ===== TradeStation -> Trade Dispensary signal writer =====
Put your own entry logic where Condition1 is set. Everything below that
builds one JSON object and writes it to a file the watcher is monitoring. }
Inputs:
FileFolder("C:\TradeDispensary\"), { trailing backslash matters }
WriterTag("1S"), { UNIQUE per chart. See the note below. }
Symbol("ES"),
Side("sell"), { "buy" or "sell" }
OrderQty(1),
AutoSend(true), { false = chart only, writes nothing }
UseTestOffset(false), { true = force a far-away limit for testing }
TestLimitPrice(7750),
TestStopPrice(7730),
TestTargetPrice(7780),
ProfitAmt(3),
LossAmt(5);
Vars:
PosID(""), JsonPayload(""), FileName(""), SigSeq(0),
EntryPx(0), StopPx(0), TargetPx(0),
WantEntry(false), CondTag(""), SentThisBar(false);
{ Reset the once-per-bar latch when a new bar starts. Without this, a condition
that stays true will write a new signal on every tick of the bar. }
if BarStatus(1) = 0 then
SentThisBar = false;
{ ========== YOUR SIGNAL LOGIC ========== }
if Condition1 then begin
WantEntry = true;
CondTag = "C1-";
EntryPx = Close;
if Side = "sell" then begin
StopPx = EntryPx + LossAmt;
TargetPx = EntryPx - ProfitAmt;
end
else begin
StopPx = EntryPx - LossAmt;
TargetPx = EntryPx + ProfitAmt;
end;
end;
{ ========== END YOUR SIGNAL LOGIC ========== }
if AutoSend and WantEntry and LastBarOnChart and SentThisBar = false then begin
if UseTestOffset then begin
EntryPx = TestLimitPrice;
StopPx = TestStopPrice;
TargetPx = TestTargetPrice;
end;
if EntryPx > 0 then begin
SigSeq = SigSeq + 1;
PosID = "TS-" + WriterTag + "-" + CondTag + NumToStr(Date, 0) + "-"
+ NumToStr(Time, 0) + "-" + NumToStr(CurrentBar, 0);
{ ONE FILE PER SIGNAL. Do not write every signal to the same fixed
filename: the watcher reads the file when it gets to it, not the
instant it is written, so a second signal can overwrite the first
before anything has read it and that order is then gone with no
error anywhere. WriterTag keeps two charts from colliding and
SigSeq keeps the same bar from colliding with itself. }
FileName = FileFolder + "signal-" + WriterTag + "-"
+ NumToStr(Date, 0) + "-" + NumToStr(Time, 0) + "-"
+ NumToStr(CurrentBar, 0) + "-" + NumToStr(SigSeq, 0) + ".json";
JsonPayload = "{"
+ DoubleQuote + "symbol" + DoubleQuote + ":" + DoubleQuote + Symbol + DoubleQuote + ","
+ DoubleQuote + "action" + DoubleQuote + ":" + DoubleQuote + "entry" + DoubleQuote + ","
+ DoubleQuote + "side" + DoubleQuote + ":" + DoubleQuote + Side + DoubleQuote + ","
+ DoubleQuote + "order_type" + DoubleQuote + ":" + DoubleQuote + "limit" + DoubleQuote + ","
+ DoubleQuote + "quantity" + DoubleQuote + ":" + NumToStr(OrderQty, 0) + ","
+ DoubleQuote + "price" + DoubleQuote + ":" + NumToStr(EntryPx, 2) + ","
+ DoubleQuote + "stop_loss" + DoubleQuote + ":" + NumToStr(StopPx, 2) + ","
+ DoubleQuote + "take_profit" + DoubleQuote + ":" + NumToStr(TargetPx, 2) + ","
+ DoubleQuote + "position_id" + DoubleQuote + ":" + DoubleQuote + PosID + DoubleQuote + ","
+ DoubleQuote + "comment" + DoubleQuote + ":" + DoubleQuote + PosID + DoubleQuote
+ "}";
{ FileDelete first is a guard, not part of normal operation. The name is
already unique, so usually there is nothing to delete. It matters if a
chart is reloaded and reproduces a name it used before, because
FileAppend would otherwise append a second JSON object to the first
and the result would not parse. }
FileDelete(FileName);
FileAppend(FileName, JsonPayload);
SentThisBar = true;
WantEntry = false;
CondTag = "";
end;
end;
Two things in that listing are worth pausing on.
LastBarOnChart is true on every tick of the current bar, not once
per bar. A condition that stays true across several ticks will therefore write a new signal on every
one of them. Because each signal carries a fresh id, nothing downstream can tell those apart from
genuine separate entries, and you get a cluster of orders instead of one. The
SentThisBar latch, reset on BarStatus(1) = 0, is what holds it to one
signal per bar. If you would rather act only on a closed bar, gate the whole block on
BarStatus(1) = 2 instead.
One file per signal, and this is the single most important line in the whole guide. The obvious design is to write every signal to one fixed filename and let the watcher pick it up. Do not do it. The watcher reads the file when the handler gets around to running, not at the instant the file changes, and a second signal writing to the same name in that window overwrites the first. The first order is then gone from disk before anything ever read it, and no retry can recover it because there is nothing left to read. Measured further down.
WriterTag is what keeps two charts apart, so give every chart its own
tag. SigSeq keeps a single chart from colliding with itself. Between them the
filename is unique per signal, which is what makes the signal safe on disk until it has been sent.
The JSON it produces looks like this:
{
"symbol": "ES",
"action": "entry",
"side": "sell",
"order_type": "limit",
"quantity": 1,
"price": 7750.00,
"stop_loss": 7730.00,
"take_profit": 7780.00,
"position_id": "TS-C1-1260927-161800-42",
"comment": "TS-C1-1260927-161800-42"
}
Step 3: the watcher
Save this as Watch-Signals.ps1 in the same folder. In Notepad, set
Save as type to All Files so it does not become
Watch-Signals.ps1.txt.
# Watch-Signals.ps1 -- TradeStation -> Trade Dispensary signal bridge
#
# Watches a folder for signal*.json files and POSTs each new signal to Trade
# Dispensary's webhook endpoint.
#
# Based on a script contributed by a Trade Dispensary customer that has carried
# several thousand live trades. This version changes six things, each for a
# measured reason recorded below.
#
# The version is printed on the READY line deliberately. "The watcher is missing
# signals" cannot be answered without knowing which build produced it, and by the
# time anyone asks, the console is usually the only record that still exists.
#
# ---------------------------------------------------------------------------
# 1. SINGLE INSTANCE. This is the important one.
#
# Measured 2026-09-27: two copies of this script running at the same time
# both see the same file change and both POST it, producing two orders with
# an IDENTICAL position_id. The duplicate check further down lives in memory
# inside one process, so it cannot see what another process has sent. It is
# structurally incapable of preventing this.
#
# Starting the script twice is easy to do: a forgotten window, a second
# attempt after a typo, or a scheduled task overlapping a manual run. The
# named mutex below means the second copy refuses to start and says so.
#
# 2. ONE place to set the address. The port is a variable and the URL is built
# from it once. Previously the address was typed a second time inside the
# handler, so editing the variable at the top had no effect.
#
# 3. A FAILED POST IS RETRIED. The duplicate check records a signal as handled
# before the POST is attempted, which is deliberate and correct, because it
# closes a race. The cost is that a failed send would never be retried. This
# version retries inside the handler and clears the record if it gives up.
#
# Safe because PowerShell runs event handlers SERIALLY: measured on this
# machine, every handler ran to completion on the same thread before the
# next began. So a retry cannot interleave with another signal.
#
# 4. Port is not assumed to be 5000. Trade Dispensary honours TD_PORT, and a
# mismatched port leaves this script looking perfectly healthy while posting
# into nothing.
#
# 5. BOTH Created AND Changed are subscribed. This one prevents LOST signals and
# it replaces an earlier explanation of mine that was simply wrong.
#
# EasyLanguage writes a signal with FileDelete followed by FileAppend, so the
# file is destroyed and recreated rather than edited in place. Measured on
# this machine over 30 consecutive writes to an existing file
# (30 consecutive writes, measured):
#
# Created fired 30 / 30
# Changed fired 28 / 30
# Deleted fired 30 / 30
#
# A Changed-only watcher therefore misses roughly one signal in fifteen, with
# no error and nothing in the console - the worst possible failure for an
# entry order. Created alone is not safe either, since an in-place overwrite
# raises only Changed. Subscribing to both is what covers every write style.
#
# That also corrects the reason the debounce exists. One delete-and-append
# does NOT raise several Changed events; it raises one. The duplicate the
# debounce actually stops is Created and Changed both firing for the SAME
# write, which happened on 28 of those 30 writes. So with both events
# subscribed the same-payload guard below is load-bearing, not a precaution.
#
# 6. ONE FILE PER SIGNAL, and the file is DELETED once it has been sent.
#
# This is the most important change in the file and it is not about events at
# all. The handler reads the file when the HANDLER runs, not when the event
# fired. While one signal is being read and POSTed, a newer signal writing to
# the same fixed filename overwrites it - and the older payload is then gone
# from disk, permanently, before anything ever read it. No retry can recover
# it because there is nothing left to retry from.
#
# Measured with the original fixed-filename design, signals 600 ms apart
# (an end-to-end test against a local listener): 25 written, 18 arrived, 7 destroyed on disk. The watcher
# logged no error, because from its point of view nothing failed.
#
# Why this never bit the original author: the EasyLanguage SentThisBar latch
# allows one signal per bar, so on a single chart signals are minutes apart
# and the race never opens. It opens as soon as the same file path is used by
# more than one chart or symbol, which is the normal case for a copier.
#
# The fix is on both sides. EasyLanguage writes signal-<PosID>.json, a new
# name per signal, so nothing can be overwritten before it is read. This
# script deletes each file after a successful POST, and deliberately LEAVES it
# on disk after a failure so the signal still exists.
# ---------------------------------------------------------------------------
#
# Trading futures and other leveraged products carries substantial risk of loss
# and is not suitable for every investor. Test this on a demo or evaluation
# account before running it anywhere near funded money.
$scriptVersion = "1.1"
# ----------------------------- settings ------------------------------------
$folder = "C:\TradeDispensary" # where TradeStation writes signal*.json
$port = 5000 # must match Trade Dispensary's port
$sourceName = "TradeStation" # the source account name you created in TD
$sendRetries = 2 # attempts per signal before giving up
$retryWaitMs = 250
$uri = "http://127.0.0.1:$port/webhook/tradingview/$sourceName"
# --------------------------- single instance --------------------------------
$mutexName = "Global\TradeDispensary_WatchSignals"
$createdNew = $false
$mutex = New-Object System.Threading.Mutex($true, $mutexName, [ref]$createdNew)
if (-not $createdNew) {
Write-Host ""
Write-Host "ANOTHER COPY OF Watch-Signals.ps1 IS ALREADY RUNNING." -ForegroundColor Red
Write-Host "Two copies means every signal is sent twice, as two separate orders" -ForegroundColor Red
Write-Host "with the same id. Close the other PowerShell window first." -ForegroundColor Red
Write-Host ""
Write-Host "To find it: Get-Process powershell | Select-Object Id, StartTime" -ForegroundColor DarkGray
Write-Host ""
exit 1
}
# ------------------------------- checks ------------------------------------
if (-not (Test-Path $folder)) {
Write-Host "Folder not found: $folder" -ForegroundColor Red
$mutex.ReleaseMutex(); exit 1
}
# Confirm Trade Dispensary is actually listening before claiming to be ready.
# A watcher posting into a closed port prints nothing unusual and looks fine.
try {
$probe = Test-NetConnection -ComputerName "127.0.0.1" -Port $port -WarningAction SilentlyContinue `
-InformationLevel Quiet -ErrorAction Stop
} catch {
$probe = $null # Test-NetConnection is not on every Windows build
}
if ($probe -eq $false) {
Write-Host "WARNING: nothing is listening on port $port." -ForegroundColor Yellow
Write-Host " Start Trade Dispensary, or correct the `$port setting at the top of this file." -ForegroundColor Yellow
Write-Host " Signals will be lost until it is up." -ForegroundColor Yellow
}
# Leftover signal files from a previous session are reported but NEVER sent.
# Sending them would place orders from signals that are potentially hours old and
# based on prices that no longer exist, which is worse than missing them. The
# user is told they are there so the decision is theirs.
$stale = @(Get-ChildItem -Path $folder -Filter "signal*.json" -File -ErrorAction SilentlyContinue)
if ($stale.Count -gt 0) {
Write-Host ""
Write-Host "NOTE: $($stale.Count) signal file(s) already in $folder." -ForegroundColor Yellow
Write-Host " These are NOT sent: they may be stale, and an old signal is worse" -ForegroundColor Yellow
Write-Host " than a missed one. Delete them, or move them aside, before trading." -ForegroundColor Yellow
foreach ($s in $stale | Select-Object -First 5) {
Write-Host (" {0} last written {1}" -f $s.Name, $s.LastWriteTime) -ForegroundColor DarkGray
}
Write-Host ""
}
Write-Host "Starting up. Do not run your strategy until the green READY line appears." -ForegroundColor DarkGray
# ------------------------------- watcher -----------------------------------
$global:WatchLastJson = @{}
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $folder
$watcher.Filter = "signal*.json"
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor
[System.IO.NotifyFilters]::FileName -bor [System.IO.NotifyFilters]::Size
$watcher.IncludeSubdirectories = $false
# The handler needs the settings above, and an event action does not inherit
# script scope reliably, so bake them in as literals.
$actionText = @'
Start-Sleep -Milliseconds 15
$path = $Event.SourceEventArgs.FullPath
if (-not (Test-Path $path)) { return }
$name = [System.IO.Path]::GetFileName($path)
try {
$json = [System.IO.File]::ReadAllText($path).Trim()
if ([string]::IsNullOrWhiteSpace($json)) { return }
# Guard keyed on the FULL PATH, not just the filename. With one signal
# per file the path is unique per signal, so this suppresses only the
# extra events belonging to the same write and never a real signal.
#
# Recorded BEFORE sending, on purpose: one write raises two or three
# events within a few milliseconds, and recording first is what stops
# them becoming several orders with the same position id.
$prev = $global:WatchLastJson[$path]
if ($json -eq $prev) { return }
$global:WatchLastJson[$path] = $json
$ts = Get-Date -Format "HH:mm:ss.fff"
Write-Host "$ts $name sending..." -ForegroundColor Cyan
Write-Host $json
$sent = $false
$lastErr = ""
for ($attempt = 1; $attempt -le __RETRIES__; $attempt++) {
try {
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("Content-Type", "application/json")
$null = $wc.UploadString("__URI__", "POST", $json)
$wc.Dispose()
$sent = $true
break
}
catch {
$lastErr = $_.Exception.Message
if ($attempt -lt __RETRIES__) {
Write-Host "$ts $name attempt $attempt failed, retrying: $lastErr" -ForegroundColor Yellow
Start-Sleep -Milliseconds __RETRYWAIT__
}
}
}
if ($sent) {
Write-Host "$ts $name OK" -ForegroundColor Green
# Consume the file. Two reasons, and the second is the important one:
# it keeps the folder from filling up, and it makes a late duplicate
# event harmless because the handler finds nothing to read. Failure
# to delete is not fatal - the path guard above already covers it.
try { Remove-Item -LiteralPath $path -Force -ErrorAction Stop }
catch { Write-Host "$ts $name sent, but could not delete: $($_.Exception.Message)" -ForegroundColor DarkYellow }
}
else {
# Give up, and forget the payload so a further file event can try
# again rather than being silently swallowed as a duplicate. The file
# is deliberately LEFT ON DISK so the signal is not destroyed.
$global:WatchLastJson.Remove($path)
Write-Host "$ts $name NOT SENT after __RETRIES__ attempts: $lastErr" -ForegroundColor Red
Write-Host "$ts $name ---> THIS SIGNAL DID NOT REACH TRADE DISPENSARY <---" -ForegroundColor Red
Write-Host "$ts $name file left in place: $path" -ForegroundColor DarkYellow
}
}
catch {
$ts = Get-Date -Format "HH:mm:ss.fff"
Write-Host "$ts $name ERROR: $($_.Exception.Message)" -ForegroundColor Red
}
'@
$actionText = $actionText.Replace("__URI__", $uri).
Replace("__RETRIES__", [string]$sendRetries).
Replace("__RETRYWAIT__", [string]$retryWaitMs)
$action = [ScriptBlock]::Create($actionText)
# ALL THREE events, for the reason measured in note 5 at the top of this file.
# This looks excessive and is not. Delete-and-append is not one notification, it
# is a race between three, and which one carries readable content changes from
# signal to signal:
#
# Deleted often fires late enough that the file has ALREADY been recreated,
# so by the time the handler reads it the payload is there
# Created usually fires with the content present, but sometimes with an empty
# file, because creating and writing are separate operations
# Changed carries the content on an in-place overwrite, and on a
# delete-and-append is missing altogether about 1 time in 15
#
# Subscribing to any one of them loses signals. Measured end-to-end against this
# exact file with an end-to-end test against a local listener. The same-payload guard above is what turns the
# two or three events one write produces back into a single POST, so these are a
# matched pair: do not remove the guard.
Register-ObjectEvent $watcher "Created" -Action $action | Out-Null
Register-ObjectEvent $watcher "Changed" -Action $action | Out-Null
Register-ObjectEvent $watcher "Deleted" -Action $action | Out-Null
$watcher.EnableRaisingEvents = $true
# The ready message is printed HERE, after the watcher is actually listening,
# and not before. Printed earlier it is a lie: any signal written in the gap is
# silently lost, which is a real thing that happened during testing.
Write-Host ""
Write-Host "READY - watching signal*.json in $folder (Watch-Signals v$scriptVersion)" -ForegroundColor Green
Write-Host "POST -> $uri" -ForegroundColor DarkGray
Write-Host "Press CTRL+C to stop sending orders." -ForegroundColor DarkGray
Write-Host ""
try {
while ($true) { Start-Sleep -Seconds 1 }
}
finally {
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Get-EventSubscriber | Unregister-Event -ErrorAction SilentlyContinue
$mutex.ReleaseMutex()
$mutex.Dispose()
Write-Host "Stopped. No further signals will be sent." -ForegroundColor Yellow
}
Step 4: start it, and prove it works before it can cost anything
Open PowerShell, change to the folder, and run it:
cd C:\TradeDispensary
.\Watch-Signals.ps1
Wait for the green READY line. Do not start your strategy before it appears: anything written in the gap between launching the script and the watcher actually arming is lost silently, with no error anywhere.
Then prove the path end to end without risking a fill. Set UseTestOffset(true) in
the EasyLanguage and choose a limit price far away from the current market, so the order can rest
without being touched. You should see the watcher print the JSON and then OK, and the
order should appear on your destination account. Cancel it, set UseTestOffset back to
false, and you are connected.
To stop sending orders, press CTRL+C or close the window. Worth knowing before you need it rather than after.
The trap that silently loses orders
This is the part most worth reading. It is the failure that costs money, it does not look like a bug from the outside, and the watcher reports nothing at all when it happens.
The instinct is to write every signal to one fixed filename. We built it that way, tested it, and it loses orders. Twenty-five signals written 600 ms apart to a single filename, POSTing to a local listener that recorded everything it received:
| Design | Signals written | Orders that arrived | Destroyed on disk |
|---|---|---|---|
| One fixed filename, overwritten each time | 25 | 16 | 9 |
| One file per signal (the code above) | 25 | 25 | 0 |
Nine signals never became orders, and the watcher printed no error, because from its point of view nothing failed. It read the file, it sent what it found, and it succeeded every time. The payloads it never saw had already been overwritten by later ones. They did not fail to send, they stopped existing.
The one-file-per-signal version was then rerun at 250 ms and 100 ms spacing and still delivered 25 of 25, exactly once each.
Why this does not bite everyone immediately: the SentThisBar latch allows one signal
per bar, so on a single chart signals are minutes apart and the window never opens. It opens as soon
as the same filename is shared by more than one chart or symbol, which is the normal case for anyone
running a copier.
Which file events the watcher has to listen to
A related measurement, because it is the other way to lose a signal and the fix is not obvious.
FileDelete followed by FileAppend destroys and recreates the file rather
than editing it, so it does not raise the event you would expect. Over 30 consecutive writes to an
existing file:
| Event | Fired | If you subscribe to only this one |
|---|---|---|
Created | 30 of 30 | Misses an in-place overwrite |
Changed | 28 of 30 | Misses about 1 delete-and-append in 15 |
Deleted | 30 of 30 | Often fires after the file already has new content |
Most examples on the internet, including the one this guide started from, subscribe to
Changed alone. That quietly drops roughly one signal in fifteen. The script above
subscribes to all three, which is why it needs the same-payload guard: one write now raises two or
three events, and the guard is what collapses them back into a single order. Note where it
records the payload, which is before the POST rather than after. That ordering is what makes it
effective against events arriving in the same millisecond. The two changes only work as a pair.
The worse case is not covered by that guard at all. Two copies of the watcher running at
once will each send every signal. We measured this too: one signal, two watcher processes,
two POSTs, both carrying an identical position_id. The duplicate check lives in memory
inside one process, so it has no way to know another process has already sent the same thing.
Running it twice is easy to do by accident: a window left open behind another, a second attempt after a typo, or a scheduled task overlapping a manual run. That is why the script above takes a named mutex and refuses to start if one is already held. If you are adapting an older copy of this script, that guard is the single most valuable thing to add to it.
If you ever see two orders with the same id, check for a second PowerShell window before you suspect your strategy or your copier:
Get-Process powershell | Select-Object Id, StartTime, MainWindowTitle
When a send fails
PowerShell runs these event handlers one at a time, in sequence, on a single thread. We confirmed that by timing them: every handler ran to completion before the next began. Two consequences follow.
The good one is that a retry is safe. The handler can attempt the POST twice without any risk of interleaving with the next signal, which is why the script above retries rather than dropping a failed send, and clears its record if it gives up so a later file event can try again.
The cost is that a retry blocks the queue. A refused connection on localhost takes roughly 2.2 seconds to fail, so two attempts against a Trade Dispensary that is not running will stall the handler for about four and a half seconds, and any signal arriving in that window waits. That is an acceptable trade when the alternative is losing a signal silently, but it is a reason to keep the retry count low rather than generous.
When a send does fail for good, the script says so in red and states plainly that the signal did not arrive. Silence would be worse: a bridge that fails quietly is indistinguishable from a strategy that simply did not fire.
What this route does not do
Being clear about the limits is more useful than overselling it.
- It is one-directional. Signals go from TradeStation outward. Nothing reports fills back into TradeStation, so your chart will not know what happened downstream.
- It sends intent, not fills. The signal is written when your condition is met. Whether the destination order fills, and at what price, is a separate matter handled by the copier and the destination platform.
- It is local. Both TradeStation and Trade Dispensary need to be on the same machine, or the URL needs to point somewhere reachable, which brings its own security questions worth thinking about carefully before you open anything up.
- The file is the state. If the folder is not writable, or another program has the file open, the signal does not get written and nothing downstream will notice.
Where it goes from there
Once signals reach Trade Dispensary, TradeStation is just another source. The same signal can be copied to several accounts at once, with per-account sizing, and to destinations on entirely different platforms. If your accounts are funded futures evaluations, the prop firm copy trading rules are worth reading first, because copying between your own accounts and copying somebody else's signals are treated very differently by most firms. For the drawdown side of running several accounts from one signal, see drawdown protection.
One thing that is easy to miss when a single source starts driving several accounts: the positions are correlated by construction. Three accounts on one signal is not diversification, it is the same trade three times, and the drawdown arrives everywhere together.
Test on a demo or evaluation account before running this anywhere near funded money. Trading futures carries substantial risk of loss and is not suitable for every investor. Nothing here is a recommendation to trade, and past performance does not indicate future results.
Frequently asked questions
Does TradeStation support webhooks?
No. TradeStation has no built-in webhook feature, and EasyLanguage has no function for making HTTP requests, so a strategy cannot POST to a URL on its own. The TradeStation REST API exists but it is designed for interacting with TradeStation's own brokerage rather than for sending signals out to third-party software. The usual workaround is to have EasyLanguage write a small file with FileAppend and have a separate local program watch that folder and make the HTTP request, which is the method described in this guide.
Can you send TradeStation signals to Topstep or another funded futures account?
Yes, indirectly. TradeStation cannot place orders on a Topstep account, because that account lives on a different platform. What you can do is export the signal from TradeStation and have a trade copier place the order on the destination account. Check your firm's rules first: as of 2026 most futures prop firms permit copying between your own accounts, while copying somebody else's signals into your account is restricted or banned almost everywhere.
Why am I getting duplicate orders from a TradeStation file bridge?
The most likely cause is two copies of the watcher script running at the same time. Each one sees the same file change and sends its own request, producing two orders with an identical position id. A duplicate check inside the script cannot prevent this, because it only knows what its own process has sent, which is why the script in this guide takes a named mutex and refuses to start twice. The other cause is subscribing to several file events without a same-payload guard, because one write raises two or three events. Check for a second PowerShell window before suspecting your strategy or your copier.
Why does my TradeStation file bridge miss some signals?
Almost always because every signal is written to the same filename. The watcher reads the file when its handler runs rather than at the instant the file changes, so a second signal can overwrite the first before anything has read it, and that order is then gone from disk with no error logged anywhere. Writing 25 signals 600 milliseconds apart to one fixed filename delivered 16 and destroyed 9 in testing; giving each signal its own filename delivered all 25. The second cause is subscribing only to the Changed event, which is missing on roughly one delete-and-append write in fifteen.
How much delay does a file-based signal bridge add?
Very little on the local hop. Measured on a normal Windows desktop, the POST from the watcher to a listener on the same machine completes in roughly 2 to 3 milliseconds once the first request has warmed up .NET, with that first request taking around 400 milliseconds. The meaningful delays in the chain are elsewhere: how quickly your strategy evaluates, and how quickly the destination broker acknowledges the order.
Do I need to keep the PowerShell window open?
Yes. The window is the process that is watching the folder, so closing it stops signals being sent. That is also the intended emergency stop: pressing CTRL+C or closing the window immediately halts any further orders going out, which is worth knowing before you need it.
Tags:
Share this post
Related Posts
Continue learning about copy trading with these related articles: