<#
SimNow front-end selector -- picks which front set to use, based on Beijing time,
optionally probing TCP reachability first.
Rule (same as README "local test config"):
- Trading hours (Beijing, Mon-Fri):
day 08:55-15:15, night 20:55-02:35
-> set 1 "same as live trading" (preferred whenever both work)
- Otherwise -> set 2 "7x24"
Usage (works on Windows PowerShell 5.1 and PowerShell 7):
powershell -ExecutionPolicy Bypass -File scripts/simnow-fronts.ps1
powershell -ExecutionPolicy Bypass -File scripts/simnow-fronts.ps1 -Probe
powershell -ExecutionPolicy Bypass -File scripts/simnow-fronts.ps1 -Env
powershell -ExecutionPolicy Bypass -File scripts/simnow-fronts.ps1 -Force 724
NOTE: this file is intentionally ASCII-only. Windows PowerShell 5.1 decodes
.ps1 files as ANSI unless they carry a UTF-8 BOM, which corrupts non-ASCII
string literals and breaks the parser. Keep it ASCII.
SimNow is closed on weekends/holidays; the 7x24 set is also unavailable
between Saturday 12:00 and Sunday 16:00 (the script warns about that).
#>
[CmdletBinding()]
param(
[string]$Config = '',
[ValidateSet('live', '724')][string]$Force,
[switch]$Probe,
[switch]$Env
)
$ErrorActionPreference = 'Stop'
# $PSScriptRoot is EMPTY while param() defaults are being bound when this script is
# started with -File from another PowerShell host -> the documented invocation used to
# die on "Join-Path: Cannot bind argument to parameter 'Path'". Resolve it here instead.
if ([string]::IsNullOrEmpty($Config)) {
$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
$Config = Join-Path $scriptDir '..\config\simnow.local.env'
}
# ---------- read KEY=VALUE config (force UTF-8 for PS 5.1) ----------
if (-not (Test-Path -LiteralPath $Config)) {
Write-Error "config file not found: $Config (start from config/simnow.example.env)"
exit 2
}
$kv = @{}
foreach ($line in (Get-Content -LiteralPath $Config -Encoding UTF8)) {
$t = $line.Trim()
if ($t -eq '' -or $t.StartsWith('#')) { continue }
$i = $t.IndexOf('=')
if ($i -lt 1) { continue }
$kv[$t.Substring(0, $i).Trim()] = $t.Substring($i + 1).Trim()
}
function Get-Cfg([string]$k, [string]$d = '') {
if ($kv.ContainsKey($k) -and $kv[$k] -ne '') { return $kv[$k] }
return $d
}
# ---------- Beijing time (fixed +08:00, matches ek-ctp D-TZ) ----------
$bj = [datetime]::UtcNow.AddHours(8)
# Trading window: day 08:55-15:15, night 20:55-02:35, Mon-Fri
function Test-TradingWindow([datetime]$t) {
if ($t.DayOfWeek -eq [DayOfWeek]::Saturday -or $t.DayOfWeek -eq [DayOfWeek]::Sunday) { return $false }
$hm = $t.Hour * 60 + $t.Minute
return (($hm -ge 535 -and $hm -le 915) -or ($hm -ge 1255 -or $hm -le 155))
}
# TCP reachability probe (tcp://host:port, 1.5s timeout)
function Test-Front([string]$addr) {
if ($addr -notmatch '^tcp://(?<h>[^:]+):(?<p>\d+)$') { return $false }
$h = $Matches['h']
$p = [int]$Matches['p']
$c = New-Object System.Net.Sockets.TcpClient
try {
$iar = $c.BeginConnect($h, $p, $null, $null)
if (-not $iar.AsyncWaitHandle.WaitOne(1500)) { return $false }
$c.EndConnect($iar)
return $true
} catch {
return $false
} finally {
$c.Close()
}
}
# ---------- config values ----------
$liveTd = Get-Cfg 'EK_CTP_LIVE_TD'
$liveMd = Get-Cfg 'EK_CTP_LIVE_MD'
$bakTd = Get-Cfg 'EK_CTP_LIVE_TD_BAK'
$bakMd = Get-Cfg 'EK_CTP_LIVE_MD_BAK'
$p724Td = Get-Cfg 'EK_CTP_724_TD'
$p724Md = Get-Cfg 'EK_CTP_724_MD'
$inWindow = Test-TradingWindow $bj
$probeLines = @()
# ---------- decide ----------
$pick = ''
$reason = ''
if ($Force -ne '') {
$pick = $Force
$reason = "forced by -Force $Force"
} elseif ($Probe) {
$liveTdOk = Test-Front $liveTd
$liveMdOk = Test-Front $liveMd
$bakTdOk = $false
if ($bakTd -ne '') { $bakTdOk = Test-Front $bakTd }
$bakMdOk = $false
if ($bakMd -ne '') { $bakMdOk = Test-Front $bakMd }
$p724TdOk = Test-Front $p724Td
$p724MdOk = Test-Front $p724Md
$probeLines += "probe set1 trade $liveTd -> $(if ($liveTdOk) { 'UP' } else { 'down' })"
$probeLines += "probe set1 md $liveMd -> $(if ($liveMdOk) { 'UP' } else { 'down' })"
if ($bakTd -ne '') { $probeLines += "probe set1 trade(bak) $bakTd -> $(if ($bakTdOk) { 'UP' } else { 'down' })" }
if ($bakMd -ne '') { $probeLines += "probe set1 md(bak) $bakMd -> $(if ($bakMdOk) { 'UP' } else { 'down' })" }
$probeLines += "probe set2 trade $p724Td -> $(if ($p724TdOk) { 'UP' } else { 'down' })"
$probeLines += "probe set2 md $p724Md -> $(if ($p724MdOk) { 'UP' } else { 'down' })"
$liveOk = ($liveTdOk -or $bakTdOk)
$p724Ok = $p724TdOk
if ($liveOk) {
$pick = 'live'
$reason = 'probe: set1 (live-aligned) reachable, preferred'
} elseif ($p724Ok) {
$pick = '724'
$reason = 'probe: set1 unreachable, set2 (7x24) reachable'
} else {
if ($inWindow) { $pick = 'live' } else { $pick = '724' }
$reason = 'probe: neither reachable (outside service window?), fell back to time rule'
}
} elseif ($inWindow) {
$pick = 'live'
$reason = 'inside trading window, prefer set1 (live-aligned)'
} else {
$pick = '724'
$reason = 'outside trading window, use set2 (7x24)'
}
if ($pick -eq 'live') {
$td = $liveTd
$md = $liveMd
$tdBak = $bakTd
$mdBak = $bakMd
$setName = 'set1 (live-aligned)'
} else {
$td = $p724Td
$md = $p724Md
$tdBak = ''
$mdBak = ''
$setName = 'set2 (7x24)'
}
if ($Env) {
"EK_CTP_TD_ADDR=$td"
"EK_CTP_MD_ADDR=$md"
exit 0
}
# ---------- report ----------
"beijing time : $($bj.ToString('yyyy-MM-dd HH:mm:ss')) (day-of-week $([int]$bj.DayOfWeek))"
"trading hours : $(if ($inWindow) { 'yes' } else { 'no' })"
"selected : $setName"
"reason : $reason"
"td front : $td"
"md front : $md"
if ($tdBak -ne '') { "td front (bak): $tdBak" }
if ($mdBak -ne '') { "md front (bak): $mdBak" }
"flow dir : $(Get-Cfg 'EK_CTP_FLOW_DIR' 'target/ctp-flow')"
foreach ($l in $probeLines) { $l }
# 7x24 set service window: trading day 16:00-next 09:00 / non-trading day 16:00-next 12:00
if ($pick -eq '724') {
$hm = $bj.Hour * 60 + $bj.Minute
$weekendGap = ($bj.DayOfWeek -eq [DayOfWeek]::Saturday -and $hm -gt 720) -or ($bj.DayOfWeek -eq [DayOfWeek]::Sunday -and $hm -lt 960)
if ($weekendGap) {
"warning : between Sat 12:00 and Sun 16:00, SimNow is normally closed"
}
}
"note : SimNow provides snapshot quotes only (no tick-by-tick); closed on weekends/holidays"