a
This commit is contained in:
parent
e80fd1059b
commit
45a654f654
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user