add launcher

This commit is contained in:
2026-09-17 15:10:46 +08:00
parent 1962d3f8a7
commit a2b0bba05f
10 changed files with 778 additions and 3 deletions

1
.gitignore vendored
View File

@@ -22,6 +22,7 @@ logs/
# Go workspace file
go.work
go.work.sum
setup.txt
# env file
.env

21
launcher/build.bat Normal file
View File

@@ -0,0 +1,21 @@
@echo off
setlocal
where go.exe >nul 2>&1
if errorlevel 1 (
echo Go was not found in PATH.
exit /b 1
)
pushd "%~dp0" || exit /b 1
go build -trimpath -ldflags "-s -w -H=windowsgui" -o start.exe start.go
set "BUILD_EXIT=%errorlevel%"
popd
if not "%BUILD_EXIT%"=="0" (
echo Failed to build start.exe.
exit /b %BUILD_EXIT%
)
echo Built "%~dp0start.exe" successfully.
exit /b 0

View File

@@ -0,0 +1,99 @@
; ==============================================================================
; 国金证券 QMT 自动登录脚本 (精准识别版)
; 适用版本: 2.0.8.300 | 窗口类名: Qt5QWindowIcon
; ==============================================================================
#Requires AutoHotkey v2.0
#SingleInstance Force
#Warn All, Off
; ---------- 核心配置区 ----------
global QMT_PATH := "D:\gjqmt\bin.x64\XtItClient.exe" ; 程序路径自己填
global USERNAME := EnvGet("QMT_ACCOUNT_ID") ; 账号自己填
global PASSWORD := EnvGet("QMT_ACCOUNT_PWD") ; 密码自己填
; 精准窗口识别特征
global QMT_CLASS := "ahk_class Qt5QWindowIcon" ; 自己抓 国金不用管 理论所有程序都可以
global QMT_EXE := "ahk_exe XtItClient.exe" ; 自己抓 国金不用管 理论所有程序都可以
global QMT_TITLE := QMT_CLASS . " " . QMT_EXE ; 不用管
; 强制等待时间60秒
global FORCE_WAIT_MS := 60000
; ------------------------------
if (USERNAME = "" or PASSWORD = "")
{
MsgBox("❌ QMT_ACQMT_PWD 未配置")
ExitApp()
}
; 1. 环境清理:确保没有残留进程
if ProcessExist("XtItClient.exe") {
ProcessClose("XtItClient.exe")
Sleep(2000)
}
; 2. 以管理员权限启动 QMT
try {
Run(QMT_PATH, "", "runas")
} catch Error as e {
MsgBox("启动失败!请检查路径是否正确:`n" . QMT_PATH)
ExitApp()
}
; 3. 等待窗口初步加载
; 只要检测到窗口类名 Qt5QWindowIcon 出现,立即启动 30 秒计时
if !WinWait(QMT_TITLE, , 30) {
MsgBox("❌ QMT 窗口在 30 秒内未启动,流程终止。")
ExitApp()
}
; 4. 【强制等待】精准 30 秒倒计时
startTime := A_TickCount
while (A_TickCount - startTime < FORCE_WAIT_MS) {
remaining := Ceil((FORCE_WAIT_MS - (A_TickCount - startTime)) / 1000)
ToolTip("已识别 QMT 窗口,强制等待中... 剩余 " . remaining . " 秒开始输入")
Sleep(100)
}
ToolTip() ; 清除倒计时
; 5. 精准激活并执行登录
if WinExist(QMT_TITLE) {
; 强制置顶并激活
WinActivate(QMT_TITLE)
if !WinWaitActive(QMT_TITLE, , 5) {
ToolTip("无法激活 QMT 窗口,请手动点击窗口")
Sleep(2000)
ToolTip()
}
; 锁定键鼠,确保输入不被干扰
BlockInput(true)
; --- 填写账号 ---
; 针对 Qt 界面,全选后 Backspace 是最稳妥的清理方式
Send("^a{Backspace}")
Sleep(2000)
SendText(USERNAME)
Sleep(2000)
; --- 切换到密码框 ---
Send("{Tab}")
Sleep(2000)
; --- 填写密码 ---
Send("^a{Backspace}")
Sleep(2000)
SendText(PASSWORD)
Sleep(2000)
; --- 回车登录 ---
Send("{Enter}")
BlockInput(false) ; 解锁键鼠
}
; 6. 任务完成
ToolTip("✅ 30秒强制等待结束登录流程已完成")
SetTimer () => ToolTip(), -5000
; 脚本保持运行。若需登录后自动退出,取消下方注释
ExitApp()

35
launcher/setup.bat Normal file
View File

@@ -0,0 +1,35 @@
@echo off
setlocal
set "TASK_NAME=qmt_launcher"
set "LAUNCHER_EXE=%~dp0start.exe"
if not exist "%LAUNCHER_EXE%" (
call "%~dp0build.bat"
if errorlevel 1 (
echo Failed to prepare start.exe.
pause
exit /b 1
)
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command ^
"$ErrorActionPreference = 'Stop';" ^
"$taskName = '%TASK_NAME%';" ^
"$launcherDir = (Resolve-Path -LiteralPath '%~dp0').Path;" ^
"$workingDir = Split-Path -Parent $launcherDir;" ^
"$launcherExe = Join-Path $launcherDir 'start.exe';" ^
"$action = New-ScheduledTaskAction -Execute $launcherExe -WorkingDirectory $workingDir;" ^
"$trigger = New-ScheduledTaskTrigger -AtLogOn -User ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name);" ^
"$principal = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Highest;" ^
"$settings = New-ScheduledTaskSettingsSet -Hidden:$false;" ^
"Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null"
if errorlevel 1 (
echo Failed to create task "%TASK_NAME%".
pause
exit /b 1
)
echo Task "%TASK_NAME%" created or updated successfully with a visible window.
pause
exit /b 0

300
launcher/start.go Normal file
View File

@@ -0,0 +1,300 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"unsafe"
)
const (
collectorURL = "http://139.224.247.176:13499/collector.exe"
statusURL = "http://127.0.0.1:5000/status"
qmtStartupDelay = 5 * time.Minute
statusInterval = 1 * time.Minute
restartCooldown = 15 * time.Minute
failureThreshold = 3
httpTimeout = 5 * time.Second
createNewProcessGroup = 0x00000200
errorAlreadyExists = syscall.Errno(183)
mutexName = `Local\qmt_launcher_single_instance`
)
func acquireSingleInstance() (syscall.Handle, error) {
kernel32 := syscall.NewLazyDLL("kernel32.dll")
createMutex := kernel32.NewProc("CreateMutexW")
name, err := syscall.UTF16PtrFromString(mutexName)
if err != nil {
return 0, err
}
handle, _, callErr := createMutex.Call(0, 0, uintptr(unsafe.Pointer(name)))
if handle == 0 {
return 0, fmt.Errorf("CreateMutexW failed: %v", callErr)
}
if callErr == errorAlreadyExists {
_ = syscall.CloseHandle(syscall.Handle(handle))
return 0, fmt.Errorf("another launcher instance is already running")
}
return syscall.Handle(handle), nil
}
var (
launcherDir string
projectRoot string
collectorPath string
ahkScript string
runScript string
httpClient = &http.Client{Timeout: httpTimeout}
)
func initPaths() error {
executable, err := os.Executable()
if err != nil {
return fmt.Errorf("locate launcher executable: %w", err)
}
executable, err = filepath.EvalSymlinks(executable)
if err != nil {
return fmt.Errorf("resolve launcher executable: %w", err)
}
launcherDir = filepath.Dir(executable)
projectRoot = filepath.Dir(launcherDir)
collectorPath = filepath.Join(projectRoot, "bin", "collector.exe")
ahkScript = filepath.Join(launcherDir, "qmt_auto_login.ahk")
runScript = filepath.Join(projectRoot, "run.bat")
return nil
}
func configureLogging() (*os.File, error) {
logFile, err := os.OpenFile(filepath.Join(launcherDir, "start.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
return nil, err
}
log.SetOutput(logFile)
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
return logFile, nil
}
func downloadCollector() error {
if info, err := os.Stat(collectorPath); err == nil && !info.IsDir() {
log.Printf("INFO collector.exe already exists: %s", collectorPath)
return nil
} else if err != nil && !os.IsNotExist(err) {
return err
}
if err := os.MkdirAll(filepath.Dir(collectorPath), 0o755); err != nil {
return err
}
temporaryPath := collectorPath + ".download"
log.Printf("INFO Downloading collector.exe from %s", collectorURL)
response, err := httpClient.Get(collectorURL)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("download returned HTTP %d", response.StatusCode)
}
output, err := os.Create(temporaryPath)
if err != nil {
return err
}
_, copyErr := io.Copy(output, response.Body)
closeErr := output.Close()
if copyErr != nil || closeErr != nil {
_ = os.Remove(temporaryPath)
if copyErr != nil {
return copyErr
}
return closeErr
}
info, err := os.Stat(temporaryPath)
if err != nil || info.Size() == 0 {
_ = os.Remove(temporaryPath)
if err != nil {
return err
}
return fmt.Errorf("downloaded collector.exe is empty")
}
if err := os.Rename(temporaryPath, collectorPath); err != nil {
_ = os.Remove(temporaryPath)
return err
}
log.Printf("INFO collector.exe downloaded: %s", collectorPath)
return nil
}
func launchQMTLogin() error {
if _, err := os.Stat(ahkScript); err != nil {
return fmt.Errorf("AHK script not found: %s", ahkScript)
}
log.Printf("INFO Launching %s", ahkScript)
shell32 := syscall.NewLazyDLL("shell32.dll")
shellExecute := shell32.NewProc("ShellExecuteW")
verb, _ := syscall.UTF16PtrFromString("open")
file, _ := syscall.UTF16PtrFromString(ahkScript)
result, _, callErr := shellExecute.Call(0, uintptr(unsafe.Pointer(verb)), uintptr(unsafe.Pointer(file)), 0, 0, 1)
if result <= 32 {
return fmt.Errorf("ShellExecuteW failed (%d): %v", result, callErr)
}
return nil
}
func launchRunScript() error {
if _, err := os.Stat(runScript); err != nil {
return fmt.Errorf("run.bat not found: %s", runScript)
}
log.Printf("INFO Launching %s", runScript)
command := exec.Command("cmd.exe", "/c", runScript)
command.Dir = projectRoot
command.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNewProcessGroup}
return command.Start()
}
func restartSystem() error {
if err := launchQMTLogin(); err != nil {
return err
}
log.Printf("INFO Waiting %d seconds for QMT login", int(qmtStartupDelay/time.Second))
time.Sleep(qmtStartupDelay)
if err := launchRunScript(); err != nil {
return err
}
time.Sleep(statusInterval)
return nil
}
func systemIsHealthy() bool {
response, err := httpClient.Get(statusURL)
if err != nil {
log.Printf("WARNING Status check failed: %v", err)
return false
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
log.Printf("WARNING Status endpoint returned HTTP %d", response.StatusCode)
return false
}
body, err := io.ReadAll(response.Body)
if err != nil {
return false
}
bodyString := strings.ReplaceAll(string(body), "Null", "\"\"")
bodyString = strings.ReplaceAll(bodyString, "NULL", "\"\"")
bodyString = strings.ReplaceAll(bodyString, "NONE", "\"\"")
bodyString = strings.ReplaceAll(bodyString, "None", "\"\"")
bodyString = strings.ReplaceAll(bodyString, "Nil", "\"\"")
var payload struct {
Status map[string]any `json:"status"`
}
if err := json.Unmarshal([]byte(bodyString), &payload); err != nil {
log.Printf("WARNING Status check failed: %v, %s", err, bodyString)
return false
}
healthy := payload.Status != nil && payload.Status["qmt_status"] == "connected"
if !healthy {
log.Printf("WARNING System is not fully started: %v", payload.Status)
}
return healthy
}
func isTradingTime(now time.Time) bool {
weekday := now.Weekday()
if weekday == time.Saturday || weekday == time.Sunday {
return false
}
hhmm := now.Hour()*100 + now.Minute()
return (hhmm >= 910 && hhmm <= 1130) || (hhmm >= 1240 && hhmm <= 1500)
}
func run() error {
log.Print("INFO ==================================================")
log.Print("INFO QMT launcher supervisor started")
if err := downloadCollector(); err != nil {
return err
}
consecutiveFailures := 0
var lastRestart time.Time
wasOutsideTradingTime := false
for {
if !isTradingTime(time.Now()) {
if !wasOutsideTradingTime {
log.Print("INFO Outside trading hours; skipping health check and startup")
}
wasOutsideTradingTime = true
consecutiveFailures = 0
time.Sleep(statusInterval)
continue
}
if wasOutsideTradingTime {
log.Print("INFO Trading hours started; resuming health checks")
wasOutsideTradingTime = false
}
if systemIsHealthy() {
consecutiveFailures = 0
time.Sleep(statusInterval)
continue
}
consecutiveFailures++
if consecutiveFailures < failureThreshold {
log.Printf("WARNING Health check failed %d/%d; waiting before restart", consecutiveFailures, failureThreshold)
time.Sleep(statusInterval)
continue
}
if remaining := restartCooldown - time.Since(lastRestart); !lastRestart.IsZero() && remaining > 0 {
log.Printf("WARNING Restart suppressed by cooldown; retrying in approximately %s", remaining.Round(time.Second))
time.Sleep(statusInterval)
continue
}
log.Printf("WARNING Health check failed %d consecutive times; restarting system", consecutiveFailures)
if err := restartSystem(); err != nil {
return err
}
lastRestart = time.Now()
consecutiveFailures = 0
time.Sleep(statusInterval)
}
}
func main() {
instanceMutex, err := acquireSingleInstance()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
defer syscall.CloseHandle(instanceMutex)
if err := initPaths(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
logFile, err := configureLogging()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer logFile.Close()
if err := run(); err != nil {
log.Printf("ERROR Launcher failed: %v", err)
os.Exit(1)
}
}

264
launcher/start.log Normal file
View File

@@ -0,0 +1,264 @@
2026-08-13 15:22:43,135 INFO QMT launcher supervisor started
2026-08-13 15:22:43,135 INFO Downloading collector.exe from http://139.224.247.176/collector.exe
2026-08-13 15:22:43,244 ERROR Launcher cycle failed
Traceback (most recent call last):
File "D:\work\quant\qmt-v2\launcher\start.py", line 107, in main
download_collector()
File "D:\work\quant\qmt-v2\launcher\start.py", line 47, in download_collector
with urlopen(COLLECTOR_URL, timeout=HTTP_TIMEOUT_SECONDS) as response:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\devapps\Python311\Lib\urllib\request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\devapps\Python311\Lib\urllib\request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "D:\devapps\Python311\Lib\urllib\request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "D:\devapps\Python311\Lib\urllib\request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "D:\devapps\Python311\Lib\urllib\request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "D:\devapps\Python311\Lib\urllib\request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
2026-08-13 15:26:45,007 INFO QMT launcher supervisor started
2026-08-13 15:26:45,008 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026-08-13 15:26:45,008 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026-08-13 15:26:45,106 INFO Waiting 300 seconds for QMT login
2026-08-13 21:53:04,684 INFO QMT launcher supervisor started
2026-08-13 21:53:04,686 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026-08-13 21:53:04,686 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026-08-13 21:53:06,267 INFO Waiting 180 seconds for QMT login
2026-08-13 22:01:06,389 INFO QMT launcher supervisor started
2026-08-13 22:01:06,389 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026-08-13 22:01:06,389 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026-08-13 22:01:06,463 INFO Waiting 180 seconds for QMT login
2026-08-13 22:04:06,464 INFO Launching D:\work\quant\qmt-v2\run.bat
2026-08-13 23:02:15,722 INFO ==================================================
2026-08-13 23:02:15,722 INFO QMT launcher supervisor started
2026-08-13 23:02:15,722 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026-08-13 23:02:17,756 WARNING Status check failed: <urlopen error [WinError 10061] 由于目标计算机积极拒绝,无法连接。>
2026-08-13 23:02:17,756 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026-08-13 23:02:17,837 INFO Waiting 300 seconds for QMT login
2026-08-13 23:07:17,838 INFO Launching D:\work\quant\qmt-v2\run.bat
2026-08-13 23:27:05,964 INFO ==================================================
2026-08-13 23:27:05,965 INFO QMT launcher supervisor started
2026-08-13 23:27:05,965 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026-08-13 23:27:08,034 WARNING Status check failed: <urlopen error [WinError 10061] 由于目标计算机积极拒绝,无法连接。>
2026-08-13 23:27:08,035 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026-08-13 23:27:08,118 INFO Waiting 300 seconds for QMT login
2026/08/13 23:45:13.608527 INFO ==================================================
2026/08/13 23:45:13.622235 INFO QMT launcher supervisor started
2026/08/13 23:45:13.622235 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/13 23:45:13.623496 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/13 23:45:13.624026 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/13 23:45:13.703711 INFO Waiting 300 seconds for QMT login
2026/08/13 23:50:13.704349 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/13 23:52:13.715253 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/13 23:52:13.715407 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/13 23:52:13.767285 INFO Waiting 300 seconds for QMT login
2026/08/14 08:48:36.427941 INFO ==================================================
2026/08/14 08:48:36.961109 INFO QMT launcher supervisor started
2026/08/14 08:48:37.692841 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/14 08:48:37.769022 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/14 08:48:38.177308 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/14 08:48:39.449445 INFO Waiting 300 seconds for QMT login
2026/08/14 08:53:39.559317 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/14 08:55:39.569610 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/14 08:55:39.569610 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/14 08:55:39.622915 INFO Waiting 300 seconds for QMT login
2026/08/14 09:00:39.623224 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/14 09:02:39.640363 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/14 09:02:39.640363 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/14 09:02:39.687327 INFO Waiting 300 seconds for QMT login
2026/08/15 08:50:00.885570 INFO ==================================================
2026/08/15 08:50:01.657003 INFO QMT launcher supervisor started
2026/08/15 08:50:01.659591 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/15 08:50:01.662166 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/15 08:50:01.894393 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/15 08:50:04.507973 INFO Waiting 300 seconds for QMT login
2026/08/16 08:52:26.025434 INFO ==================================================
2026/08/16 08:52:26.104316 INFO QMT launcher supervisor started
2026/08/16 08:52:26.125470 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/16 08:52:26.127520 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/16 08:52:26.129067 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/16 08:52:27.204828 INFO Waiting 300 seconds for QMT login
2026/08/16 08:57:27.234137 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/17 07:48:38.513724 INFO ==================================================
2026/08/17 07:48:38.824692 INFO QMT launcher supervisor started
2026/08/17 07:48:38.856254 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/17 07:48:38.875302 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/17 07:48:39.231263 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/17 07:48:40.845492 INFO Waiting 300 seconds for QMT login
2026/08/17 07:53:40.940886 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/17 09:25:42.472480 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/17 09:25:42.476066 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/17 09:25:42.698749 INFO Waiting 300 seconds for QMT login
2026/08/17 09:30:42.699596 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/17 09:32:42.711957 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/17 09:32:42.712191 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/17 09:32:42.762591 INFO Waiting 300 seconds for QMT login
2026/08/17 09:37:42.763160 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/17 09:40:42.778731 WARNING Status check failed: invalid character 'N' looking for beginning of value
2026/08/17 09:40:42.779241 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/17 09:40:42.832414 INFO Waiting 300 seconds for QMT login
2026/08/18 08:03:14.885643 INFO ==================================================
2026/08/18 08:03:15.353370 INFO QMT launcher supervisor started
2026/08/18 08:03:15.758480 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/18 08:03:16.301320 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/18 08:03:16.385159 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/18 08:03:17.010169 INFO Waiting 300 seconds for QMT login
2026/08/18 08:08:17.204987 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/18 08:10:17.215378 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/18 08:10:17.215747 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/18 08:10:17.258153 INFO Waiting 300 seconds for QMT login
2026/08/18 08:15:17.258601 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/18 13:58:18.503095 WARNING Status check failed: invalid character 'N' looking for beginning of value
2026/08/18 13:58:18.528903 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/18 13:58:19.022561 INFO Waiting 300 seconds for QMT login
2026/08/18 13:59:24.123137 INFO ==================================================
2026/08/18 13:59:24.136062 INFO QMT launcher supervisor started
2026/08/18 13:59:24.136062 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/18 13:59:24.139154 WARNING Status check failed: invalid character 'N' looking for beginning of value
2026/08/18 13:59:24.139154 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/18 13:59:24.222221 INFO Waiting 300 seconds for QMT login
2026/08/18 14:03:19.023203 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/18 14:04:24.222305 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/18 14:05:19.039743 WARNING Status check failed: invalid character 'N' looking for beginning of value
2026/08/18 14:05:19.039889 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/18 14:05:19.091765 INFO Waiting 300 seconds for QMT login
2026/08/18 16:50:14.766362 INFO ==================================================
2026/08/18 16:50:14.813222 INFO QMT launcher supervisor started
2026/08/18 16:50:14.829377 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/18 16:50:15.043111 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/18 16:50:15.239254 WARNING Health check failed 1/3; waiting before restart
2026/08/18 16:51:15.239641 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/18 16:51:15.240155 WARNING Health check failed 2/3; waiting before restart
2026/08/18 16:52:15.241716 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/18 16:52:15.241716 WARNING Health check failed 3 consecutive times; restarting system
2026/08/18 16:52:15.242314 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/18 16:52:15.333484 INFO Waiting 300 seconds for QMT login
2026/08/18 16:57:15.334509 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/19 19:22:16.079187 INFO ==================================================
2026/08/19 19:22:16.773161 INFO QMT launcher supervisor started
2026/08/19 19:22:16.891082 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/19 19:22:16.941701 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/19 19:22:17.669752 WARNING Health check failed 1/3; waiting before restart
2026/08/19 19:23:17.696305 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/19 19:23:17.696812 WARNING Health check failed 2/3; waiting before restart
2026/08/19 19:24:17.698348 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/19 19:24:17.698348 WARNING Health check failed 3 consecutive times; restarting system
2026/08/19 19:24:17.699493 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/19 19:24:17.778244 INFO Waiting 300 seconds for QMT login
2026/08/19 19:29:17.787076 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/20 00:46:19.533672 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/20 00:46:19.557647 WARNING Health check failed 1/3; waiting before restart
2026/08/20 00:47:19.558550 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/20 00:47:19.558550 WARNING Health check failed 2/3; waiting before restart
2026/08/20 00:48:19.561081 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/20 00:48:19.561171 WARNING Health check failed 3 consecutive times; restarting system
2026/08/20 00:48:19.562250 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/20 00:48:20.393672 INFO Waiting 300 seconds for QMT login
2026/08/20 00:53:20.394245 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/20 00:55:20.405666 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/20 00:55:20.405666 WARNING Health check failed 1/3; waiting before restart
2026/08/20 09:04:29.885549 INFO ==================================================
2026/08/20 09:04:30.104470 INFO QMT launcher supervisor started
2026/08/20 09:04:30.188385 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/20 09:04:30.296629 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/20 09:04:30.904740 WARNING Health check failed 1/3; waiting before restart
2026/08/20 09:05:30.933442 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/20 09:05:30.933442 WARNING Health check failed 2/3; waiting before restart
2026/08/20 09:06:30.935437 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/20 09:06:30.935437 WARNING Health check failed 3 consecutive times; restarting system
2026/08/20 09:06:30.936235 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/20 09:06:31.015171 INFO Waiting 300 seconds for QMT login
2026/08/20 09:11:31.019034 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/20 18:38:33.643883 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/20 18:38:33.644433 WARNING Health check failed 1/3; waiting before restart
2026/08/20 18:39:33.645042 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/20 18:39:33.645042 WARNING Health check failed 2/3; waiting before restart
2026/08/20 18:40:33.645183 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/20 18:40:33.645183 WARNING Health check failed 3 consecutive times; restarting system
2026/08/20 18:40:33.645183 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/20 18:40:33.708064 INFO Waiting 300 seconds for QMT login
2026/08/21 07:43:26.860445 INFO ==================================================
2026/08/21 07:43:26.900291 INFO QMT launcher supervisor started
2026/08/21 07:43:26.900802 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/21 07:43:26.900802 INFO Outside trading hours; skipping health check and startup
2026/08/21 07:56:42.689005 INFO ==================================================
2026/08/21 07:56:42.956200 INFO QMT launcher supervisor started
2026/08/21 07:56:43.003770 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/21 07:56:43.098420 INFO Outside trading hours; skipping health check and startup
2026/08/21 09:10:43.146562 INFO Trading hours started; resuming health checks
2026/08/21 09:10:43.151689 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/21 09:10:43.152235 WARNING Health check failed 1/3; waiting before restart
2026/08/21 09:11:43.153756 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/21 09:11:43.153892 WARNING Health check failed 2/3; waiting before restart
2026/08/21 09:12:43.155121 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/21 09:12:43.155121 WARNING Health check failed 3 consecutive times; restarting system
2026/08/21 09:12:43.158139 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/21 09:12:43.602943 INFO Waiting 300 seconds for QMT login
2026/08/21 09:17:43.603673 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/21 09:41:43.700827 WARNING Status check failed: invalid character 'N' looking for beginning of value
2026/08/21 09:41:43.701040 WARNING Health check failed 1/3; waiting before restart
2026/08/21 09:42:43.705588 WARNING Status check failed: invalid character 'N' looking for beginning of value
2026/08/21 09:42:43.705588 WARNING Health check failed 2/3; waiting before restart
2026/08/21 09:43:43.708222 WARNING Status check failed: invalid character 'N' looking for beginning of value
2026/08/21 09:43:43.708222 WARNING Health check failed 3 consecutive times; restarting system
2026/08/21 09:43:43.708222 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/21 09:43:43.755418 INFO Waiting 300 seconds for QMT login
2026/08/24 07:34:46.696720 INFO ==================================================
2026/08/24 07:34:46.886535 INFO QMT launcher supervisor started
2026/08/24 07:34:47.102305 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/24 07:34:47.117274 INFO Outside trading hours; skipping health check and startup
2026/08/25 07:26:35.556191 INFO ==================================================
2026/08/25 07:26:35.588196 INFO QMT launcher supervisor started
2026/08/25 07:26:35.597019 INFO collector.exe already exists: D:\work\quant\qmt-v2\bin\collector.exe
2026/08/25 07:26:35.598275 INFO Outside trading hours; skipping health check and startup
2026/08/25 09:10:35.669830 INFO Trading hours started; resuming health checks
2026/08/25 09:10:35.796782 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:10:35.798360 WARNING Health check failed 1/3; waiting before restart
2026/08/25 09:11:35.798720 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:11:35.798720 WARNING Health check failed 2/3; waiting before restart
2026/08/25 09:12:35.800607 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:12:35.800607 WARNING Health check failed 3 consecutive times; restarting system
2026/08/25 09:12:35.802484 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/25 09:12:36.274607 INFO Waiting 300 seconds for QMT login
2026/08/25 09:17:36.276883 INFO Launching D:\work\quant\qmt-v2\run.bat
2026/08/25 09:19:36.305826 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:19:36.305826 WARNING Health check failed 1/3; waiting before restart
2026/08/25 09:20:36.306951 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:20:36.306951 WARNING Health check failed 2/3; waiting before restart
2026/08/25 09:21:36.308254 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:21:36.308254 WARNING Restart suppressed by cooldown; retrying in approximately 12m0s
2026/08/25 09:22:36.312490 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:22:36.312490 WARNING Restart suppressed by cooldown; retrying in approximately 11m0s
2026/08/25 09:23:36.313957 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:23:36.313957 WARNING Restart suppressed by cooldown; retrying in approximately 10m0s
2026/08/25 09:24:36.315175 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:24:36.315175 WARNING Restart suppressed by cooldown; retrying in approximately 9m0s
2026/08/25 09:25:36.317724 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:25:36.317758 WARNING Restart suppressed by cooldown; retrying in approximately 8m0s
2026/08/25 09:26:36.318252 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:26:36.318252 WARNING Restart suppressed by cooldown; retrying in approximately 7m0s
2026/08/25 09:27:36.320154 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:27:36.320237 WARNING Restart suppressed by cooldown; retrying in approximately 6m0s
2026/08/25 09:28:36.322021 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:28:36.322021 WARNING Restart suppressed by cooldown; retrying in approximately 5m0s
2026/08/25 09:29:36.323197 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:29:36.323354 WARNING Restart suppressed by cooldown; retrying in approximately 4m0s
2026/08/25 09:30:36.323848 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:30:36.323848 WARNING Restart suppressed by cooldown; retrying in approximately 3m0s
2026/08/25 09:31:36.325264 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:31:36.325264 WARNING Restart suppressed by cooldown; retrying in approximately 2m0s
2026/08/25 09:32:36.326993 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:32:36.327058 WARNING Restart suppressed by cooldown; retrying in approximately 1m0s
2026/08/25 09:33:36.328252 WARNING Status check failed: Get "http://127.0.0.1:5000/status": dial tcp 127.0.0.1:5000: connectex: No connection could be made because the target machine actively refused it.
2026/08/25 09:33:36.328252 WARNING Health check failed 15 consecutive times; restarting system
2026/08/25 09:33:36.328757 INFO Launching D:\work\quant\qmt-v2\launcher\qmt_auto_login.ahk
2026/08/25 09:33:36.384905 INFO Waiting 300 seconds for QMT login

28
launcher/start_test.go Normal file
View File

@@ -0,0 +1,28 @@
package main
import (
"reflect"
"testing"
)
func TestNormalizeJSONEmptyValues(t *testing.T) {
input := map[string]any{
"json_null": nil,
"nil_text": "nil",
"null_text": " NULL ",
"normal": "connected",
"nested": []any{nil, "NIL", 1.0},
}
want := map[string]any{
"json_null": "",
"nil_text": "",
"null_text": "",
"normal": "connected",
"nested": []any{"", "", 1.0},
}
got := normalizeJSONEmptyValues(input)
if !reflect.DeepEqual(got, want) {
t.Fatalf("normalizeJSONEmptyValues() = %#v, want %#v", got, want)
}
}

View File

@@ -1,6 +1,6 @@
account_id: 8889399698
host_key: liao
buy_value: 5000
buy_value: 10000
min_cash_ratio: 0.10
grid_step_pct: 1
strategy: trend

View File

@@ -1,11 +1,13 @@
"""策略单次运行所需的公共上下文对象。"""
import logging
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from dataclasses import dataclass, field
from config import AccountConfig, GlobalConfig
from config import AccountConfig, GlobalConfig, HTTP_TIMEOUT
from sdk import Client
from libs.grid_take_profit import GridTrailingTracker
from libs.http import get_json
from libs.order import OrderBook
from libs.watch import DipWatch
@@ -22,3 +24,23 @@ class Runtime:
add_watch: DipWatch
profit_tracker: GridTrailingTracker
executor: ThreadPoolExecutor | None = None
server_inital: dict[str, list[str]] = field(init=False)
def __post_init__(self) -> None:
"""读取服务端初始化数据,失败时保留空证券列表。"""
self.server_inital = {"all_codes": [], "today_pass_codes": []}
try:
url = f"{self.global_cfg.api_host.rstrip('/')}/a/initial"
payload = get_json(url, HTTP_TIMEOUT)
if not isinstance(payload, dict) or str(payload.get("code")) != "0":
raise ValueError(f"服务端初始化响应失败: {payload!r}")
data = payload.get("data")
if not isinstance(data, dict):
raise ValueError("服务端初始化 data 必须为对象")
for key in ("all_codes", "today_pass_codes"):
codes = data.get(key)
if not isinstance(codes, list) or not all(isinstance(code, str) for code in codes):
raise ValueError(f"服务端初始化 {key} 必须为字符串列表")
self.server_inital = data
except Exception:
logging.exception("[初始化] 获取服务端初始化数据失败")

View File

@@ -166,6 +166,11 @@ def handle_loss(
return TradeDecision(False, f"次数无效:{add_num}")
if pnl_rate > LOSS_TIERS[add_num]:
return TradeDecision(False)
all_codes = runtime.server_inital.get("all_codes") or []
if position.stock_code not in all_codes:
return TradeDecision(False, "不在服务端初始化股票池内,禁止补仓")
if tick.last_price > 200 or position.market_value >= 20_000:
return TradeDecision(False, "价格或仓位市值超过补仓限制")
if not runtime.add_watch.triggered("补仓", position.stock_code, tick.last_price):