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:]}"
|
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("")
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def list_accounts(user: Dict[str, Any] = Depends(get_current_user)) -> List[Dict[str, Any]]:
|
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:
|
if int(account_id) <= 0:
|
||||||
raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
|
raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
|
||||||
require_account_owner(int(account_id), user)
|
require_account_owner(int(account_id), user)
|
||||||
|
_ensure_account_active_for_start(int(account_id))
|
||||||
program = program_name_for_account(int(account_id))
|
program = program_name_for_account(int(account_id))
|
||||||
try:
|
try:
|
||||||
out = run_supervisorctl(["start", program])
|
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:
|
if int(account_id) <= 0:
|
||||||
raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
|
raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
|
||||||
require_account_owner(int(account_id), user)
|
require_account_owner(int(account_id), user)
|
||||||
|
_ensure_account_active_for_start(int(account_id))
|
||||||
program = program_name_for_account(int(account_id))
|
program = program_name_for_account(int(account_id))
|
||||||
try:
|
try:
|
||||||
out = run_supervisorctl(["restart", program])
|
out = run_supervisorctl(["restart", program])
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ router = APIRouter(prefix="/api/system")
|
||||||
|
|
||||||
# 管理员鉴权(JWT;未启用登录时兼容 X-Admin-Token)
|
# 管理员鉴权(JWT;未启用登录时兼容 X-Admin-Token)
|
||||||
from api.auth_deps import require_system_admin # noqa: E402
|
from api.auth_deps import require_system_admin # noqa: E402
|
||||||
|
from database.models import Account # noqa: E402
|
||||||
|
|
||||||
LOG_GROUPS = ("error", "warning", "info")
|
LOG_GROUPS = ("error", "warning", "info")
|
||||||
|
|
||||||
|
|
@ -918,8 +919,22 @@ async def trading_restart_all(
|
||||||
names = _list_supervisor_process_names(status_all)
|
names = _list_supervisor_process_names(status_all)
|
||||||
|
|
||||||
targets: list[str] = []
|
targets: list[str] = []
|
||||||
|
skipped_disabled: list[Dict[str, Any]] = []
|
||||||
for n in names:
|
for n in names:
|
||||||
if n.startswith(prefix):
|
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)
|
targets.append(n)
|
||||||
|
|
||||||
if include_default:
|
if include_default:
|
||||||
|
|
@ -935,6 +950,7 @@ async def trading_restart_all(
|
||||||
"count": 0,
|
"count": 0,
|
||||||
"targets": [],
|
"targets": [],
|
||||||
"status_all": status_all,
|
"status_all": status_all,
|
||||||
|
"skipped_disabled": skipped_disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
reread_out = ""
|
reread_out = ""
|
||||||
|
|
@ -982,6 +998,7 @@ async def trading_restart_all(
|
||||||
"update": update_out,
|
"update": update_out,
|
||||||
"targets": targets,
|
"targets": targets,
|
||||||
"results": results,
|
"results": results,
|
||||||
|
"skipped_disabled": skipped_disabled,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"批量重启失败: {e}")
|
raise HTTPException(status_code=500, detail=f"批量重启失败: {e}")
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import Recommendations from './components/Recommendations'
|
||||||
import LogMonitor from './components/LogMonitor'
|
import LogMonitor from './components/LogMonitor'
|
||||||
import AccountSelector from './components/AccountSelector'
|
import AccountSelector from './components/AccountSelector'
|
||||||
import Login from './components/Login'
|
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'
|
import './App.css'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
@ -20,21 +20,29 @@ function App() {
|
||||||
const u = await api.me()
|
const u = await api.me()
|
||||||
setMe(u)
|
setMe(u)
|
||||||
|
|
||||||
// 普通用户:登录后默认选择“自己的账号”
|
// 登录后默认选择账号(避免 admin/用户切换时沿用旧 accountId)
|
||||||
// 规则:若可见账号列表里存在 account_id == user.id,则优先选它;否则选第一个可见账号。
|
|
||||||
try {
|
try {
|
||||||
if ((u?.role || '') !== 'admin') {
|
|
||||||
const list = await api.getAccounts()
|
const list = await api.getAccounts()
|
||||||
const accounts = Array.isArray(list) ? list : []
|
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 uid = parseInt(String(u?.id || ''), 10)
|
||||||
const match = accounts.find((a) => parseInt(String(a?.id || ''), 10) === uid)
|
const match = active.find((a) => parseInt(String(a?.id || ''), 10) === uid)
|
||||||
const target = match?.id || accounts[0]?.id
|
target = match?.id || active[0]?.id || accounts[0]?.id
|
||||||
|
}
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
const cur = getCurrentAccountId()
|
const cur = getCurrentAccountId()
|
||||||
const next = parseInt(String(target), 10)
|
const next = parseInt(String(target), 10)
|
||||||
if (Number.isFinite(next) && next > 0 && cur !== next) setCurrentAccountId(next)
|
if (Number.isFinite(next) && next > 0 && cur !== next) setCurrentAccountId(next)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +99,7 @@ function App() {
|
||||||
className="nav-logout"
|
className="nav-logout"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearAuthToken()
|
clearAuthToken()
|
||||||
|
clearCurrentAccountId()
|
||||||
setMe(null)
|
setMe(null)
|
||||||
setChecking(false)
|
setChecking(false)
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ const AccountSelector = ({ onChanged }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!options.length) return
|
if (!options.length) return
|
||||||
if (options.some((a) => a.id === accountId)) 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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [optionsKey, accountId])
|
}, [optionsKey, accountId])
|
||||||
|
|
||||||
|
|
@ -57,6 +58,7 @@ const AccountSelector = ({ onChanged }) => {
|
||||||
{options.map((a) => (
|
{options.map((a) => (
|
||||||
<option key={a.id} value={a.id}>
|
<option key={a.id} value={a.id}>
|
||||||
#{a.id} {a.name || 'account'}
|
#{a.id} {a.name || 'account'}
|
||||||
|
{String(a?.status || 'active') === 'disabled' ? '(已禁用)' : ''}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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 withAuthHeaders = (headers = {}) => {
|
||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
if (!token) return { ...headers };
|
if (!token) return { ...headers };
|
||||||
|
|
|
||||||
|
|
@ -6,23 +6,31 @@ import logging
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# 启动方式兼容:
|
# 启动方式兼容(更鲁棒):
|
||||||
# - python trading_system/main.py(__package__ 为空,需从同目录导入)
|
# - supervisor 推荐:python -m trading_system.main(相对导入)
|
||||||
# - python -m trading_system.main(__package__='trading_system',必须用相对导入)
|
# - 手动调试:python trading_system/main.py(同目录导入)
|
||||||
if __package__ in (None, ""):
|
# - 其它非常规启动方式:尽量通过补齐 sys.path 避免本地模块找不到
|
||||||
from binance_client import BinanceClient
|
try:
|
||||||
from market_scanner import MarketScanner
|
from .binance_client import BinanceClient # type: ignore
|
||||||
from risk_manager import RiskManager
|
from .market_scanner import MarketScanner # type: ignore
|
||||||
from position_manager import PositionManager
|
from .risk_manager import RiskManager # type: ignore
|
||||||
from strategy import TradingStrategy
|
from .position_manager import PositionManager # type: ignore
|
||||||
import config
|
from .strategy import TradingStrategy # type: ignore
|
||||||
else:
|
from . import config # type: ignore
|
||||||
from .binance_client import BinanceClient
|
except Exception:
|
||||||
from .market_scanner import MarketScanner
|
_here = Path(__file__).resolve().parent
|
||||||
from .risk_manager import RiskManager
|
_root = _here.parent
|
||||||
from .position_manager import PositionManager
|
# 某些 supervisor/启动脚本可能会导致 sys.path 没包含 trading_system 目录
|
||||||
from .strategy import TradingStrategy
|
if str(_here) not in sys.path:
|
||||||
from . import config
|
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
|
log_file = config.LOG_FILE
|
||||||
|
|
|
||||||
|
|
@ -14,21 +14,27 @@ from pathlib import Path
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
|
||||||
# 启动方式兼容:
|
# 启动方式兼容(更鲁棒):
|
||||||
# - python trading_system/recommendations_main.py(__package__ 为空,需从同目录导入)
|
# - supervisor 推荐:python -m trading_system.recommendations_main(相对导入)
|
||||||
# - python -m trading_system.recommendations_main(__package__='trading_system',必须用相对导入)
|
# - 手动调试:python trading_system/recommendations_main.py(同目录导入)
|
||||||
if __package__ in (None, ""):
|
try:
|
||||||
from binance_client import BinanceClient
|
from .binance_client import BinanceClient # type: ignore
|
||||||
from market_scanner import MarketScanner
|
from .market_scanner import MarketScanner # type: ignore
|
||||||
from risk_manager import RiskManager
|
from .risk_manager import RiskManager # type: ignore
|
||||||
from trade_recommender import TradeRecommender
|
from .trade_recommender import TradeRecommender # type: ignore
|
||||||
import config
|
from . import config # type: ignore
|
||||||
else:
|
except Exception:
|
||||||
from .binance_client import BinanceClient
|
_here = Path(__file__).resolve().parent
|
||||||
from .market_scanner import MarketScanner
|
_root = _here.parent
|
||||||
from .risk_manager import RiskManager
|
if str(_here) not in sys.path:
|
||||||
from .trade_recommender import TradeRecommender
|
sys.path.insert(0, str(_here))
|
||||||
from . import config
|
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):
|
class BeijingTimeFormatter(logging.Formatter):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user