From f7dce1a004549505b016cba4f8e5a14c94bf34e6 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 21 May 2026 20:12:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A4=8D=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_binance/app.py | 21 +- crypto_monitor_gate/app.py | 21 +- crypto_monitor_gate_bot/app.py | 21 +- crypto_monitor_okx/app.py | 21 +- deploy/setup_env.ps1 | 420 ++++++++++++++++----------------- history_window_lib.py | 22 ++ 6 files changed, 288 insertions(+), 238 deletions(-) diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 5919b12..9848d12 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -59,8 +59,10 @@ from history_window_lib import ( PRESET_UTC_LAST7D, PRESET_UTC_TODAY, list_window_redirect_query, + normalize_bj_datetime_storage, resolve_list_window, resolve_window, + sql_list_time_field, utc_window_to_bj_sql_strings, ) @@ -5653,9 +5655,9 @@ def render_main_page(page="trade"): order_list = [] for o in raw_order_list: order_list.append(enrich_order_item(row_to_dict(o), current_capital)) + tr_ts = sql_list_time_field("closed_at", "created_at", "opened_at") raw_records = conn.execute( - "SELECT * FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? " - "AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id DESC LIMIT 1000", + f"SELECT * FROM trade_records WHERE {tr_ts} >= ? AND {tr_ts} <= ? ORDER BY id DESC LIMIT 1000", (start_bj, end_bj), ).fetchall() records = [to_effective_trade_dict(r) for r in raw_records] @@ -6834,8 +6836,8 @@ def export_trade_records(): "margin_capital,leverage,pnl_amount,hold_seconds,hold_minutes,planned_rr,actual_rr,risk_amount," "opened_at,closed_at,result,miss_reason,entry_reason,reviewed_entry_reason," "exchange_realized_pnl,exchange_opened_at,exchange_closed_at,created_at " - "FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? " - "AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id ASC", + f"FROM trade_records WHERE {sql_list_time_field('closed_at', 'created_at', 'opened_at')} >= ? " + f"AND {sql_list_time_field('closed_at', 'created_at', 'opened_at')} <= ? ORDER BY id ASC", (start_bj, end_bj), ).fetchall() conn.close() @@ -7239,7 +7241,12 @@ def add_journal(): new_trade_while_occupied, note, image) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", ( - entry_id, d.get("open_datetime"), d.get("close_datetime"), hold_duration, d.get("coin"), d.get("tf"), + entry_id, + normalize_bj_datetime_storage(d.get("open_datetime")), + normalize_bj_datetime_storage(d.get("close_datetime")), + hold_duration, + d.get("coin"), + d.get("tf"), d.get("pnl"), entry_reason_norm, exit_reason_stored, d.get("expect_rr"), real_rr_text, early_exit_raw, early_exit_reason_saved, early_exit_trigger, early_exit_note, None, None, None, mood_issues, @@ -7261,9 +7268,9 @@ def api_journals(): win = _list_window_from_request() start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ) conn = get_db() + j_ts = sql_list_time_field("close_datetime", "created_at", "open_datetime") rows = conn.execute( - "SELECT * FROM journal_entries WHERE COALESCE(close_datetime, created_at, open_datetime) >= ? " - "AND COALESCE(close_datetime, created_at, open_datetime) <= ? ORDER BY created_at DESC LIMIT 500", + f"SELECT * FROM journal_entries WHERE {j_ts} >= ? AND {j_ts} <= ? ORDER BY created_at DESC LIMIT 500", (start_bj, end_bj), ).fetchall() conn.close() diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 5a4f1ef..e0552d8 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -60,8 +60,10 @@ from history_window_lib import ( PRESET_UTC_LAST7D, PRESET_UTC_TODAY, list_window_redirect_query, + normalize_bj_datetime_storage, resolve_list_window, resolve_window, + sql_list_time_field, utc_window_to_bj_sql_strings, ) @@ -5632,9 +5634,9 @@ def render_main_page(page="trade"): exchange_pnl_sync = sync_trade_records_from_exchange(conn) or {} except Exception as e: exchange_pnl_sync = {"ok": False, "reason": str(e)} + tr_ts = sql_list_time_field("closed_at", "created_at", "opened_at") raw_records = conn.execute( - "SELECT * FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? " - "AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id DESC LIMIT 1000", + f"SELECT * FROM trade_records WHERE {tr_ts} >= ? AND {tr_ts} <= ? ORDER BY id DESC LIMIT 1000", (start_bj, end_bj), ).fetchall() records = [to_effective_trade_dict(r) for r in raw_records] @@ -6879,8 +6881,8 @@ def export_trade_records(): "margin_capital,leverage,pnl_amount,hold_seconds,hold_minutes,planned_rr,actual_rr,risk_amount," "opened_at,closed_at,result,miss_reason,entry_reason,reviewed_entry_reason," "exchange_realized_pnl,exchange_opened_at,exchange_closed_at,created_at " - "FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? " - "AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id ASC", + f"FROM trade_records WHERE {sql_list_time_field('closed_at', 'created_at', 'opened_at')} >= ? " + f"AND {sql_list_time_field('closed_at', 'created_at', 'opened_at')} <= ? ORDER BY id ASC", (start_bj, end_bj), ).fetchall() conn.close() @@ -7265,7 +7267,12 @@ def add_journal(): new_trade_while_occupied, note, image) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", ( - entry_id, d.get("open_datetime"), d.get("close_datetime"), hold_duration, d.get("coin"), d.get("tf"), + entry_id, + normalize_bj_datetime_storage(d.get("open_datetime")), + normalize_bj_datetime_storage(d.get("close_datetime")), + hold_duration, + d.get("coin"), + d.get("tf"), d.get("pnl"), entry_reason_norm, exit_reason_stored, d.get("expect_rr"), real_rr_text, early_exit_raw, early_exit_reason_saved, early_exit_trigger, early_exit_note, None, None, None, mood_issues, @@ -7287,9 +7294,9 @@ def api_journals(): win = _list_window_from_request() start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ) conn = get_db() + j_ts = sql_list_time_field("close_datetime", "created_at", "open_datetime") rows = conn.execute( - "SELECT * FROM journal_entries WHERE COALESCE(close_datetime, created_at, open_datetime) >= ? " - "AND COALESCE(close_datetime, created_at, open_datetime) <= ? ORDER BY created_at DESC LIMIT 500", + f"SELECT * FROM journal_entries WHERE {j_ts} >= ? AND {j_ts} <= ? ORDER BY created_at DESC LIMIT 500", (start_bj, end_bj), ).fetchall() conn.close() diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 6841e35..5e5f331 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -40,8 +40,10 @@ from history_window_lib import ( PRESET_UTC_LAST7D, PRESET_UTC_TODAY, list_window_redirect_query, + normalize_bj_datetime_storage, resolve_list_window, resolve_window, + sql_list_time_field, utc_window_to_bj_sql_strings, ) @@ -5213,8 +5215,8 @@ def render_main_page(page="trade"): pass if page in ("records", "plan_history"): raw_records = conn.execute( - "SELECT * FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? " - "AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id DESC LIMIT 2000", + f"SELECT * FROM trade_records WHERE {sql_list_time_field('closed_at', 'created_at', 'opened_at')} >= ? " + f"AND {sql_list_time_field('closed_at', 'created_at', 'opened_at')} <= ? ORDER BY id DESC LIMIT 2000", (start_bj, end_bj), ).fetchall() else: @@ -6542,8 +6544,8 @@ def export_trade_records(): "pnl_amount,hold_seconds,hold_minutes,opened_at,closed_at,result,miss_reason," "entry_reason,reviewed_entry_reason,created_at,trend_plan_id,exchange_realized_pnl," "exchange_opened_at,exchange_closed_at,exchange_sync_key FROM trade_records " - "WHERE COALESCE(closed_at, created_at, opened_at) >= ? " - "AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id ASC", + f"WHERE {sql_list_time_field('closed_at', 'created_at', 'opened_at')} >= ? " + f"AND {sql_list_time_field('closed_at', 'created_at', 'opened_at')} <= ? ORDER BY id ASC", (start_bj, end_bj), ).fetchall() conn.close() @@ -6916,7 +6918,12 @@ def add_journal(): new_trade_while_occupied, note, image) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", ( - entry_id, d.get("open_datetime"), d.get("close_datetime"), hold_duration, d.get("coin"), d.get("tf"), + entry_id, + normalize_bj_datetime_storage(d.get("open_datetime")), + normalize_bj_datetime_storage(d.get("close_datetime")), + hold_duration, + d.get("coin"), + d.get("tf"), d.get("pnl"), entry_reason_norm, exit_reason_stored, d.get("expect_rr"), real_rr_text, early_exit_raw, early_exit_reason_saved, early_exit_trigger, early_exit_note, None, None, None, mood_issues, @@ -6939,8 +6946,8 @@ def api_journals(): start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ) conn = get_db() rows = conn.execute( - "SELECT * FROM journal_entries WHERE COALESCE(close_datetime, created_at, open_datetime) >= ? " - "AND COALESCE(close_datetime, created_at, open_datetime) <= ? ORDER BY created_at DESC LIMIT 500", + f"SELECT * FROM journal_entries WHERE {sql_list_time_field('close_datetime', 'created_at', 'open_datetime')} >= ? " + f"AND {sql_list_time_field('close_datetime', 'created_at', 'open_datetime')} <= ? ORDER BY created_at DESC LIMIT 500", (start_bj, end_bj), ).fetchall() conn.close() diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index e2c3cd0..b45a454 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -59,8 +59,10 @@ from history_window_lib import ( PRESET_UTC_LAST7D, PRESET_UTC_TODAY, list_window_redirect_query, + normalize_bj_datetime_storage, resolve_list_window, resolve_window, + sql_list_time_field, utc_window_to_bj_sql_strings, ) @@ -4166,8 +4168,8 @@ def render_main_page(page="trade"): for o in raw_order_list: order_list.append(enrich_order_item(row_to_dict(o), current_capital)) raw_records = conn.execute( - "SELECT * FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? " - "AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id DESC LIMIT 1000", + f"SELECT * FROM trade_records WHERE {sql_list_time_field('closed_at', 'created_at', 'opened_at')} >= ? " + f"AND {sql_list_time_field('closed_at', 'created_at', 'opened_at')} <= ? ORDER BY id DESC LIMIT 1000", (start_bj, end_bj), ).fetchall() records = [to_effective_trade_dict(r) for r in raw_records] @@ -5139,8 +5141,8 @@ def export_trade_records(): "SELECT id,symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit," "margin_capital,leverage,pnl_amount,hold_seconds,hold_minutes,planned_rr,actual_rr,risk_amount," "opened_at,closed_at,result,miss_reason,entry_reason,reviewed_entry_reason,created_at " - "FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? " - "AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id ASC", + f"FROM trade_records WHERE {sql_list_time_field('closed_at', 'created_at', 'opened_at')} >= ? " + f"AND {sql_list_time_field('closed_at', 'created_at', 'opened_at')} <= ? ORDER BY id ASC", (start_bj, end_bj), ).fetchall() conn.close() @@ -5502,7 +5504,12 @@ def add_journal(): new_trade_while_occupied, note, image) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", ( - entry_id, d.get("open_datetime"), d.get("close_datetime"), hold_duration, d.get("coin"), d.get("tf"), + entry_id, + normalize_bj_datetime_storage(d.get("open_datetime")), + normalize_bj_datetime_storage(d.get("close_datetime")), + hold_duration, + d.get("coin"), + d.get("tf"), d.get("pnl"), entry_reason_norm, exit_reason_stored, d.get("expect_rr"), real_rr_text, early_exit_raw, early_exit_reason_saved, early_exit_trigger, early_exit_note, None, None, None, mood_issues, @@ -5525,8 +5532,8 @@ def api_journals(): start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ) conn = get_db() rows = conn.execute( - "SELECT * FROM journal_entries WHERE COALESCE(close_datetime, created_at, open_datetime) >= ? " - "AND COALESCE(close_datetime, created_at, open_datetime) <= ? ORDER BY created_at DESC LIMIT 500", + f"SELECT * FROM journal_entries WHERE {sql_list_time_field('close_datetime', 'created_at', 'open_datetime')} >= ? " + f"AND {sql_list_time_field('close_datetime', 'created_at', 'open_datetime')} <= ? ORDER BY created_at DESC LIMIT 500", (start_bj, end_bj), ).fetchall() conn.close() diff --git a/deploy/setup_env.ps1 b/deploy/setup_env.ps1 index b6f87c4..6fd6c34 100644 --- a/deploy/setup_env.ps1 +++ b/deploy/setup_env.ps1 @@ -1,210 +1,210 @@ -#Requires -Version 5.1 -<# -.SYNOPSIS - crypto_monitor 一键环境部署(Windows PowerShell) - -.DESCRIPTION - - 为各子项目创建 Python venv 并安装依赖 - - 从 .env.example 复制 .env(不覆盖已有) - - 创建 static/images 等运行时目录 - - 可选安装 PM2(需已安装 Node.js) - -.EXAMPLE - .\deploy\setup_env.ps1 - .\deploy\setup_env.ps1 -Only binance,gate_bot - .\deploy\setup_env.ps1 -SkipPm2 -#> -param( - [string]$Only = "all", - [switch]$SkipPm2, - [switch]$SkipEnvCopy, - [switch]$RecreateVenv -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -$DeployDir = $PSScriptRoot -$RepoRoot = (Resolve-Path (Join-Path $DeployDir "..")).Path -$ReqFile = Join-Path $RepoRoot "requirements.txt" -$HubReqFile = Join-Path $RepoRoot "manual_trading_hub\requirements.txt" - -$MonitorProjects = @( - @{ Key = "binance"; Dir = "crypto_monitor_binance" }, - @{ Key = "gate"; Dir = "crypto_monitor_gate" }, - @{ Key = "gate_bot"; Dir = "crypto_monitor_gate_bot" }, - @{ Key = "okx"; Dir = "crypto_monitor_okx" } -) -$HubProject = @{ Key = "hub"; Dir = "manual_trading_hub" } - -function Write-Step([string]$Msg) { - Write-Host "" - Write-Host "==> $Msg" -ForegroundColor Cyan -} - -function Test-Python310 { - $py = Get-Command python -ErrorAction SilentlyContinue - if (-not $py) { - throw "未找到 python。请安装 Python 3.10+ 并加入 PATH:https://www.python.org/downloads/" - } - $verText = & python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" - $parts = $verText.Trim() -split "\." - $major = [int]$parts[0] - $minor = [int]$parts[1] - if ($major -lt 3 -or ($major -eq 3 -and $minor -lt 10)) { - throw "需要 Python 3.10+,当前: $verText" - } - Write-Host "Python: $(python --version 2>&1)" -ForegroundColor DarkGray -} - -function Should-Include([string]$Key, [string[]]$Selected) { - if ($Selected -contains "all") { return $true } - return $Selected -contains $Key -} - -function Setup-MonitorProject([hashtable]$Proj) { - $projPath = Join-Path $RepoRoot $Proj.Dir - if (-not (Test-Path $projPath)) { - Write-Host " 跳过(目录不存在): $($Proj.Dir)" -ForegroundColor Yellow - return - } - Write-Step "$($Proj.Dir)" - Push-Location $projPath - try { - $venvDir = Join-Path $projPath ".venv" - $venvPy = Join-Path $venvDir "Scripts\python.exe" - $venvPip = Join-Path $venvDir "Scripts\pip.exe" - - if ($RecreateVenv -and (Test-Path $venvDir)) { - Write-Host " 删除旧 venv ..." - Remove-Item -Recurse -Force $venvDir - } - if (-not (Test-Path $venvPy)) { - Write-Host " 创建 venv ..." - & python -m venv .venv - } - - Write-Host " 升级 pip ..." - & $venvPy -m pip install -U pip setuptools wheel -q - - Write-Host " 安装依赖 (requirements.txt) ..." - & $venvPip install -r $ReqFile -q - - if (-not $SkipEnvCopy) { - $envExample = Join-Path $projPath ".env.example" - $envFile = Join-Path $projPath ".env" - if ((Test-Path $envExample) -and -not (Test-Path $envFile)) { - Copy-Item $envExample $envFile - Write-Host " 已复制 .env.example -> .env" -ForegroundColor Green - } elseif (Test-Path $envFile) { - Write-Host " 保留已有 .env" -ForegroundColor DarkGray - } else { - Write-Host " 无 .env.example,请手动配置 .env" -ForegroundColor Yellow - } - } - - $staticDirs = @( - "static\images", - "static\images\order_charts" - ) - foreach ($d in $staticDirs) { - $full = Join-Path $projPath $d - if (-not (Test-Path $full)) { - New-Item -ItemType Directory -Path $full -Force | Out-Null - } - } - Write-Host " 完成: $venvPy" -ForegroundColor Green - } finally { - Pop-Location - } -} - -function Setup-HubProject() { - $projPath = Join-Path $RepoRoot $HubProject.Dir - if (-not (Test-Path $projPath)) { - Write-Host " 跳过 hub(目录不存在)" -ForegroundColor Yellow - return - } - Write-Step $HubProject.Dir - Push-Location $projPath - try { - $venvDir = Join-Path $projPath ".venv" - $venvPy = Join-Path $venvDir "Scripts\python.exe" - $venvPip = Join-Path $venvDir "Scripts\pip.exe" - - if ($RecreateVenv -and (Test-Path $venvDir)) { - Remove-Item -Recurse -Force $venvDir - } - if (-not (Test-Path $venvPy)) { - & python -m venv .venv - } - & $venvPy -m pip install -U pip setuptools wheel -q - if (Test-Path $HubReqFile) { - & $venvPip install -r $HubReqFile -q - } - - if (-not $SkipEnvCopy) { - $envExample = Join-Path $projPath ".env.example" - $envFile = Join-Path $projPath ".env" - if ((Test-Path $envExample) -and -not (Test-Path $envFile)) { - Copy-Item $envExample $envFile - Write-Host " 已复制 .env.example -> .env" -ForegroundColor Green - } - } - Write-Host " 完成: $venvPy" -ForegroundColor Green - } finally { - Pop-Location - } -} - -function Install-Pm2IfNeeded() { - if ($SkipPm2) { return } - $node = Get-Command node -ErrorAction SilentlyContinue - if (-not $node) { - Write-Host "未检测到 Node.js,跳过 PM2。安装 Node 后执行: npm install -g pm2" -ForegroundColor Yellow - return - } - Write-Step "PM2(可选进程托管)" - $pm2 = Get-Command pm2 -ErrorAction SilentlyContinue - if ($pm2) { - Write-Host " PM2 已安装: $(pm2 -v)" -ForegroundColor Green - return - } - Write-Host " 正在全局安装 pm2 ..." - & npm install -g pm2 - Write-Host " PM2 安装完成。在各子目录执行: pm2 start ecosystem.config.cjs" -ForegroundColor Green -} - -# --- main --- -Write-Host "crypto_monitor 环境部署" -ForegroundColor White -Write-Host "仓库根目录: $RepoRoot" -ForegroundColor DarkGray - -if (-not (Test-Path $ReqFile)) { - throw "缺少 $ReqFile" -} - -Test-Python310 - -$selected = ($Only -split "[,;\s]+" | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ }) -if (-not $selected -or $selected.Count -eq 0) { $selected = @("all") } - -foreach ($p in $MonitorProjects) { - if (Should-Include $p.Key $selected) { - Setup-MonitorProject $p - } -} -if (Should-Include $HubProject.Key $selected) { - Setup-HubProject -} - -Install-Pm2IfNeeded - -Write-Host "" -Write-Host "部署完成。下一步:" -ForegroundColor Green -Write-Host " 1. 编辑各子目录 .env(API Key、密码、代理等)" -Write-Host " 2. 启动示例(Binance):" -Write-Host " cd crypto_monitor_binance" -Write-Host " .\.venv\Scripts\activate" -Write-Host " python app.py" -Write-Host " 3. Linux 服务器可用: bash deploy/setup_env.sh" -Write-Host "" +#Requires -Version 5.1 +<# +.SYNOPSIS + crypto_monitor 一键环境部署(Windows PowerShell) + +.DESCRIPTION + - 为各子项目创建 Python venv 并安装依赖 + - 从 .env.example 复制 .env(不覆盖已有) + - 创建 static/images 等运行时目录 + - 可选安装 PM2(需已安装 Node.js) + +.EXAMPLE + .\deploy\setup_env.ps1 + .\deploy\setup_env.ps1 -Only binance,gate_bot + .\deploy\setup_env.ps1 -SkipPm2 +#> +param( + [string]$Only = "all", + [switch]$SkipPm2, + [switch]$SkipEnvCopy, + [switch]$RecreateVenv +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +$DeployDir = $PSScriptRoot +$RepoRoot = (Resolve-Path (Join-Path $DeployDir "..")).Path +$ReqFile = Join-Path $RepoRoot "requirements.txt" +$HubReqFile = Join-Path $RepoRoot "manual_trading_hub\requirements.txt" + +$MonitorProjects = @( + @{ Key = "binance"; Dir = "crypto_monitor_binance" }, + @{ Key = "gate"; Dir = "crypto_monitor_gate" }, + @{ Key = "gate_bot"; Dir = "crypto_monitor_gate_bot" }, + @{ Key = "okx"; Dir = "crypto_monitor_okx" } +) +$HubProject = @{ Key = "hub"; Dir = "manual_trading_hub" } + +function Write-Step([string]$Msg) { + Write-Host "" + Write-Host "==> $Msg" -ForegroundColor Cyan +} + +function Test-Python310 { + $py = Get-Command python -ErrorAction SilentlyContinue + if (-not $py) { + throw "未找到 python。请安装 Python 3.10+ 并加入 PATH:https://www.python.org/downloads/" + } + $verText = & python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" + $parts = $verText.Trim() -split "\." + $major = [int]$parts[0] + $minor = [int]$parts[1] + if ($major -lt 3 -or ($major -eq 3 -and $minor -lt 10)) { + throw "需要 Python 3.10+,当前: $verText" + } + Write-Host "Python: $(python --version 2>&1)" -ForegroundColor DarkGray +} + +function Should-Include([string]$Key, [string[]]$Selected) { + if ($Selected -contains "all") { return $true } + return $Selected -contains $Key +} + +function Setup-MonitorProject([hashtable]$Proj) { + $projPath = Join-Path $RepoRoot $Proj.Dir + if (-not (Test-Path $projPath)) { + Write-Host " 跳过(目录不存在): $($Proj.Dir)" -ForegroundColor Yellow + return + } + Write-Step "$($Proj.Dir)" + Push-Location $projPath + try { + $venvDir = Join-Path $projPath ".venv" + $venvPy = Join-Path $venvDir "Scripts\python.exe" + $venvPip = Join-Path $venvDir "Scripts\pip.exe" + + if ($RecreateVenv -and (Test-Path $venvDir)) { + Write-Host " 删除旧 venv ..." + Remove-Item -Recurse -Force $venvDir + } + if (-not (Test-Path $venvPy)) { + Write-Host " 创建 venv ..." + & python -m venv .venv + } + + Write-Host " 升级 pip ..." + & $venvPy -m pip install -U pip setuptools wheel -q + + Write-Host " 安装依赖 (requirements.txt) ..." + & $venvPip install -r $ReqFile -q + + if (-not $SkipEnvCopy) { + $envExample = Join-Path $projPath ".env.example" + $envFile = Join-Path $projPath ".env" + if ((Test-Path $envExample) -and -not (Test-Path $envFile)) { + Copy-Item $envExample $envFile + Write-Host " 已复制 .env.example -> .env" -ForegroundColor Green + } elseif (Test-Path $envFile) { + Write-Host " 保留已有 .env" -ForegroundColor DarkGray + } else { + Write-Host " 无 .env.example,请手动配置 .env" -ForegroundColor Yellow + } + } + + $staticDirs = @( + "static\images", + "static\images\order_charts" + ) + foreach ($d in $staticDirs) { + $full = Join-Path $projPath $d + if (-not (Test-Path $full)) { + New-Item -ItemType Directory -Path $full -Force | Out-Null + } + } + Write-Host " 完成: $venvPy" -ForegroundColor Green + } finally { + Pop-Location + } +} + +function Setup-HubProject() { + $projPath = Join-Path $RepoRoot $HubProject.Dir + if (-not (Test-Path $projPath)) { + Write-Host " 跳过 hub(目录不存在)" -ForegroundColor Yellow + return + } + Write-Step $HubProject.Dir + Push-Location $projPath + try { + $venvDir = Join-Path $projPath ".venv" + $venvPy = Join-Path $venvDir "Scripts\python.exe" + $venvPip = Join-Path $venvDir "Scripts\pip.exe" + + if ($RecreateVenv -and (Test-Path $venvDir)) { + Remove-Item -Recurse -Force $venvDir + } + if (-not (Test-Path $venvPy)) { + & python -m venv .venv + } + & $venvPy -m pip install -U pip setuptools wheel -q + if (Test-Path $HubReqFile) { + & $venvPip install -r $HubReqFile -q + } + + if (-not $SkipEnvCopy) { + $envExample = Join-Path $projPath ".env.example" + $envFile = Join-Path $projPath ".env" + if ((Test-Path $envExample) -and -not (Test-Path $envFile)) { + Copy-Item $envExample $envFile + Write-Host " 已复制 .env.example -> .env" -ForegroundColor Green + } + } + Write-Host " 完成: $venvPy" -ForegroundColor Green + } finally { + Pop-Location + } +} + +function Install-Pm2IfNeeded() { + if ($SkipPm2) { return } + $node = Get-Command node -ErrorAction SilentlyContinue + if (-not $node) { + Write-Host "未检测到 Node.js,跳过 PM2。安装 Node 后执行: npm install -g pm2" -ForegroundColor Yellow + return + } + Write-Step "PM2(可选进程托管)" + $pm2 = Get-Command pm2 -ErrorAction SilentlyContinue + if ($pm2) { + Write-Host " PM2 已安装: $(pm2 -v)" -ForegroundColor Green + return + } + Write-Host " 正在全局安装 pm2 ..." + & npm install -g pm2 + Write-Host " PM2 安装完成。在各子目录执行: pm2 start ecosystem.config.cjs" -ForegroundColor Green +} + +# --- main --- +Write-Host "crypto_monitor 环境部署" -ForegroundColor White +Write-Host "仓库根目录: $RepoRoot" -ForegroundColor DarkGray + +if (-not (Test-Path $ReqFile)) { + throw "缺少 $ReqFile" +} + +Test-Python310 + +$selected = ($Only -split "[,;\s]+" | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ }) +if (-not $selected -or $selected.Count -eq 0) { $selected = @("all") } + +foreach ($p in $MonitorProjects) { + if (Should-Include $p.Key $selected) { + Setup-MonitorProject $p + } +} +if (Should-Include $HubProject.Key $selected) { + Setup-HubProject +} + +Install-Pm2IfNeeded + +Write-Host "" +Write-Host "部署完成。下一步:" -ForegroundColor Green +Write-Host " 1. 编辑各子目录 .env(API Key、密码、代理等)" +Write-Host " 2. 启动示例(Binance):" +Write-Host " cd crypto_monitor_binance" +Write-Host " .\.venv\Scripts\activate" +Write-Host " python app.py" +Write-Host " 3. Linux 服务器可用: bash deploy/setup_env.sh" +Write-Host "" diff --git a/history_window_lib.py b/history_window_lib.py index 3651736..a97d94d 100644 --- a/history_window_lib.py +++ b/history_window_lib.py @@ -75,6 +75,28 @@ def utc_window_to_bj_sql_strings(start_utc, end_utc, app_tz): return start_bj, end_bj +def normalize_bj_datetime_storage(raw): + """表单 datetime-local(含 T)入库前统一为 YYYY-MM-DD HH:MM:SS(北京时间)。""" + s = (raw or "").strip().replace("T", " ").replace("Z", "").strip() + if not s: + return "" + for fmt, n in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)): + try: + return datetime.strptime(s[:n], fmt).strftime("%Y-%m-%d %H:%M:%S") + except ValueError: + continue + return s + + +def sql_list_time_field(*columns): + """ + SQLite 列表时间窗比较表达式。 + journal_entries 的 open/close 可能含 'T',直接与 bounds(空格格式)比会误判为超出上界。 + """ + cols = ", ".join(columns) + return f"REPLACE(COALESCE({cols}), 'T', ' ')" + + SESSION_KEY_LIST_WIN = "list_win_filter"