Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion backend/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from trading.position_service import get_position_service
from services.prompt_service import get_trading_strategy, set_trading_strategy

from functools import wraps
from fastapi import HTTPException, status
router = APIRouter()


Expand Down Expand Up @@ -49,6 +51,19 @@ class CacheInfoResponse(BaseModel):
symbol_details: Dict[str, Dict[str, Any]]


def check_control_permission(func):
"""装饰器:检查控制操作权限"""
@wraps(func)
async def wrapper(*args, **kwargs):
if not config.system.allow_control_operations:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Control operations are disabled for security reasons. Set allow_control_operations=true in agent.yaml to enable."
)
return await func(*args, **kwargs)
return wrapper


@router.get("/health", response_model=HealthResponse)
async def health_check():
"""System health check"""
Expand Down Expand Up @@ -182,7 +197,8 @@ async def get_system_config():
"system": {
"host": config.system.host,
"port": config.system.port,
"log_level": config.system.log_level
"log_level": config.system.log_level,
"allow_control_operations": config.system.allow_control_operations
},
"risk": {
"max_position_size_percent": config.default_risk.max_position_size_percent,
Expand Down Expand Up @@ -370,6 +386,7 @@ class AgentStatusResponse(BaseModel):

# Agent control endpoints
@router.post("/agent/start", response_model=AgentControlResponse)
@check_control_permission
async def start_agent():
"""启动 AI Agent 调度器"""
try:
Expand Down Expand Up @@ -405,6 +422,7 @@ async def start_agent():


@router.post("/agent/stop", response_model=AgentControlResponse)
@check_control_permission
async def stop_agent():
"""停止 AI Agent 调度器"""
try:
Expand Down Expand Up @@ -670,6 +688,7 @@ async def get_trade_stats(days: int = 30):


@router.post("/trading/history/reset")
@check_control_permission
async def reset_trading_history(init_time: Optional[str] = None):
"""重置交易历史系统(清空所有数据并重新初始化)"""
try:
Expand All @@ -695,6 +714,7 @@ async def reset_trading_history(init_time: Optional[str] = None):


@router.post("/trading/history/sync")
@check_control_permission
async def sync_trading_history(full_sync: bool = False):
"""手动同步交易历史"""
try:
Expand Down Expand Up @@ -775,6 +795,7 @@ async def get_current_trading_strategy():


@router.post("/trading/strategy", response_model=TradingStrategyUpdateResponse)
@check_control_permission
async def update_trading_strategy(request: TradingStrategyRequest):
"""更新用户自定义交易策略"""
try:
Expand All @@ -801,6 +822,7 @@ async def update_trading_strategy(request: TradingStrategyRequest):


@router.delete("/trading/strategy", response_model=TradingStrategyUpdateResponse)
@check_control_permission
async def reset_trading_strategy():
"""重置交易策略为默认值(删除数据库中的自定义配置)"""
try:
Expand Down
21 changes: 11 additions & 10 deletions backend/config/agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ agent:
model_name: "deepseek-chat" # Model name (gpt-4o, claude-3-5-sonnet, deepseek-chat, qwen-plus, etc.)
base_url: "https://api.deepseek.com/v1" # API base URL (null for OpenAI, or custom like "https://api.deepseek.com/v1")
api_key: "${OPENAI_API_KEY}" # API key environment variable

# Trading Configuration
decision_interval: 180 # seconds between decisions
symbols:
Expand All @@ -17,7 +17,7 @@ agent:
- "3m"
- "1h"
- "4h"

# 用户可配置的交易策略(会被数据库覆盖)
trading_strategy: |
1. 单一币种仓位上限为可用余额的 20%
Expand All @@ -33,30 +33,30 @@ exchange:
api_key: "${BINANCE_API_KEY}"
api_secret: "${BINANCE_API_SECRET}"
testnet: false # Use testnet for development

# WebSocket and REST API endpoints
websocket_url: "wss://fstream.binance.com/stream" # Production
rest_api_url: "https://fapi.binance.com" # Production

# Testnet endpoints (used when testnet: true)
testnet_websocket_url: "wss://stream.binancefuture.com/stream"
testnet_rest_api_url: "https://testnet.binancefuture.com"

# Futures trading settings (for CCXT)
default_leverage: 1
margin_mode: "cross" # cross or isolated
enable_rate_limit: true
timeout: 10000 # milliseconds
retries: 3
sandbox: false # CCXT sandbox mode (alternative to testnet)

# Risk Management (these can be overridden in user prompts)
default_risk:
max_position_size_percent: 0.1 # 10% of account per position
max_daily_loss_percent: 0.05 # 5% max daily loss
# Maximum leverage
stop_loss_percent: 0.02 # 2% stop loss

# Account Snapshot Configuration
account_snapshot:
enabled: true
Expand All @@ -69,9 +69,10 @@ logging:
save_decisions: true
save_executions: true
save_snapshots: true
# System Configuration

# System Configuration
system:
host: "0.0.0.0"
port: 8000
max_concurrent_decisions: 1 # Prevent overlapping decisions
max_concurrent_decisions: 1 # Prevent overlapping decisions
allow_control_operations: true # 是否允许前端控制操作(启动/停止 agent、修改策略等)
1 change: 1 addition & 0 deletions backend/config/agent_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class SystemConfig(BaseModel):
port: int = 8000
log_level: str = "INFO"
max_concurrent_decisions: int = 1
allow_control_operations: bool = False # 是否允许前端控制操作


class AppConfig(BaseModel):
Expand Down
31 changes: 17 additions & 14 deletions backend/services/prompt_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,33 +23,34 @@
_strategy_cache: Optional[str] = None
_cache_valid = False


async def get_trading_strategy() -> str:
"""
获取交易策略配置,按优先级:数据库 > 配置文件 > 代码默认
"""
global _strategy_cache, _cache_valid

# 先检查缓存
if _cache_valid and _strategy_cache is not None:
return _strategy_cache

try:
# 1. 优先级最高:检查数据库
async with get_session_maker()() as session:
result = await session.execute(
select(SystemConfig).where(SystemConfig.key == "trading_strategy")
)
config_row = result.scalar_one_or_none()

if config_row and config_row.value.strip():
logger.info("使用数据库中的交易策略配置")
_strategy_cache = config_row.value.strip()
_cache_valid = True
return _strategy_cache

except Exception as e:
logger.warning(f"读取数据库交易策略失败: {e}")

# 2. 次优先级:检查配置文件
try:
config_strategy = getattr(config.agent, 'trading_strategy', None)
Expand All @@ -60,32 +61,33 @@ async def get_trading_strategy() -> str:
return _strategy_cache
except Exception as e:
logger.warning(f"读取配置文件交易策略失败: {e}")

# 3. 最低优先级:使用代码默认
logger.info("使用代码默认的交易策略配置")
_strategy_cache = DEFAULT_TRADING_STRATEGY
_cache_valid = True
return _strategy_cache


async def set_trading_strategy(strategy: str) -> bool:
"""
设置用户自定义的交易策略(存储到数据库)
"""
global _strategy_cache, _cache_valid

try:
if not strategy or not strategy.strip():
raise ValueError("交易策略内容不能为空")

strategy = strategy.strip()

async with get_session_maker()() as session:
# 查找现有配置
result = await session.execute(
select(SystemConfig).where(SystemConfig.key == "trading_strategy")
)
config_row = result.scalar_one_or_none()

if config_row:
# 更新现有配置
config_row.value = strategy
Expand All @@ -99,19 +101,20 @@ async def set_trading_strategy(strategy: str) -> bool:
)
session.add(new_config)
logger.info("创建新的交易策略配置")

await session.commit()

# 清除缓存,强制下次重新读取
_strategy_cache = None
_cache_valid = False

return True

except Exception as e:
logger.error(f"设置交易策略失败: {e}")
return False


def clear_strategy_cache():
"""清除策略缓存(用于测试或强制刷新)"""
global _strategy_cache, _cache_valid
Expand Down
20 changes: 10 additions & 10 deletions frontend/src/app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { AgentStatus } from "@/lib/api";
import Toast from "@/components/Toast";
import Sidebar from "@/components/Sidebar";
import Header from "@/components/Header";
import { isControlOperationsAllowed } from "@/lib/config";

export default function SettingsPage() {
const [strategy, setStrategy] = useState("");
Expand All @@ -25,6 +26,7 @@ export default function SettingsPage() {
text: string;
} | null>(null);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [controlAllowed, setControlAllowed] = useState(false);

// Bot status
const [botStatus, setBotStatus] = useState<AgentStatus>({
Expand All @@ -43,6 +45,7 @@ export default function SettingsPage() {
useEffect(() => {
loadStrategy();
loadBotStatus();
isControlOperationsAllowed().then(setControlAllowed);
}, []);

const loadStrategy = async () => {
Expand Down Expand Up @@ -82,9 +85,8 @@ export default function SettingsPage() {

await loadBotStatus(); // Reload status
} catch (error) {
const fallbackMessage = `Failed to ${
botStatus.is_running ? "stop" : "start"
} trading bot`;
const fallbackMessage = `Failed to ${botStatus.is_running ? "stop" : "start"
} trading bot`;
setMessage({
type: "error",
text: error instanceof Error ? error.message : fallbackMessage,
Expand Down Expand Up @@ -204,11 +206,10 @@ export default function SettingsPage() {
<span className="font-medium">
Trading Bot Status:
<span
className={`ml-2 font-bold ${
botStatus.is_running
className={`ml-2 font-bold ${botStatus.is_running
? "text-green-600"
: "text-red-600"
}`}
}`}
>
{botStatus.is_running ? "RUNNING" : "STOPPED"}
</span>
Expand All @@ -218,12 +219,11 @@ export default function SettingsPage() {

<button
onClick={handleBotToggle}
disabled={botLoading}
className={`flex items-center justify-center space-x-2 px-4 py-2 font-bold uppercase tracking-wide text-sm border-2 border-black transition-colors duration-200 ${
botStatus.is_running
disabled={!controlAllowed || botLoading}
className={`flex items-center justify-center space-x-2 px-4 py-2 font-bold uppercase tracking-wide text-sm border-2 border-black transition-colors duration-200 ${botStatus.is_running
? "bg-red-600 hover:bg-red-700 text-white"
: "bg-blue-600 hover:bg-blue-700 text-white"
} disabled:bg-gray-400 disabled:cursor-not-allowed disabled:text-gray-200`}
} disabled:bg-gray-400 disabled:cursor-not-allowed disabled:text-gray-200`}
>
{botLoading ? (
<>
Expand Down
Loading