Add auth and continuous monitor endpoints

Этот коммит содержится в:
Виктор
2026-05-08 04:16:17 +09:00
родитель 1e79c58fd4
Коммит fbf24d8d96
+67 -40
Просмотреть файл
@@ -1,3 +1,4 @@
import asyncio
from pathlib import Path from pathlib import Path
from fastapi import Depends, FastAPI, HTTPException, WebSocket, WebSocketDisconnect from fastapi import Depends, FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
@@ -5,12 +6,14 @@ from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.ai_bot import AiTradingBot, decision_to_dict, signal_to_dict, trade_marker_from_record from app.ai_bot import AiTradingBot, decision_to_dict, signal_to_dict, trade_marker_from_record
from app.auth import require_auth
from app.coinex import CoinExClient, CoinExLiveStream from app.coinex import CoinExClient, CoinExLiveStream
from app.core import get_settings from app.core import get_settings
from app.db import SessionLocal, get_db, init_db from app.db import SessionLocal, get_db, init_db
from app.demo_account import DemoAccountService from app.demo_account import DemoAccountService
from app.legacy_bot import LegacyBotService from app.legacy_bot import LegacyBotService
from app.models import DemoTradeRecord from app.models import DemoTradeRecord
from app.monitor import SignalMonitor
settings = get_settings() settings = get_settings()
coinex = CoinExClient(settings.coinex_api_base) coinex = CoinExClient(settings.coinex_api_base)
@@ -18,6 +21,8 @@ live_stream = CoinExLiveStream(settings.coinex_ws_spot, coinex)
demo_service = DemoAccountService(settings.demo_initial_balance, settings.demo_quote_asset) demo_service = DemoAccountService(settings.demo_initial_balance, settings.demo_quote_asset)
ai_bot = AiTradingBot(coinex, settings.markets) ai_bot = AiTradingBot(coinex, settings.markets)
legacy_bot = LegacyBotService() legacy_bot = LegacyBotService()
monitor = SignalMonitor(ai_bot, demo_service, legacy_bot)
monitor_task: asyncio.Task | None = None
app = FastAPI(title=settings.app_name) app = FastAPI(title=settings.app_name)
static_dir = Path(__file__).parent / 'static' static_dir = Path(__file__).parent / 'static'
@@ -32,7 +37,7 @@ def on_startup() -> None:
@app.get('/') @app.get('/')
async def dashboard() -> FileResponse: async def dashboard(_: str = Depends(require_auth)) -> FileResponse:
return FileResponse(static_dir / 'index.html') return FileResponse(static_dir / 'index.html')
@@ -42,32 +47,25 @@ async def health() -> dict:
@app.get('/api/v1/app/bootstrap') @app.get('/api/v1/app/bootstrap')
async def app_bootstrap(db: Session = Depends(get_db)) -> dict: async def app_bootstrap(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
state = ai_bot.get_or_create_state(db) state = ai_bot.get_or_create_state(db)
account = demo_service.snapshot(db) account = demo_service.snapshot(db)
legacy_bot.ensure_defaults(db) legacy_bot.ensure_defaults(db)
return { return {'app': settings.app_name, 'default_market': settings.default_market, 'markets': settings.markets, 'bot': ai_bot.state_to_dict(state), 'account': account.model_dump(mode='json'), 'legacy': legacy_bot.dashboard(db), 'monitor': monitor.status()}
'app': settings.app_name,
'default_market': settings.default_market,
'markets': settings.markets,
'bot': ai_bot.state_to_dict(state),
'account': account.model_dump(mode='json'),
'legacy': legacy_bot.dashboard(db),
}
@app.get('/api/v1/demo/account') @app.get('/api/v1/demo/account')
async def demo_account(db: Session = Depends(get_db)) -> dict: async def demo_account(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return demo_service.snapshot(db).model_dump(mode='json') return demo_service.snapshot(db).model_dump(mode='json')
@app.post('/api/v1/demo/reset') @app.post('/api/v1/demo/reset')
async def reset_demo_account(db: Session = Depends(get_db)) -> dict: async def reset_demo_account(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return demo_service.reset(db).model_dump(mode='json') return demo_service.reset(db).model_dump(mode='json')
@app.post('/api/v1/demo/trades') @app.post('/api/v1/demo/trades')
async def create_demo_trade(market: str, side: str, price: float, amount: float, reason: str = 'manual demo trade', db: Session = Depends(get_db)) -> dict: async def create_demo_trade(market: str, side: str, price: float, amount: float, reason: str = 'manual demo trade', _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
try: try:
trade = demo_service.add_demo_trade(db=db, market=market, side=side, price=price, amount=amount, reason=reason) trade = demo_service.add_demo_trade(db=db, market=market, side=side, price=price, amount=amount, reason=reason)
legacy_bot.snapshot_history(db) legacy_bot.snapshot_history(db)
@@ -77,17 +75,17 @@ async def create_demo_trade(market: str, side: str, price: float, amount: float,
@app.get('/api/v1/market/kline') @app.get('/api/v1/market/kline')
async def market_kline(market: str | None = None, period: str = '1min', limit: int = 100) -> dict: async def market_kline(market: str | None = None, period: str = '1min', limit: int = 100, _: str = Depends(require_auth)) -> dict:
return await coinex.get_kline(market or settings.default_market, period, limit) return await coinex.get_kline(market or settings.default_market, period, limit)
@app.get('/api/v1/market/ticker') @app.get('/api/v1/market/ticker')
async def market_ticker(market: str | None = None) -> dict: async def market_ticker(market: str | None = None, _: str = Depends(require_auth)) -> dict:
return await coinex.get_ticker(market or settings.default_market) return await coinex.get_ticker(market or settings.default_market)
@app.get('/api/v1/bot/settings') @app.get('/api/v1/bot/settings')
async def bot_settings(db: Session = Depends(get_db)) -> dict: async def bot_settings(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
state = ai_bot.get_or_create_state(db) state = ai_bot.get_or_create_state(db)
data = ai_bot.state_to_dict(state) data = ai_bot.state_to_dict(state)
data['markets'] = settings.markets data['markets'] = settings.markets
@@ -95,27 +93,52 @@ async def bot_settings(db: Session = Depends(get_db)) -> dict:
@app.post('/api/v1/bot/settings') @app.post('/api/v1/bot/settings')
async def update_bot_settings(enabled: bool | None = None, trade_mode: str | None = None, trade_style_mode: str | None = None, min_signal_score: float | None = None, max_open_positions: int | None = None, max_quote_per_trade: float | None = None, emergency_stop: bool | None = None, db: Session = Depends(get_db)) -> dict: async def update_bot_settings(enabled: bool | None = None, trade_mode: str | None = None, trade_style_mode: str | None = None, min_signal_score: float | None = None, max_open_positions: int | None = None, max_quote_per_trade: float | None = None, emergency_stop: bool | None = None, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
if trade_mode is not None and trade_mode not in {'demo', 'live'}: if trade_mode is not None and trade_mode not in {'demo', 'live'}:
raise HTTPException(status_code=400, detail='trade_mode must be demo or live') raise HTTPException(status_code=400, detail='trade_mode must be demo or live')
state = ai_bot.update_state(db, enabled=enabled, trade_mode=trade_mode, trade_style_mode=trade_style_mode, min_signal_score=min_signal_score, max_open_positions=max_open_positions, max_quote_per_trade=max_quote_per_trade, emergency_stop=emergency_stop) state = ai_bot.update_state(db, enabled=enabled, trade_mode=trade_mode, trade_style_mode=trade_style_mode, min_signal_score=min_signal_score, max_open_positions=max_open_positions, max_quote_per_trade=max_quote_per_trade, emergency_stop=emergency_stop)
return ai_bot.state_to_dict(state) return ai_bot.state_to_dict(state)
@app.get('/api/v1/monitor/status')
async def monitor_status(_: str = Depends(require_auth)) -> dict:
return monitor.status()
@app.post('/api/v1/monitor/start')
async def monitor_start(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
global monitor_task
state = ai_bot.update_state(db, enabled=True)
if monitor_task is None or monitor_task.done():
monitor_task = asyncio.create_task(monitor.loop(SessionLocal, interval_seconds=20))
data = monitor.status()
data['bot'] = ai_bot.state_to_dict(state)
return data
@app.post('/api/v1/monitor/stop')
async def monitor_stop(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
state = ai_bot.update_state(db, enabled=False)
monitor.stop()
data = monitor.status()
data['bot'] = ai_bot.state_to_dict(state)
return data
@app.post('/api/v1/bot/live/enable') @app.post('/api/v1/bot/live/enable')
async def enable_live_mode(db: Session = Depends(get_db)) -> dict: async def enable_live_mode(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
state = ai_bot.update_state(db, trade_mode='live', emergency_stop=False) state = ai_bot.update_state(db, trade_mode='live', emergency_stop=False)
return {'trade_mode': 'live', 'settings': ai_bot.state_to_dict(state)} return {'trade_mode': 'live', 'settings': ai_bot.state_to_dict(state)}
@app.post('/api/v1/bot/live/disable') @app.post('/api/v1/bot/live/disable')
async def disable_live_mode(db: Session = Depends(get_db)) -> dict: async def disable_live_mode(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
state = ai_bot.update_state(db, trade_mode='demo') state = ai_bot.update_state(db, trade_mode='demo')
return {'trade_mode': 'demo', 'settings': ai_bot.state_to_dict(state)} return {'trade_mode': 'demo', 'settings': ai_bot.state_to_dict(state)}
@app.get('/api/v1/bot/analyze') @app.get('/api/v1/bot/analyze')
async def analyze_market(market: str | None = None, db: Session = Depends(get_db)) -> dict: async def analyze_market(market: str | None = None, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
try: try:
pack = await ai_bot.analyze_market(db, market or settings.default_market) pack = await ai_bot.analyze_market(db, market or settings.default_market)
return pack.as_dict() return pack.as_dict()
@@ -124,7 +147,7 @@ async def analyze_market(market: str | None = None, db: Session = Depends(get_db
@app.get('/api/v1/bot/best-market') @app.get('/api/v1/bot/best-market')
async def best_market(db: Session = Depends(get_db)) -> dict: async def best_market(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
try: try:
pack = await ai_bot.choose_best_market(db) pack = await ai_bot.choose_best_market(db)
return pack.as_dict() return pack.as_dict()
@@ -133,7 +156,7 @@ async def best_market(db: Session = Depends(get_db)) -> dict:
@app.post('/api/v1/bot/decide') @app.post('/api/v1/bot/decide')
async def bot_decide(market: str | None = None, db: Session = Depends(get_db)) -> dict: async def bot_decide(market: str | None = None, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
try: try:
decision = await ai_bot.make_decision(db, market) decision = await ai_bot.make_decision(db, market)
return decision_to_dict(decision) return decision_to_dict(decision)
@@ -156,7 +179,7 @@ def _force_demo_trade(db: Session, decision) -> dict:
@app.post('/api/v1/bot/auto-demo-trade') @app.post('/api/v1/bot/auto-demo-trade')
async def auto_demo_trade(market: str | None = None, db: Session = Depends(get_db)) -> dict: async def auto_demo_trade(market: str | None = None, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
try: try:
decision = await ai_bot.make_decision(db, market) decision = await ai_bot.make_decision(db, market)
return _force_demo_trade(db, decision) return _force_demo_trade(db, decision)
@@ -165,7 +188,7 @@ async def auto_demo_trade(market: str | None = None, db: Session = Depends(get_d
@app.post('/api/v1/bot/auto-trade') @app.post('/api/v1/bot/auto-trade')
async def auto_trade(market: str | None = None, db: Session = Depends(get_db)) -> dict: async def auto_trade(market: str | None = None, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
state = ai_bot.get_or_create_state(db) state = ai_bot.get_or_create_state(db)
decision = await ai_bot.make_decision(db, market) decision = await ai_bot.make_decision(db, market)
if state.trade_mode == 'live': if state.trade_mode == 'live':
@@ -174,23 +197,23 @@ async def auto_trade(market: str | None = None, db: Session = Depends(get_db)) -
@app.post('/api/v1/signals/manual') @app.post('/api/v1/signals/manual')
async def create_manual_signal(title: str, market: str, sentiment: str = 'neutral', score: float = 50.0, source: str = 'manual', url: str = '', db: Session = Depends(get_db)) -> dict: async def create_manual_signal(title: str, market: str, sentiment: str = 'neutral', score: float = 50.0, source: str = 'manual', url: str = '', _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
signal = ai_bot.record_manual_signal(db, title=title, market=market, sentiment=sentiment, score=score, source=source, url=url) signal = ai_bot.record_manual_signal(db, title=title, market=market, sentiment=sentiment, score=score, source=source, url=url)
return signal_to_dict(signal) return signal_to_dict(signal)
@app.get('/api/v1/signals/recent') @app.get('/api/v1/signals/recent')
async def recent_signals(limit: int = 50, db: Session = Depends(get_db)) -> dict: async def recent_signals(limit: int = 50, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return {'items': [signal_to_dict(item) for item in ai_bot.recent_signals(db, limit)]} return {'items': [signal_to_dict(item) for item in ai_bot.recent_signals(db, limit)]}
@app.get('/api/v1/decisions/recent') @app.get('/api/v1/decisions/recent')
async def recent_decisions(limit: int = 50, db: Session = Depends(get_db)) -> dict: async def recent_decisions(limit: int = 50, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return {'items': [decision_to_dict(item) for item in ai_bot.recent_decisions(db, limit)]} return {'items': [decision_to_dict(item) for item in ai_bot.recent_decisions(db, limit)]}
@app.get('/api/v1/dashboard/markers') @app.get('/api/v1/dashboard/markers')
async def dashboard_markers(market: str | None = None, limit: int = 100, db: Session = Depends(get_db)) -> dict: async def dashboard_markers(market: str | None = None, limit: int = 100, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
query = db.query(DemoTradeRecord) query = db.query(DemoTradeRecord)
if market: if market:
query = query.filter(DemoTradeRecord.market == market.upper()) query = query.filter(DemoTradeRecord.market == market.upper())
@@ -199,33 +222,33 @@ async def dashboard_markers(market: str | None = None, limit: int = 100, db: Ses
@app.get('/api/v1/legacy/dashboard') @app.get('/api/v1/legacy/dashboard')
async def legacy_dashboard(db: Session = Depends(get_db)) -> dict: async def legacy_dashboard(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return legacy_bot.dashboard(db) return legacy_bot.dashboard(db)
@app.get('/api/v1/legacy/open-trades') @app.get('/api/v1/legacy/open-trades')
async def legacy_open_trades(db: Session = Depends(get_db)) -> dict: async def legacy_open_trades(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return {'items': legacy_bot.open_positions(db)} return {'items': legacy_bot.open_positions(db)}
@app.get('/api/v1/legacy/closed-trades') @app.get('/api/v1/legacy/closed-trades')
async def legacy_closed_trades(limit: int = 50, db: Session = Depends(get_db)) -> dict: async def legacy_closed_trades(limit: int = 50, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return {'items': legacy_bot.closed_positions(db, limit)} return {'items': legacy_bot.closed_positions(db, limit)}
@app.get('/api/v1/legacy/chart-history') @app.get('/api/v1/legacy/chart-history')
async def legacy_chart_history(limit: int = 100, db: Session = Depends(get_db)) -> dict: async def legacy_chart_history(limit: int = 100, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return {'items': legacy_bot.chart_history(db, limit)} return {'items': legacy_bot.chart_history(db, limit)}
@app.get('/api/v1/legacy/strategies') @app.get('/api/v1/legacy/strategies')
async def legacy_strategies(db: Session = Depends(get_db)) -> dict: async def legacy_strategies(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
legacy_bot.ensure_defaults(db) legacy_bot.ensure_defaults(db)
return {'items': legacy_bot.dashboard(db)['strategies']} return {'items': legacy_bot.dashboard(db)['strategies']}
@app.post('/api/v1/legacy/strategies/{strategy_id}') @app.post('/api/v1/legacy/strategies/{strategy_id}')
async def legacy_update_strategy(strategy_id: int, enabled: bool | None = None, dry_run: bool | None = None, db: Session = Depends(get_db)) -> dict: async def legacy_update_strategy(strategy_id: int, enabled: bool | None = None, dry_run: bool | None = None, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
try: try:
return legacy_bot.update_strategy(db, strategy_id, enabled, dry_run) return legacy_bot.update_strategy(db, strategy_id, enabled, dry_run)
except ValueError as exc: except ValueError as exc:
@@ -233,32 +256,32 @@ async def legacy_update_strategy(strategy_id: int, enabled: bool | None = None,
@app.get('/api/v1/legacy/coinex') @app.get('/api/v1/legacy/coinex')
async def legacy_coinex(db: Session = Depends(get_db)) -> dict: async def legacy_coinex(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return legacy_bot.dashboard(db)['coinex'] return legacy_bot.dashboard(db)['coinex']
@app.post('/api/v1/legacy/coinex') @app.post('/api/v1/legacy/coinex')
async def legacy_update_coinex(enabled: bool | None = None, access_id: str | None = None, secret_key: str | None = None, mode: str | None = None, db: Session = Depends(get_db)) -> dict: async def legacy_update_coinex(enabled: bool | None = None, access_id: str | None = None, secret_key: str | None = None, mode: str | None = None, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return legacy_bot.update_coinex(db, enabled, access_id, secret_key, mode) return legacy_bot.update_coinex(db, enabled, access_id, secret_key, mode)
@app.post('/api/v1/legacy/telegram') @app.post('/api/v1/legacy/telegram')
async def legacy_update_telegram(enabled: bool | None = None, chat_id: str | None = None, message_format: str | None = None, db: Session = Depends(get_db)) -> dict: async def legacy_update_telegram(enabled: bool | None = None, chat_id: str | None = None, message_format: str | None = None, _: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return legacy_bot.update_telegram(db, enabled, chat_id, message_format) return legacy_bot.update_telegram(db, enabled, chat_id, message_format)
@app.get('/api/v1/legacy/updater/status') @app.get('/api/v1/legacy/updater/status')
async def legacy_updater_status(db: Session = Depends(get_db)) -> dict: async def legacy_updater_status(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return legacy_bot.updater_status(db) return legacy_bot.updater_status(db)
@app.post('/api/v1/legacy/terminal') @app.post('/api/v1/legacy/terminal')
async def legacy_terminal(command: str) -> dict: async def legacy_terminal(command: str, _: str = Depends(require_auth)) -> dict:
return legacy_bot.terminal_command(command) return legacy_bot.terminal_command(command)
@app.get('/api/v1/android/status') @app.get('/api/v1/android/status')
async def android_status(db: Session = Depends(get_db)) -> dict: async def android_status(_: str = Depends(require_auth), db: Session = Depends(get_db)) -> dict:
return legacy_bot.android_status(db) return legacy_bot.android_status(db)
@@ -270,6 +293,10 @@ async def market_stream(websocket: WebSocket, market: str) -> None:
with SessionLocal() as db: with SessionLocal() as db:
account = demo_service.snapshot(db).model_dump(mode='json') account = demo_service.snapshot(db).model_dump(mode='json')
state = ai_bot.state_to_dict(ai_bot.get_or_create_state(db)) state = ai_bot.state_to_dict(ai_bot.get_or_create_state(db))
price = live_event.get('price')
if price:
demo_service.reprice_positions(db, {market: float(price)})
db.commit()
live_event['demo_account'] = account live_event['demo_account'] = account
live_event['bot_state'] = state live_event['bot_state'] = state
await websocket.send_json(live_event) await websocket.send_json(live_event)