This commit is contained in:
薇薇安 2026-01-21 21:45:10 +08:00
parent e80fd1059b
commit 45a654f654
7 changed files with 107 additions and 46 deletions

View File

@ -55,6 +55,15 @@ def _mask(s: str) -> str:
return f"{s[:4]}...{s[-4:]}"
def _ensure_account_active_for_start(account_id: int):
row = Account.get(int(account_id))
if not row:
raise HTTPException(status_code=404, detail="账号不存在")
status = (row.get("status") or "active").strip().lower()
if status != "active":
raise HTTPException(status_code=400, detail="账号已禁用,不能启动/重启交易进程")
@router.get("")
@router.get("/")
async def list_accounts(user: Dict[str, Any] = Depends(get_current_user)) -> List[Dict[str, Any]]:
@ -268,6 +277,7 @@ async def trading_start_for_account(account_id: int, user: Dict[str, Any] = Depe
if int(account_id) <= 0:
raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
require_account_owner(int(account_id), user)
_ensure_account_active_for_start(int(account_id))
program = program_name_for_account(int(account_id))
try:
out = run_supervisorctl(["start", program])
@ -311,6 +321,7 @@ async def trading_restart_for_account(account_id: int, user: Dict[str, Any] = De
if int(account_id) <= 0:
raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
require_account_owner(int(account_id), user)
_ensure_account_active_for_start(int(account_id))
program = program_name_for_account(int(account_id))
try:
out = run_supervisorctl(["restart", program])

View File

@ -17,6 +17,7 @@ router = APIRouter(prefix="/api/system")
# 管理员鉴权JWT未启用登录时兼容 X-Admin-Token
from api.auth_deps import require_system_admin # noqa: E402
from database.models import Account # noqa: E402
LOG_GROUPS = ("error", "warning", "info")
@ -918,8 +919,22 @@ async def trading_restart_all(
names = _list_supervisor_process_names(status_all)
targets: list[str] = []
skipped_disabled: list[Dict[str, Any]] = []
for n in names:
if n.startswith(prefix):
# 若能解析出 account_id则跳过 disabled 的账号
try:
m = re.match(rf"^{re.escape(prefix)}(\d+)$", n)
if m:
aid = int(m.group(1))
row = Account.get(aid)
st = (row.get("status") if isinstance(row, dict) else None) or "active"
if str(st).strip().lower() != "active":
skipped_disabled.append({"program": n, "account_id": aid, "status": st})
continue
except Exception:
# 解析失败/查库失败:不影响批量重启流程
pass
targets.append(n)
if include_default:
@ -935,6 +950,7 @@ async def trading_restart_all(
"count": 0,
"targets": [],
"status_all": status_all,
"skipped_disabled": skipped_disabled,
}
reread_out = ""
@ -982,6 +998,7 @@ async def trading_restart_all(
"update": update_out,
"targets": targets,
"results": results,
"skipped_disabled": skipped_disabled,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"批量重启失败: {e}")

View File

@ -8,7 +8,7 @@ import Recommendations from './components/Recommendations'
import LogMonitor from './components/LogMonitor'
import AccountSelector from './components/AccountSelector'
import Login from './components/Login'
import { api, clearAuthToken, setCurrentAccountId, getCurrentAccountId } from './services/api'
import { api, clearAuthToken, clearCurrentAccountId, setCurrentAccountId, getCurrentAccountId } from './services/api'
import './App.css'
function App() {
@ -20,20 +20,28 @@ function App() {
const u = await api.me()
setMe(u)
//
// account_id == user.id
// admin/沿 accountId
try {
if ((u?.role || '') !== 'admin') {
const list = await api.getAccounts()
const accounts = Array.isArray(list) ? list : []
const list = await api.getAccounts()
const accounts = Array.isArray(list) ? list : []
const active = accounts.filter((a) => String(a?.status || 'active') === 'active')
const isAdmin = (u?.role || '') === 'admin'
let target = null
if (isAdmin) {
// active 沿
target = active[0]?.id || accounts[0]?.id
} else {
// active active
const uid = parseInt(String(u?.id || ''), 10)
const match = accounts.find((a) => parseInt(String(a?.id || ''), 10) === uid)
const target = match?.id || accounts[0]?.id
if (target) {
const cur = getCurrentAccountId()
const next = parseInt(String(target), 10)
if (Number.isFinite(next) && next > 0 && cur !== next) setCurrentAccountId(next)
}
const match = active.find((a) => parseInt(String(a?.id || ''), 10) === uid)
target = match?.id || active[0]?.id || accounts[0]?.id
}
if (target) {
const cur = getCurrentAccountId()
const next = parseInt(String(target), 10)
if (Number.isFinite(next) && next > 0 && cur !== next) setCurrentAccountId(next)
}
} catch (e) {
// ignore
@ -91,6 +99,7 @@ function App() {
className="nav-logout"
onClick={() => {
clearAuthToken()
clearCurrentAccountId()
setMe(null)
setChecking(false)
}}

View File

@ -38,7 +38,8 @@ const AccountSelector = ({ onChanged }) => {
useEffect(() => {
if (!options.length) return
if (options.some((a) => a.id === accountId)) return
setAccountId(options[0].id)
const firstActive = options.find((a) => String(a?.status || 'active') === 'active') || options[0]
setAccountId(firstActive.id)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [optionsKey, accountId])
@ -57,6 +58,7 @@ const AccountSelector = ({ onChanged }) => {
{options.map((a) => (
<option key={a.id} value={a.id}>
#{a.id} {a.name || 'account'}
{String(a?.status || 'active') === 'disabled' ? '(已禁用)' : ''}
</option>
))}
</select>

View File

@ -47,6 +47,14 @@ export const setCurrentAccountId = (accountId) => {
}
};
export const clearCurrentAccountId = () => {
try {
localStorage.removeItem(ACCOUNT_ID_STORAGE_KEY);
} catch (e) {
// ignore
}
};
const withAuthHeaders = (headers = {}) => {
const token = getAuthToken();
if (!token) return { ...headers };

View File

@ -6,23 +6,31 @@ import logging
import sys
from pathlib import Path
# 启动方式兼容:
# - python trading_system/main.py__package__ 为空,需从同目录导入)
# - python -m trading_system.main__package__='trading_system',必须用相对导入)
if __package__ in (None, ""):
from binance_client import BinanceClient
from market_scanner import MarketScanner
from risk_manager import RiskManager
from position_manager import PositionManager
from strategy import TradingStrategy
import config
else:
from .binance_client import BinanceClient
from .market_scanner import MarketScanner
from .risk_manager import RiskManager
from .position_manager import PositionManager
from .strategy import TradingStrategy
from . import config
# 启动方式兼容(更鲁棒):
# - supervisor 推荐python -m trading_system.main相对导入
# - 手动调试python trading_system/main.py同目录导入
# - 其它非常规启动方式:尽量通过补齐 sys.path 避免本地模块找不到
try:
from .binance_client import BinanceClient # type: ignore
from .market_scanner import MarketScanner # type: ignore
from .risk_manager import RiskManager # type: ignore
from .position_manager import PositionManager # type: ignore
from .strategy import TradingStrategy # type: ignore
from . import config # type: ignore
except Exception:
_here = Path(__file__).resolve().parent
_root = _here.parent
# 某些 supervisor/启动脚本可能会导致 sys.path 没包含 trading_system 目录
if str(_here) not in sys.path:
sys.path.insert(0, str(_here))
if str(_root) not in sys.path:
sys.path.insert(0, str(_root))
from binance_client import BinanceClient # type: ignore
from market_scanner import MarketScanner # type: ignore
from risk_manager import RiskManager # type: ignore
from position_manager import PositionManager # type: ignore
from strategy import TradingStrategy # type: ignore
import config # type: ignore
# 配置日志(支持相对路径)
log_file = config.LOG_FILE

View File

@ -14,21 +14,27 @@ from pathlib import Path
from datetime import datetime, timezone, timedelta
# 启动方式兼容:
# - python trading_system/recommendations_main.py__package__ 为空,需从同目录导入)
# - python -m trading_system.recommendations_main__package__='trading_system',必须用相对导入)
if __package__ in (None, ""):
from binance_client import BinanceClient
from market_scanner import MarketScanner
from risk_manager import RiskManager
from trade_recommender import TradeRecommender
import config
else:
from .binance_client import BinanceClient
from .market_scanner import MarketScanner
from .risk_manager import RiskManager
from .trade_recommender import TradeRecommender
from . import config
# 启动方式兼容(更鲁棒):
# - supervisor 推荐python -m trading_system.recommendations_main相对导入
# - 手动调试python trading_system/recommendations_main.py同目录导入
try:
from .binance_client import BinanceClient # type: ignore
from .market_scanner import MarketScanner # type: ignore
from .risk_manager import RiskManager # type: ignore
from .trade_recommender import TradeRecommender # type: ignore
from . import config # type: ignore
except Exception:
_here = Path(__file__).resolve().parent
_root = _here.parent
if str(_here) not in sys.path:
sys.path.insert(0, str(_here))
if str(_root) not in sys.path:
sys.path.insert(0, str(_root))
from binance_client import BinanceClient # type: ignore
from market_scanner import MarketScanner # type: ignore
from risk_manager import RiskManager # type: ignore
from trade_recommender import TradeRecommender # type: ignore
import config # type: ignore
class BeijingTimeFormatter(logging.Formatter):