update refactor
This commit is contained in:
parent
01abc7e446
commit
55ad0607f3
39 changed files with 4304 additions and 584 deletions
37
.gitignore
vendored
37
.gitignore
vendored
|
|
@ -1,26 +1,27 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.pyc
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# Virtual env
|
||||
venv/
|
||||
ENV/
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Log, data
|
||||
*.log
|
||||
*.sqlite3
|
||||
# VSCode
|
||||
.vscode/
|
||||
|
||||
# Build
|
||||
/dist/
|
||||
/build/
|
||||
|
||||
# Others
|
||||
*.local
|
||||
20
Dockerfile
20
Dockerfile
|
|
@ -1,20 +0,0 @@
|
|||
FROM python:3.10-slim
|
||||
|
||||
# Cài đặt các thư viện hệ thống cần thiết
|
||||
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Tạo thư mục app
|
||||
WORKDIR /app
|
||||
|
||||
# Copy requirements và cài đặt
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy toàn bộ mã nguồn
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Lệnh chạy uvicorn
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
14
README.md
14
README.md
|
|
@ -124,3 +124,17 @@ For open source projects, say how it is licensed.
|
|||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
|
||||
# AI Trading System (NodeJS)
|
||||
|
||||
## Hướng dẫn cài đặt
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Chạy server
|
||||
|
||||
```bash
|
||||
node src/app.js
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
[
|
||||
{
|
||||
"id": "user1",
|
||||
"bybit_api_key": "your_bybit_api_key_1",
|
||||
"bybit_api_secret": "your_bybit_api_secret_1",
|
||||
"user_name": "Nguyen Van A"
|
||||
},
|
||||
{
|
||||
"id": "user2",
|
||||
"bybit_api_key": "your_bybit_api_key_2",
|
||||
"bybit_api_secret": "your_bybit_api_secret_2",
|
||||
"user_name": "Tran Thi B"
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
"bybit_api_key": "dqGCPAJzLKoTRfgCMq",
|
||||
"bybit_api_secret": "aDYziLWN2jvdWNuz4QhWrM1O65JZ5f1NtEUO",
|
||||
"user_name": "Key-test"
|
||||
}
|
||||
]
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
from fastapi import APIRouter, Query, HTTPException
|
||||
from app.schemas.account import AccountResponseSchema
|
||||
from app.services.bybit_service import get_bybit_client
|
||||
import json
|
||||
import os
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../accounts.json')
|
||||
|
||||
def get_user_api(user_id: str):
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
|
||||
accounts = json.load(f)
|
||||
for acc in accounts:
|
||||
if acc['id'] == user_id:
|
||||
return acc['bybit_api_key'], acc['bybit_api_secret']
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}")
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy userId")
|
||||
|
||||
@router.get("/account", response_model=AccountResponseSchema, tags=["Account"])
|
||||
def get_account_info(userId: str = Query(..., description="User ID to get API key/secret")):
|
||||
try:
|
||||
api_key, api_secret = get_user_api(userId)
|
||||
client = get_bybit_client(api_key, api_secret)
|
||||
res = client.get_wallet_balance(accountType="UNIFIED")
|
||||
if 'retCode' in res and res['retCode'] != 0:
|
||||
raise HTTPException(status_code=400, detail=f"Bybit API error: {res.get('retMsg', 'Unknown error')}")
|
||||
return res
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"System error: {e}")
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
from fastapi import APIRouter, Query, HTTPException
|
||||
from typing import Optional
|
||||
from app.services.bybit_service import get_bybit_client
|
||||
from app.schemas.candle import CandleResponseSchema
|
||||
import json
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../accounts.json')
|
||||
|
||||
def get_user_api(user_id: str):
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
|
||||
accounts = json.load(f)
|
||||
for acc in accounts:
|
||||
if acc['id'] == user_id:
|
||||
return acc['bybit_api_key'], acc['bybit_api_secret']
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}")
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy userId")
|
||||
|
||||
def ema(series, period):
|
||||
return series.ewm(span=period, adjust=False).mean()
|
||||
|
||||
def macd(series, fast=45, slow=90, signal=9):
|
||||
ema_fast = ema(series, fast)
|
||||
ema_slow = ema(series, slow)
|
||||
macd_line = ema_fast - ema_slow
|
||||
signal_line = macd_line.ewm(span=signal, adjust=False).mean()
|
||||
macd_hist = macd_line - signal_line
|
||||
return macd_line, signal_line, macd_hist
|
||||
|
||||
def rsi(series, period=14):
|
||||
delta = series.diff()
|
||||
gain = (delta.where(delta > 0, 0)).rolling(window=period, min_periods=1).mean()
|
||||
loss = (-delta.where(delta < 0, 0)).rolling(window=period, min_periods=1).mean()
|
||||
loss = loss.replace(0, np.nan) # tránh chia cho 0
|
||||
rs = gain / loss
|
||||
rsi = 100 - (100 / (1 + rs))
|
||||
rsi = rsi.replace([np.inf, -np.inf], np.nan)
|
||||
return rsi
|
||||
|
||||
@router.get("/candles", response_model=CandleResponseSchema, response_model_exclude_none=True, tags=["Candle"])
|
||||
def get_candles(
|
||||
userId: str = Query(..., description="User ID to get API key/secret"),
|
||||
symbol: str = Query(..., description="Trading symbol, e.g. BTCUSDT"),
|
||||
interval: str = Query("1", description="Candle interval, e.g. 1, 3, 5, 15, 30, 60, 240, D, W, M"),
|
||||
limit: Optional[int] = Query(200, description="Number of candles to return (max 1000)"),
|
||||
start: Optional[int] = Query(None, description="Start timestamp (milliseconds)"),
|
||||
end: Optional[int] = Query(None, description="End timestamp (milliseconds)")
|
||||
):
|
||||
api_key, api_secret = get_user_api(userId)
|
||||
client = get_bybit_client(api_key, api_secret)
|
||||
params = {
|
||||
"category": "linear", # or "spot" for spot
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"limit": limit
|
||||
}
|
||||
if start:
|
||||
params["start"] = start
|
||||
if end:
|
||||
params["end"] = end
|
||||
res = client.get_kline(**params)
|
||||
if 'retCode' in res and res['retCode'] != 0:
|
||||
raise HTTPException(status_code=400, detail=f"Bybit API error: {res.get('retMsg', 'Unknown error')}")
|
||||
candles = res.get('result', {}).get('list', [])
|
||||
if not candles:
|
||||
return {"data": [], "indicators": {}}
|
||||
# Convert to DataFrame
|
||||
df = pd.DataFrame(candles, columns=[
|
||||
"timestamp", "open", "high", "low", "close", "volume", "turnover"
|
||||
])
|
||||
df = df.astype({
|
||||
"timestamp": np.int64,
|
||||
"open": np.float64,
|
||||
"high": np.float64,
|
||||
"low": np.float64,
|
||||
"close": np.float64,
|
||||
"volume": np.float64,
|
||||
"turnover": np.float64
|
||||
})
|
||||
df = df.sort_values("timestamp")
|
||||
# Calculate indicators
|
||||
close = df["close"]
|
||||
macd_line, macd_signal, macd_hist = macd(close, fast=45, slow=90, signal=9)
|
||||
df["macd"] = macd_line
|
||||
df["macdsignal"] = macd_signal
|
||||
df["macdhist"] = macd_hist
|
||||
df["ema34"] = ema(close, 34)
|
||||
df["ema50"] = ema(close, 50)
|
||||
df["ema100"] = ema(close, 100)
|
||||
df["ema200"] = ema(close, 200)
|
||||
df["rsi"] = rsi(close)
|
||||
# Return result
|
||||
data = df.replace({np.nan: None}).to_dict(orient="records")
|
||||
return {
|
||||
"data": data,
|
||||
"indicators": {
|
||||
"macd": {"fast": 45, "slow": 90, "signal": 9},
|
||||
"ema": [34, 50, 100, 200],
|
||||
"rsi": True
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
from fastapi import APIRouter, Query, HTTPException, Body
|
||||
from typing import Optional
|
||||
from app.services.bybit_service import get_bybit_client
|
||||
import json
|
||||
import os
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../accounts.json')
|
||||
|
||||
def get_user_api(user_id: str):
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
|
||||
accounts = json.load(f)
|
||||
for acc in accounts:
|
||||
if acc['id'] == user_id:
|
||||
return acc['bybit_api_key'], acc['bybit_api_secret']
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}")
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy userId")
|
||||
|
||||
@router.get("/orders", tags=["Order"])
|
||||
def get_orders(
|
||||
userId: str = Query(..., description="User ID to get API key/secret"),
|
||||
symbol: str = Query(..., description="Trading symbol, e.g. BTCUSDT"),
|
||||
category: str = Query("linear", description="Order type: linear (future) or spot")
|
||||
):
|
||||
api_key, api_secret = get_user_api(userId)
|
||||
client = get_bybit_client(api_key, api_secret)
|
||||
res = client.get_open_orders(
|
||||
category=category, # "linear" for future, "spot" for spot
|
||||
symbol=symbol
|
||||
)
|
||||
return res
|
||||
|
||||
# Submit order
|
||||
@router.post("/orders", tags=["Order"])
|
||||
def submit_order(
|
||||
userId: str = Body(..., embed=True, description="User ID to get API key/secret"),
|
||||
symbol: str = Body(..., embed=True, description="Trading symbol, e.g. BTCUSDT"),
|
||||
side: str = Body(..., embed=True, description="Order side: Buy or Sell"),
|
||||
orderType: str = Body(..., embed=True, description="Order type: Market or Limit"),
|
||||
qty: float = Body(..., embed=True, description="Order quantity"),
|
||||
category: str = Body("linear", embed=True, description="Order type: linear (future) or spot"),
|
||||
price: Optional[float] = Body(None, embed=True, description="Order price (required for Limit)")
|
||||
):
|
||||
api_key, api_secret = get_user_api(userId)
|
||||
client = get_bybit_client(api_key, api_secret)
|
||||
params = {
|
||||
"category": category,
|
||||
"symbol": symbol,
|
||||
"side": side,
|
||||
"orderType": orderType,
|
||||
"qty": qty
|
||||
}
|
||||
if orderType.lower() == "limit":
|
||||
if price is None:
|
||||
raise HTTPException(status_code=400, detail="Missing price for Limit order")
|
||||
params["price"] = price
|
||||
res = client.place_order(**params)
|
||||
return res
|
||||
|
||||
# Cancel order
|
||||
@router.delete("/orders/{order_id}", tags=["Order"])
|
||||
def cancel_order(
|
||||
order_id: str,
|
||||
userId: str = Query(..., description="User ID to get API key/secret"),
|
||||
symbol: str = Query(..., description="Trading symbol, e.g. BTCUSDT"),
|
||||
category: str = Query("linear", description="Order type: linear (future) or spot")
|
||||
):
|
||||
api_key, api_secret = get_user_api(userId)
|
||||
client = get_bybit_client(api_key, api_secret)
|
||||
res = client.cancel_order(
|
||||
category=category,
|
||||
symbol=symbol,
|
||||
orderId=order_id
|
||||
)
|
||||
return res
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
from fastapi import APIRouter, Query, HTTPException
|
||||
from typing import Optional
|
||||
from app.services.bybit_service import get_bybit_client
|
||||
import json
|
||||
import os
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../accounts.json')
|
||||
|
||||
def get_user_api(user_id: str):
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
|
||||
accounts = json.load(f)
|
||||
for acc in accounts:
|
||||
if acc['id'] == user_id:
|
||||
return acc['bybit_api_key'], acc['bybit_api_secret']
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}")
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy userId")
|
||||
|
||||
@router.get("/positions", tags=["Position"])
|
||||
def get_positions(
|
||||
userId: str = Query(..., description="User ID to get API key/secret"),
|
||||
symbol: Optional[str] = Query(None, description="Trading symbol, e.g. BTCUSDT"),
|
||||
category: str = Query("linear", description="Order type: linear (future) or spot")
|
||||
):
|
||||
api_key, api_secret = get_user_api(userId)
|
||||
client = get_bybit_client(api_key, api_secret)
|
||||
params = {
|
||||
"category": category
|
||||
}
|
||||
if symbol:
|
||||
params["symbol"] = symbol
|
||||
res = client.get_positions(**params)
|
||||
return res
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
from fastapi import APIRouter, Query, HTTPException, Body
|
||||
import json
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../../accounts.json')
|
||||
|
||||
# API lấy danh sách user
|
||||
@router.get("/users", tags=["User"])
|
||||
def list_users():
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
|
||||
accounts = json.load(f)
|
||||
return accounts
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error reading accounts file: {e}")
|
||||
|
||||
# API thêm user
|
||||
@router.post("/users", tags=["User"])
|
||||
def add_user(
|
||||
id: str = Body(..., description="User ID"),
|
||||
bybit_api_key: str = Body(..., description="Bybit API Key"),
|
||||
bybit_api_secret: str = Body(..., description="Bybit API Secret"),
|
||||
user_name: str = Body(..., description="User name")
|
||||
):
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, 'r+', encoding='utf-8') as f:
|
||||
accounts = json.load(f)
|
||||
if any(acc['id'] == id for acc in accounts):
|
||||
raise HTTPException(status_code=400, detail="ID already exists")
|
||||
new_user = {
|
||||
"id": id,
|
||||
"bybit_api_key": bybit_api_key,
|
||||
"bybit_api_secret": bybit_api_secret,
|
||||
"user_name": user_name
|
||||
}
|
||||
accounts.append(new_user)
|
||||
f.seek(0)
|
||||
json.dump(accounts, f, ensure_ascii=False, indent=2)
|
||||
f.truncate()
|
||||
return {"message": "User added successfully"}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error writing accounts file: {e}")
|
||||
|
||||
# API sửa user
|
||||
@router.put("/users/{user_id}", tags=["User"])
|
||||
def update_user(
|
||||
user_id: str,
|
||||
bybit_api_key: Optional[str] = Body(None),
|
||||
bybit_api_secret: Optional[str] = Body(None),
|
||||
user_name: Optional[str] = Body(None)
|
||||
):
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, 'r+', encoding='utf-8') as f:
|
||||
accounts = json.load(f)
|
||||
for acc in accounts:
|
||||
if acc['id'] == user_id:
|
||||
if bybit_api_key is not None:
|
||||
acc['bybit_api_key'] = bybit_api_key
|
||||
if bybit_api_secret is not None:
|
||||
acc['bybit_api_secret'] = bybit_api_secret
|
||||
if user_name is not None:
|
||||
acc['user_name'] = user_name
|
||||
f.seek(0)
|
||||
json.dump(accounts, f, ensure_ascii=False, indent=2)
|
||||
f.truncate()
|
||||
return {"message": "User updated successfully"}
|
||||
raise HTTPException(status_code=404, detail="User ID not found")
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error writing accounts file: {e}")
|
||||
|
||||
# API xóa user
|
||||
@router.delete("/users/{user_id}", tags=["User"])
|
||||
def delete_user(user_id: str):
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, 'r+', encoding='utf-8') as f:
|
||||
accounts = json.load(f)
|
||||
new_accounts = [acc for acc in accounts if acc['id'] != user_id]
|
||||
if len(new_accounts) == len(accounts):
|
||||
raise HTTPException(status_code=404, detail="User ID not found")
|
||||
f.seek(0)
|
||||
json.dump(new_accounts, f, ensure_ascii=False, indent=2)
|
||||
f.truncate()
|
||||
return {"message": "User deleted successfully"}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error writing accounts file: {e}")
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
||||
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
||||
BYBIT_API_KEY = os.getenv("BYBIT_API_KEY")
|
||||
BYBIT_API_SECRET = os.getenv("BYBIT_API_SECRET")
|
||||
21
app/main.py
21
app/main.py
|
|
@ -1,21 +0,0 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.api import candles, orders, account, positions
|
||||
from app.api.user import user
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# CORS config: allow all origins (for development)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(candles.router, prefix="/api")
|
||||
app.include_router(orders.router, prefix="/api")
|
||||
app.include_router(account.router, prefix="/api")
|
||||
app.include_router(positions.router, prefix="/api")
|
||||
app.include_router(user.router, prefix="/api")
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
class CoinInfo(BaseModel):
|
||||
availableToBorrow: Optional[str] = Field(None, description="Amount available to borrow")
|
||||
bonus: Optional[str] = Field(None, description="Bonus amount")
|
||||
accruedInterest: Optional[str] = Field(None, description="Accrued interest")
|
||||
availableToWithdraw: Optional[str] = Field(None, description="Amount available to withdraw")
|
||||
totalOrderIM: Optional[str] = Field(None, description="Initial margin for orders")
|
||||
equity: Optional[str] = Field(None, description="Total coin equity")
|
||||
totalPositionMM: Optional[str] = Field(None, description="Maintenance margin for positions")
|
||||
usdValue: Optional[str] = Field(None, description="USD value of the coin")
|
||||
unrealisedPnl: Optional[str] = Field(None, description="Unrealized PnL")
|
||||
collateralSwitch: Optional[bool] = Field(None, description="Is used as collateral")
|
||||
spotHedgingQty: Optional[str] = Field(None, description="Spot hedging quantity")
|
||||
borrowAmount: Optional[str] = Field(None, description="Borrowed amount")
|
||||
totalPositionIM: Optional[str] = Field(None, description="Initial margin for positions")
|
||||
walletBalance: Optional[str] = Field(None, description="Coin wallet balance")
|
||||
cumRealisedPnl: Optional[str] = Field(None, description="Cumulative realized PnL")
|
||||
locked: Optional[str] = Field(None, description="Locked amount")
|
||||
marginCollateral: Optional[bool] = Field(None, description="Is used as margin collateral")
|
||||
coin: str = Field(..., description="Coin symbol")
|
||||
|
||||
class AccountInfo(BaseModel):
|
||||
totalEquity: str = Field(..., description="Total equity in USD")
|
||||
accountIMRate: str = Field(..., description="Initial margin rate")
|
||||
totalMarginBalance: str = Field(..., description="Total margin balance")
|
||||
totalInitialMargin: str = Field(..., description="Total initial margin")
|
||||
accountType: str = Field(..., description="Account type (e.g. UNIFIED)")
|
||||
totalAvailableBalance: str = Field(..., description="Total available balance")
|
||||
accountMMRate: str = Field(..., description="Maintenance margin rate")
|
||||
totalPerpUPL: str = Field(..., description="Unrealized PnL of perpetual positions")
|
||||
totalWalletBalance: str = Field(..., description="Total wallet balance")
|
||||
accountLTV: str = Field(..., description="Loan to value")
|
||||
totalMaintenanceMargin: str = Field(..., description="Total maintenance margin")
|
||||
coin: List[CoinInfo] = Field(..., description="List of coins")
|
||||
|
||||
class ResultInfo(BaseModel):
|
||||
list: List[AccountInfo] = Field(..., description="List of accounts")
|
||||
|
||||
class AccountResponseSchema(BaseModel):
|
||||
retCode: int = Field(..., description="Return code (0 means success)")
|
||||
retMsg: str = Field(..., description="Return message")
|
||||
result: ResultInfo = Field(..., description="Detailed result")
|
||||
retExtInfo: Dict[str, Any] = Field(..., description="Extra information")
|
||||
time: int = Field(..., description="Response timestamp")
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Any
|
||||
|
||||
class CandleSchema(BaseModel):
|
||||
timestamp: int = Field(..., description="Timestamp (milliseconds)")
|
||||
open: float = Field(..., description="Open price")
|
||||
high: float = Field(..., description="High price")
|
||||
low: float = Field(..., description="Low price")
|
||||
close: float = Field(..., description="Close price")
|
||||
volume: float = Field(..., description="Volume")
|
||||
turnover: float = Field(..., description="Turnover value")
|
||||
macd: Optional[float] = Field(None, description="MACD value")
|
||||
macdsignal: Optional[float] = Field(None, description="MACD signal")
|
||||
macdhist: Optional[float] = Field(None, description="MACD histogram")
|
||||
ema34: Optional[float] = Field(None, description="EMA 34")
|
||||
ema50: Optional[float] = Field(None, description="EMA 50")
|
||||
ema100: Optional[float] = Field(None, description="EMA 100")
|
||||
ema200: Optional[float] = Field(None, description="EMA 200")
|
||||
rsi: Optional[float] = Field(None, description="RSI")
|
||||
|
||||
class CandleResponseSchema(BaseModel):
|
||||
data: List[CandleSchema]
|
||||
indicators: Any
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from pybit.unified_trading import HTTP
|
||||
|
||||
def get_bybit_client(api_key: str, api_secret: str):
|
||||
return HTTP(
|
||||
testnet=False, # Đổi thành False nếu dùng môi trường thật
|
||||
api_key=api_key,
|
||||
api_secret=api_secret
|
||||
)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from supabase import create_client, Client
|
||||
from app.config import SUPABASE_URL, SUPABASE_KEY
|
||||
|
||||
def get_supabase_client() -> Client:
|
||||
return create_client(SUPABASE_URL, SUPABASE_KEY)
|
||||
15
example.env
Normal file
15
example.env
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Supabase
|
||||
SUPABASE_URL=https://tqsoqmohvzrbnwhvgczh.supabase.co
|
||||
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRxc29xbW9odnpyYm53aHZnY3poIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE4Nzc5MzYsImV4cCI6MjA2NzQ1MzkzNn0._JXiBlYiHBmINzEr61Pzm6_GKCJJnOkvv4EUBNxfSIs
|
||||
LARK_APP_ID=cli_a8b73c970679d028
|
||||
LARK_APP_SECRET=fRfGwMieW59Q1B5KTyNKNh4oP1YWuQOr
|
||||
# Bybit (nếu muốn dùng mặc định, có thể để trống nếu lấy theo user)
|
||||
BYBIT_API_KEY=your-bybit-api-key
|
||||
BYBIT_API_SECRET=your-bybit-api-secret
|
||||
BYBIT_TESTNET=true
|
||||
|
||||
# Lark webhook
|
||||
LARK_WEBHOOK_URL=https://open.larksuite.com/webhook/your-webhook-url
|
||||
|
||||
# Server port
|
||||
PORT=3000
|
||||
94
openapi.yaml
Normal file
94
openapi.yaml
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: AI Trading System API
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/api/user/credential:
|
||||
post:
|
||||
summary: Lưu credential Bybit cho user
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
userId:
|
||||
type: string
|
||||
apiKey:
|
||||
type: string
|
||||
apiSecret:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Thành công
|
||||
get:
|
||||
summary: Lấy credential Bybit theo userId
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Thành công
|
||||
/api/orders:
|
||||
post:
|
||||
summary: Tạo order mới
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
symbol:
|
||||
type: string
|
||||
side:
|
||||
type: string
|
||||
qty:
|
||||
type: number
|
||||
responses:
|
||||
'200':
|
||||
description: Thành công
|
||||
get:
|
||||
summary: Lấy danh sách order
|
||||
responses:
|
||||
'200':
|
||||
description: Thành công
|
||||
/api/orders/{orderId}:
|
||||
delete:
|
||||
summary: Huỷ order
|
||||
parameters:
|
||||
- in: path
|
||||
name: orderId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Thành công
|
||||
/api/positions:
|
||||
get:
|
||||
summary: Lấy vị thế hiện tại
|
||||
responses:
|
||||
'200':
|
||||
description: Thành công
|
||||
/api/candles/{symbol}/{interval}:
|
||||
get:
|
||||
summary: Phân tích nến với EMA, MACD
|
||||
parameters:
|
||||
- in: path
|
||||
name: symbol
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: interval
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Thành công
|
||||
3710
package-lock.json
generated
Normal file
3710
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
39
package.json
Normal file
39
package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "ai-trading-sys",
|
||||
"version": "1.0.0",
|
||||
"description": "## Yêu cầu - Python 3.8+ - Tài khoản Supabase - API key Bybit",
|
||||
"main": "src/app.ts",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@larksuiteoapi/node-sdk": "^1.52.0",
|
||||
"@supabase/supabase-js": "^2.39.7",
|
||||
"axios": "^1.6.7",
|
||||
"bybit-api": "^3.2.0",
|
||||
"dotenv": "^17.0.1",
|
||||
"express": "^4.18.2",
|
||||
"node-schedule": "^2.1.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"technicalindicators": "^3.0.0",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "ts-node src/app.ts",
|
||||
"build": "tsc",
|
||||
"dev": "nodemon --watch src --ext ts --exec ts-node src/app.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.14.4",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/node-schedule": "^2.1.7",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/yamljs": "^0.2.34",
|
||||
"nodemon": "^3.1.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
aiohappyeyeballs==2.6.1
|
||||
aiohttp==3.11.18
|
||||
aiosignal==1.3.2
|
||||
annotated-types==0.7.0
|
||||
anyio==4.9.0
|
||||
attrs==25.3.0
|
||||
certifi==2025.4.26
|
||||
charset-normalizer==3.4.2
|
||||
click==8.2.1
|
||||
colorama==0.4.6
|
||||
deprecation==2.1.0
|
||||
fastapi==0.115.12
|
||||
frozenlist==1.6.0
|
||||
gotrue==2.12.0
|
||||
h11==0.16.0
|
||||
h2==4.2.0
|
||||
hpack==4.1.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
hyperframe==6.1.0
|
||||
idna==3.10
|
||||
iniconfig==2.1.0
|
||||
multidict==6.4.4
|
||||
numpy==2.2.6
|
||||
packaging==25.0
|
||||
pandas==2.2.3
|
||||
pluggy==1.6.0
|
||||
postgrest==1.0.2
|
||||
propcache==0.3.1
|
||||
pybit==5.10.1
|
||||
pycryptodome==3.23.0
|
||||
pydantic==2.11.4
|
||||
pydantic_core==2.33.2
|
||||
PyJWT==2.10.1
|
||||
pytest==8.3.5
|
||||
pytest-mock==3.14.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.1.0
|
||||
pytz==2025.2
|
||||
realtime==2.4.3
|
||||
requests==2.32.3
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
starlette==0.46.2
|
||||
storage3==0.11.3
|
||||
StrEnum==0.4.15
|
||||
supabase==2.15.1
|
||||
supafunc==0.9.4
|
||||
typing-inspection==0.4.1
|
||||
typing_extensions==4.13.2
|
||||
tzdata==2025.2
|
||||
urllib3==2.4.0
|
||||
uvicorn==0.34.2
|
||||
websocket-client==1.8.0
|
||||
websockets==14.2
|
||||
yarl==1.20.0
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Remove old container if exists
|
||||
if [ $(docker ps -a -q -f name=ai-trading-sys) ]; then
|
||||
docker rm -f ai-trading-sys
|
||||
fi
|
||||
|
||||
docker build -t ai-trading-sys .
|
||||
docker run -d -p 5100:8000 --name ai-trading-sys ai-trading-sys
|
||||
8
src/api/candles.ts
Normal file
8
src/api/candles.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Router } from 'express';
|
||||
import * as candleHandler from '../handlers/candleHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/:symbol/:interval', candleHandler.analyzeCandles);
|
||||
|
||||
export default router;
|
||||
10
src/api/orders.ts
Normal file
10
src/api/orders.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Router } from 'express';
|
||||
import * as orderHandler from '../handlers/orderHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/', orderHandler.createOrder);
|
||||
router.get('/', orderHandler.listOrders);
|
||||
router.delete('/:orderId', orderHandler.cancelOrder);
|
||||
|
||||
export default router;
|
||||
8
src/api/positions.ts
Normal file
8
src/api/positions.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Router } from 'express';
|
||||
import * as positionHandler from '../handlers/positionHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', positionHandler.listPositions);
|
||||
|
||||
export default router;
|
||||
9
src/api/user.ts
Normal file
9
src/api/user.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Router } from 'express';
|
||||
import * as userHandler from '../handlers/userHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/credential', userHandler.saveCredential);
|
||||
router.get('/credential/:userId', userHandler.getCredential);
|
||||
|
||||
export default router;
|
||||
27
src/app.ts
Normal file
27
src/app.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import 'dotenv/config';
|
||||
import express, { Application } from 'express';
|
||||
import userApi from './api/user';
|
||||
import ordersApi from './api/orders';
|
||||
import positionsApi from './api/positions';
|
||||
import candlesApi from './api/candles';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import YAML from 'yamljs';
|
||||
import './schedule/candleAnalysisSchedule';
|
||||
|
||||
|
||||
const app: Application = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.use('/api/user', userApi);
|
||||
app.use('/api/orders', ordersApi);
|
||||
app.use('/api/positions', positionsApi);
|
||||
app.use('/api/candles', candlesApi);
|
||||
|
||||
const swaggerDocument = YAML.load('./openapi.yaml');
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
export default app;
|
||||
0
src/dao/analysis.ts
Normal file
0
src/dao/analysis.ts
Normal file
10
src/dao/candles.ts
Normal file
10
src/dao/candles.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export interface Candle {
|
||||
time: string;
|
||||
timestamp: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
turnover: number;
|
||||
}
|
||||
11
src/handlers/candleHandler.ts
Normal file
11
src/handlers/candleHandler.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { BybitService } from '../services/bybitService';
|
||||
import * as indicatorService from '../services/indicatorService';
|
||||
|
||||
export const analyzeCandles = async (req: Request, res: Response) => {
|
||||
const { symbol, interval } = req.params;
|
||||
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!);
|
||||
const candles = await bybitService.getCandles({ symbol, interval: '5', category: 'linear', limit: 200 });
|
||||
const analysis = indicatorService.analyze(candles);
|
||||
res.json(analysis);
|
||||
};
|
||||
21
src/handlers/orderHandler.ts
Normal file
21
src/handlers/orderHandler.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { BybitService } from '../services/bybitService';
|
||||
|
||||
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!);
|
||||
|
||||
export const createOrder = async (req: Request, res: Response) => {
|
||||
// TODO: Lấy client từ credential
|
||||
const order = await bybitService.createOrder(req.body);
|
||||
res.json(order);
|
||||
};
|
||||
|
||||
export const listOrders = async (req: Request, res: Response) => {
|
||||
const orders = await bybitService.listOrders({ category: 'linear' });
|
||||
res.json(orders);
|
||||
};
|
||||
|
||||
export const cancelOrder = async (req: Request, res: Response) => {
|
||||
const { orderId } = req.params;
|
||||
const result = await bybitService.cancelOrder({ orderId, symbol: 'BTCUSDT', category: 'linear' });
|
||||
res.json(result);
|
||||
};
|
||||
8
src/handlers/positionHandler.ts
Normal file
8
src/handlers/positionHandler.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { BybitService } from '../services/bybitService';
|
||||
|
||||
export const listPositions = async (req: Request, res: Response) => {
|
||||
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!);
|
||||
const positions = await bybitService.listPositions({ category: 'linear' });
|
||||
res.json(positions);
|
||||
};
|
||||
14
src/handlers/userHandler.ts
Normal file
14
src/handlers/userHandler.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Request, Response } from 'express';
|
||||
import * as supabaseService from '../services/supabaseService';
|
||||
|
||||
export const saveCredential = async (req: Request, res: Response) => {
|
||||
const { userId, apiKey, apiSecret } = req.body;
|
||||
const result = await supabaseService.saveBybitCredential(userId, apiKey, apiSecret);
|
||||
res.json(result);
|
||||
};
|
||||
|
||||
export const getCredential = async (req: Request, res: Response) => {
|
||||
const { userId } = req.params;
|
||||
const result = await supabaseService.getBybitCredential(userId);
|
||||
res.json(result);
|
||||
};
|
||||
26
src/schedule/candleAnalysisSchedule.ts
Normal file
26
src/schedule/candleAnalysisSchedule.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import schedule from 'node-schedule';
|
||||
import * as indicatorService from '../services/indicatorService';
|
||||
import { BybitService } from '../services/bybitService';
|
||||
import { KlineIntervalV3 } from 'bybit-api';
|
||||
import { sendLarkMessage } from '../services/messageService';
|
||||
|
||||
// Hàm thực hiện phân tích nến
|
||||
async function analyzeCandlesJob(symbol: string, interval: KlineIntervalV3) {
|
||||
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!, false);
|
||||
// TODO: Lấy client thật nếu cần
|
||||
const candles = await bybitService.getCandles({ symbol, interval, category: 'linear', limit: 200 });
|
||||
const analysis = indicatorService.analyze(candles);
|
||||
console.log(`[${new Date().toISOString()}] Phân tích nến ${symbol} ${interval}:`, analysis);
|
||||
await sendLarkMessage('oc_f9b2e8f0309ecab0c94e3e134b0ddd29', JSON.stringify(analysis));
|
||||
// Có thể gửi kết quả qua Lark, lưu DB, ...
|
||||
}
|
||||
|
||||
// Lập lịch chạy vào 00:00 mỗi ngày
|
||||
const rule = new schedule.RecurrenceRule();
|
||||
rule.minute = [4, 9, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59];
|
||||
rule.second = 59;
|
||||
|
||||
schedule.scheduleJob(rule, () => {
|
||||
// Có thể lặp qua nhiều symbol/interval nếu muốn
|
||||
analyzeCandlesJob('BTCUSDT', '5');
|
||||
});
|
||||
69
src/services/bybitService.ts
Normal file
69
src/services/bybitService.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { CancelOrderParamsV5, GetAccountOrdersParamsV5, GetKlineParamsV5, OrderParamsV5, RestClientV5 } from 'bybit-api';
|
||||
import { Candle } from '../dao/candles';
|
||||
export class BybitService {
|
||||
private client: RestClientV5;
|
||||
|
||||
constructor(apiKey: string, apiSecret: string, testnet: boolean = true) {
|
||||
this.client = new RestClientV5({
|
||||
key: apiKey,
|
||||
secret: apiSecret,
|
||||
testnet
|
||||
});
|
||||
}
|
||||
|
||||
async createOrder(orderData: OrderParamsV5): Promise<any> {
|
||||
try {
|
||||
const res = await this.client.submitOrder(orderData);
|
||||
return { success: true, data: res };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async listOrders(params: GetAccountOrdersParamsV5): Promise<any[]> {
|
||||
try {
|
||||
const res = await this.client.getActiveOrders(params);
|
||||
return res.result?.list || [];
|
||||
} catch (error: any) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async cancelOrder(params: CancelOrderParamsV5): Promise<any> {
|
||||
try {
|
||||
const res = await this.client.cancelOrder(params);
|
||||
return { success: true, data: res };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async listPositions(params: any): Promise<any[]> {
|
||||
try {
|
||||
const res = await this.client.getPositionInfo(params);
|
||||
return res.result?.list || [];
|
||||
} catch (error: any) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getCandles(params: GetKlineParamsV5): Promise<Candle[]> {
|
||||
try {
|
||||
const res = await this.client.getKline(params);
|
||||
const list = res.result?.list || [];
|
||||
// Bybit trả về: [timestamp, open, high, low, close, volume, turnover]
|
||||
return list.map((item: any[]) => ({
|
||||
time: new Date(Number(item[0])).toISOString(),
|
||||
timestamp: Number(item[0]),
|
||||
open: Number(item[1]),
|
||||
high: Number(item[2]),
|
||||
low: Number(item[3]),
|
||||
close: Number(item[4]),
|
||||
volume: Number(item[5]),
|
||||
turnover: Number(item[6])
|
||||
}));
|
||||
} catch (error: any) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/services/indicatorService.ts
Normal file
27
src/services/indicatorService.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { EMA, MACD } from 'technicalindicators';
|
||||
import { Candle } from '../dao/candles';
|
||||
|
||||
export function analyze(candles: Candle[]): { ema34: number; ema200: number; macd: any } {
|
||||
let close = candles.map(c => c.close);
|
||||
const ema34 = EMA.calculate({ period: 34, values: close, reversedInput: true });
|
||||
const ema200 = EMA.calculate({ period: 200, values: close, reversedInput: true });
|
||||
const macd = MACD.calculate({
|
||||
values: close,
|
||||
fastPeriod: 45,
|
||||
slowPeriod: 90,
|
||||
signalPeriod: 9,
|
||||
SimpleMAOscillator: false,
|
||||
SimpleMASignal: false,
|
||||
reversedInput: true
|
||||
});
|
||||
const candle = candles[0];
|
||||
if (candle.low < ema200[0] && candle.high > ema200[0] && candle.close > ema200[0]) {
|
||||
console.log('Buy');
|
||||
}
|
||||
|
||||
if (candle.high > ema200[0] && candle.low < ema200[0] && candle.close < ema200[0]) {
|
||||
console.log('Sell');
|
||||
}
|
||||
|
||||
return { ema34: ema34[0], ema200: ema200[0], macd: macd[0] };
|
||||
}
|
||||
22
src/services/messageService.ts
Normal file
22
src/services/messageService.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Client } from '@larksuiteoapi/node-sdk';
|
||||
|
||||
export async function sendLarkMessage(receiveId: string, message: string): Promise<any> {
|
||||
console.log(process.env.LARK_APP_ID!, process.env.LARK_APP_SECRET!);
|
||||
const lark = new Client({
|
||||
appId: process.env.LARK_APP_ID!,
|
||||
appSecret: process.env.LARK_APP_SECRET!
|
||||
});
|
||||
|
||||
const res = await lark.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: receiveId,
|
||||
content: JSON.stringify({text: message}),
|
||||
msg_type: 'text'
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, data: res.data };
|
||||
}
|
||||
16
src/services/supabaseService.ts
Normal file
16
src/services/supabaseService.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabase: SupabaseClient = createClient(
|
||||
process.env.SUPABASE_URL || '',
|
||||
process.env.SUPABASE_KEY || ''
|
||||
);
|
||||
|
||||
export async function saveBybitCredential(userId: string, apiKey: string, apiSecret: string): Promise<any> {
|
||||
// TODO: Lưu credential vào Supabase
|
||||
return { success: true, message: 'Saved (mock)' };
|
||||
}
|
||||
|
||||
export async function getBybitCredential(userId: string): Promise<any> {
|
||||
// TODO: Lấy credential từ Supabase
|
||||
return { userId, apiKey: 'demo', apiSecret: 'demo' };
|
||||
}
|
||||
14
test.ts
Normal file
14
test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import 'dotenv/config';
|
||||
import { BybitService } from './src/services/bybitService';
|
||||
import * as indicatorService from './src/services/indicatorService';
|
||||
import { sendLarkMessage } from './src/services/messageService';
|
||||
|
||||
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!, false);
|
||||
(async () => {
|
||||
const candles = await bybitService.getCandles({ symbol: 'BTCUSDT', interval: '15', category: 'linear', limit: 500 });
|
||||
console.log(candles[0], candles[1]);
|
||||
const analysis = indicatorService.analyze(candles);
|
||||
console.log(analysis);
|
||||
const message = JSON.stringify(analysis);
|
||||
await sendLarkMessage('oc_f9b2e8f0309ecab0c94e3e134b0ddd29', message);
|
||||
})();
|
||||
113
tsconfig.json
Normal file
113
tsconfig.json
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "libReplacement": true, /* Enable lib replacement. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue