@aicoin/aicoin-mcp 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +302 -12
- package/package.json +2 -1
- package/scripts/ft-deploy.mjs +433 -0
package/build/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync } from "fs";
|
|
5
|
-
import { fileURLToPath as
|
|
6
|
-
import { dirname as
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
|
|
@@ -44,12 +44,12 @@ var BROKER_CONFIG = {
|
|
|
44
44
|
headers: {}
|
|
45
45
|
},
|
|
46
46
|
bybit: {
|
|
47
|
-
options: {},
|
|
48
|
-
headers: {
|
|
47
|
+
options: { brokerId: "AiCoin" },
|
|
48
|
+
headers: {}
|
|
49
49
|
},
|
|
50
50
|
bitget: {
|
|
51
|
-
options: {},
|
|
52
|
-
headers: {
|
|
51
|
+
options: { broker: "tpequ" },
|
|
52
|
+
headers: {}
|
|
53
53
|
},
|
|
54
54
|
gate: {
|
|
55
55
|
options: {},
|
|
@@ -600,7 +600,7 @@ function registerTradeTools(server2) {
|
|
|
600
600
|
);
|
|
601
601
|
server2.tool(
|
|
602
602
|
"create_order",
|
|
603
|
-
"Place a new order (market or limit). For conditional SL/TP orders, use stop_loss_price or take_profit_price with type=market.",
|
|
603
|
+
"Place a new order (market or limit). Default returns a PREVIEW (confirmed=false). Set confirmed=true to actually execute. For conditional SL/TP orders, use stop_loss_price or take_profit_price with type=market.",
|
|
604
604
|
{
|
|
605
605
|
exchange: z2.string().describe("Exchange ID"),
|
|
606
606
|
symbol: z2.string().describe("Trading pair, e.g. BTC/USDT"),
|
|
@@ -608,6 +608,7 @@ function registerTradeTools(server2) {
|
|
|
608
608
|
side: z2.enum(["buy", "sell"]).describe("Order side"),
|
|
609
609
|
amount: z2.number().positive().describe("Order amount"),
|
|
610
610
|
price: z2.number().positive().optional().describe("Limit price (required for limit orders)"),
|
|
611
|
+
confirmed: z2.boolean().optional().default(false).describe("false (default) = preview only, true = execute order"),
|
|
611
612
|
pos_side: z2.enum(["long", "short"]).optional().describe("Position side for hedge mode: long/short"),
|
|
612
613
|
margin_mode: z2.enum(["cross", "isolated"]).optional().describe("Margin mode for derivatives: cross/isolated"),
|
|
613
614
|
stop_loss_price: z2.number().positive().optional().describe("Stop-loss trigger price (creates conditional market order)"),
|
|
@@ -615,11 +616,47 @@ function registerTradeTools(server2) {
|
|
|
615
616
|
reduce_only: z2.boolean().optional().describe("Reduce-only flag for closing positions (SL/TP orders)"),
|
|
616
617
|
market_type: marketTypePrivateSchema
|
|
617
618
|
},
|
|
618
|
-
async ({ exchange, symbol, type, side, amount, price, pos_side, margin_mode, stop_loss_price, take_profit_price, reduce_only, market_type }) => {
|
|
619
|
+
async ({ exchange, symbol, type, side, amount, price, confirmed, pos_side, margin_mode, stop_loss_price, take_profit_price, reduce_only, market_type }) => {
|
|
619
620
|
try {
|
|
620
621
|
if (type === "limit" && price == null) {
|
|
621
622
|
return err("price is required for limit orders");
|
|
622
623
|
}
|
|
624
|
+
if (!confirmed) {
|
|
625
|
+
const pub = getExchange(exchange, market_type, { skipAuth: true });
|
|
626
|
+
await pub.loadMarkets();
|
|
627
|
+
const mkt = pub.markets[symbol];
|
|
628
|
+
if (!mkt) return err(`Symbol '${symbol}' not found on ${exchange}`);
|
|
629
|
+
const ticker = await pub.fetchTicker(symbol);
|
|
630
|
+
const currentPrice = ticker.last ?? ticker.close ?? 0;
|
|
631
|
+
const orderPrice = type === "limit" ? price : currentPrice;
|
|
632
|
+
const contractSize = mkt.contractSize ?? 1;
|
|
633
|
+
const isDerivative = market_type && market_type !== "spot";
|
|
634
|
+
const amountInBase = isDerivative ? amount * contractSize : amount;
|
|
635
|
+
const notional = amountInBase * orderPrice;
|
|
636
|
+
const preview = {
|
|
637
|
+
_preview: true,
|
|
638
|
+
_confirm_hint: "Set confirmed=true to execute this order",
|
|
639
|
+
exchange,
|
|
640
|
+
symbol,
|
|
641
|
+
type,
|
|
642
|
+
side,
|
|
643
|
+
amount,
|
|
644
|
+
price: orderPrice,
|
|
645
|
+
current_price: currentPrice,
|
|
646
|
+
notional_value: `${notional.toFixed(2)} ${mkt.quote}`
|
|
647
|
+
};
|
|
648
|
+
if (isDerivative && contractSize !== 1) {
|
|
649
|
+
preview.contract_size = contractSize;
|
|
650
|
+
preview.amount_in_base = `${amountInBase} ${mkt.base}`;
|
|
651
|
+
preview.unit = `${amount} contracts \xD7 ${contractSize} ${mkt.base}/contract = ${amountInBase} ${mkt.base}`;
|
|
652
|
+
}
|
|
653
|
+
if (mkt.limits?.amount?.min) preview.min_amount = mkt.limits.amount.min;
|
|
654
|
+
if (stop_loss_price) preview.stop_loss_price = stop_loss_price;
|
|
655
|
+
if (take_profit_price) preview.take_profit_price = take_profit_price;
|
|
656
|
+
if (pos_side) preview.pos_side = pos_side;
|
|
657
|
+
if (reduce_only) preview.reduce_only = true;
|
|
658
|
+
return ok(preview);
|
|
659
|
+
}
|
|
623
660
|
const ex = getExchange(exchange, market_type);
|
|
624
661
|
const params = {};
|
|
625
662
|
const isBinance = exchange?.toLowerCase().startsWith("binance");
|
|
@@ -3449,6 +3486,257 @@ function registerFreqtradeDevTools(server2) {
|
|
|
3449
3486
|
);
|
|
3450
3487
|
}
|
|
3451
3488
|
|
|
3489
|
+
// src/tools/ft-deploy.ts
|
|
3490
|
+
import { z as z11 } from "zod";
|
|
3491
|
+
import { execSync } from "child_process";
|
|
3492
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3493
|
+
import { dirname as dirname2, resolve } from "path";
|
|
3494
|
+
var __dir = dirname2(fileURLToPath2(import.meta.url));
|
|
3495
|
+
var SCRIPT = resolve(__dir, "..", "scripts", "ft-deploy.mjs");
|
|
3496
|
+
function registerFtDeployTools(server2) {
|
|
3497
|
+
server2.tool(
|
|
3498
|
+
"ft_deploy",
|
|
3499
|
+
"Freqtrade one-click deployment & management.\n\u2022 check \u2014 verify Python, git, exchange keys, running status\n\u2022 deploy \u2014 full setup (clone, install, config, start). Params: dry_run, strategy, pairs, trading_mode\n\u2022 update \u2014 update Freqtrade via setup.sh\n\u2022 status \u2014 check if running, show last logs\n\u2022 start \u2014 start Freqtrade. Params: strategy\n\u2022 stop \u2014 stop Freqtrade\n\u2022 logs \u2014 view recent logs. Params: lines\n\u2022 backtest \u2014 run strategy backtest. Params: strategy, timeframe, timerange\n\u2022 download_data \u2014 download OHLCV data. Params: timeframe, timerange\n\u2022 remove \u2014 stop and remove (config preserved)",
|
|
3500
|
+
{
|
|
3501
|
+
action: z11.enum([
|
|
3502
|
+
"check",
|
|
3503
|
+
"deploy",
|
|
3504
|
+
"update",
|
|
3505
|
+
"status",
|
|
3506
|
+
"start",
|
|
3507
|
+
"stop",
|
|
3508
|
+
"logs",
|
|
3509
|
+
"backtest",
|
|
3510
|
+
"download_data",
|
|
3511
|
+
"remove"
|
|
3512
|
+
]).describe("Action to perform"),
|
|
3513
|
+
strategy: z11.string().optional().describe("Strategy name (default: SampleStrategy)"),
|
|
3514
|
+
timeframe: z11.string().optional().describe("For backtest/download_data: candle timeframe, e.g. 1h, 5m"),
|
|
3515
|
+
timerange: z11.string().optional().describe("For backtest/download_data: date range, e.g. 20240101-20240601"),
|
|
3516
|
+
dry_run: z11.boolean().optional().describe("For deploy: true=paper trading (default), false=live"),
|
|
3517
|
+
pairs: z11.array(z11.string()).optional().describe('For deploy: trading pairs, e.g. ["BTC/USDT:USDT"]'),
|
|
3518
|
+
trading_mode: z11.enum(["futures", "spot"]).optional().describe("For deploy: trading mode (default: futures)"),
|
|
3519
|
+
lines: z11.number().optional().describe("For logs: number of lines to show (default: 50)")
|
|
3520
|
+
},
|
|
3521
|
+
async ({ action, strategy, timeframe, timerange, dry_run, pairs, trading_mode, lines }) => {
|
|
3522
|
+
try {
|
|
3523
|
+
const params = {};
|
|
3524
|
+
if (strategy !== void 0) params.strategy = strategy;
|
|
3525
|
+
if (timeframe !== void 0) params.timeframe = timeframe;
|
|
3526
|
+
if (timerange !== void 0) params.timerange = timerange;
|
|
3527
|
+
if (dry_run !== void 0) params.dry_run = dry_run;
|
|
3528
|
+
if (pairs !== void 0) params.pairs = pairs;
|
|
3529
|
+
if (trading_mode !== void 0) params.trading_mode = trading_mode;
|
|
3530
|
+
if (lines !== void 0) params.lines = lines;
|
|
3531
|
+
const paramsArg = Object.keys(params).length > 0 ? ` '${JSON.stringify(params)}'` : "";
|
|
3532
|
+
const output = execSync(
|
|
3533
|
+
`node ${SCRIPT} ${action}${paramsArg}`,
|
|
3534
|
+
{ encoding: "utf-8", timeout: 6e5 }
|
|
3535
|
+
).trim();
|
|
3536
|
+
try {
|
|
3537
|
+
return ok(JSON.parse(output));
|
|
3538
|
+
} catch {
|
|
3539
|
+
return ok({ output });
|
|
3540
|
+
}
|
|
3541
|
+
} catch (e) {
|
|
3542
|
+
return err(e);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
);
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
// src/tools/auto-trade.ts
|
|
3549
|
+
import { z as z12 } from "zod";
|
|
3550
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
3551
|
+
import { resolve as resolve2 } from "path";
|
|
3552
|
+
var CONFIG_DIR = resolve2(process.env.HOME || "", ".aicoin-mcp");
|
|
3553
|
+
var CONFIG_PATH = resolve2(CONFIG_DIR, "trade-config.json");
|
|
3554
|
+
var DEFAULT_CONFIG = {
|
|
3555
|
+
exchange: "okx",
|
|
3556
|
+
symbol: "BTC/USDT:USDT",
|
|
3557
|
+
market_type: "swap",
|
|
3558
|
+
capital_pct: 0.5,
|
|
3559
|
+
leverage: 20,
|
|
3560
|
+
stop_loss_pct: 0.025,
|
|
3561
|
+
take_profit_pct: 0.05
|
|
3562
|
+
};
|
|
3563
|
+
function loadConfig() {
|
|
3564
|
+
if (existsSync(CONFIG_PATH)) {
|
|
3565
|
+
try {
|
|
3566
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) };
|
|
3567
|
+
} catch {
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
return { ...DEFAULT_CONFIG };
|
|
3571
|
+
}
|
|
3572
|
+
function saveConfig(cfg) {
|
|
3573
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
3574
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
3575
|
+
}
|
|
3576
|
+
function registerAutoTradeTools(server2) {
|
|
3577
|
+
server2.tool(
|
|
3578
|
+
"auto_trade",
|
|
3579
|
+
"Automated trading with risk management. AI agent decides strategy, this tool executes.\n\u2022 setup \u2014 save trading config (exchange, symbol, leverage, SL/TP%)\n\u2022 status \u2014 show config + balance + positions + open orders\n\u2022 open \u2014 execute trade: calculate position \u2192 set leverage \u2192 market order \u2192 SL/TP. Requires: direction (long/short)\n\u2022 close \u2014 cancel open orders \u2192 close position",
|
|
3580
|
+
{
|
|
3581
|
+
action: z12.enum(["setup", "status", "open", "close"]).describe(
|
|
3582
|
+
"setup: save config; status: show state; open: execute trade; close: close position"
|
|
3583
|
+
),
|
|
3584
|
+
direction: z12.enum(["long", "short"]).optional().describe("REQUIRED for open. Trade direction decided by AI agent"),
|
|
3585
|
+
exchange: z12.string().optional().describe("Exchange ID override"),
|
|
3586
|
+
symbol: z12.string().optional().describe("Trading pair override, e.g. BTC/USDT:USDT"),
|
|
3587
|
+
market_type: z12.enum(["spot", "future", "swap"]).optional().describe("Market type override"),
|
|
3588
|
+
capital_pct: z12.number().min(0.01).max(1).optional().describe("Capital allocation ratio (0.01-1.0)"),
|
|
3589
|
+
leverage: z12.number().int().min(1).max(125).optional().describe("Leverage multiplier"),
|
|
3590
|
+
stop_loss_pct: z12.number().min(1e-3).max(0.5).optional().describe("Stop-loss percentage (e.g. 0.025 = 2.5%)"),
|
|
3591
|
+
take_profit_pct: z12.number().min(1e-3).max(1).optional().describe("Take-profit percentage (e.g. 0.05 = 5%)")
|
|
3592
|
+
},
|
|
3593
|
+
async ({ action, direction, exchange, symbol, market_type, capital_pct, leverage, stop_loss_pct, take_profit_pct }) => {
|
|
3594
|
+
try {
|
|
3595
|
+
const overrides = {};
|
|
3596
|
+
if (exchange !== void 0) overrides.exchange = exchange;
|
|
3597
|
+
if (symbol !== void 0) overrides.symbol = symbol;
|
|
3598
|
+
if (market_type !== void 0) overrides.market_type = market_type;
|
|
3599
|
+
if (capital_pct !== void 0) overrides.capital_pct = capital_pct;
|
|
3600
|
+
if (leverage !== void 0) overrides.leverage = leverage;
|
|
3601
|
+
if (stop_loss_pct !== void 0) overrides.stop_loss_pct = stop_loss_pct;
|
|
3602
|
+
if (take_profit_pct !== void 0) overrides.take_profit_pct = take_profit_pct;
|
|
3603
|
+
const cfg = { ...loadConfig(), ...overrides };
|
|
3604
|
+
switch (action) {
|
|
3605
|
+
case "setup": {
|
|
3606
|
+
saveConfig(cfg);
|
|
3607
|
+
return ok({ saved: CONFIG_PATH, config: cfg });
|
|
3608
|
+
}
|
|
3609
|
+
case "status": {
|
|
3610
|
+
const result = { config: cfg };
|
|
3611
|
+
const ex = getExchange(cfg.exchange, cfg.market_type);
|
|
3612
|
+
try {
|
|
3613
|
+
const bal = await ex.fetchBalance();
|
|
3614
|
+
const stablecoins = ["USDT", "USDC", "BUSD", "DAI", "TUSD", "FDUSD"];
|
|
3615
|
+
const summary = {};
|
|
3616
|
+
for (const [ccy, amt] of Object.entries(bal.total || {})) {
|
|
3617
|
+
const total = Number(amt);
|
|
3618
|
+
if (total <= 0) continue;
|
|
3619
|
+
if (stablecoins.includes(ccy) && total < 0.01) continue;
|
|
3620
|
+
if (!stablecoins.includes(ccy) && total < 1e-7) continue;
|
|
3621
|
+
summary[ccy] = { free: bal.free?.[ccy], used: bal.used?.[ccy], total: bal.total?.[ccy] };
|
|
3622
|
+
}
|
|
3623
|
+
result.balance = summary;
|
|
3624
|
+
} catch (e) {
|
|
3625
|
+
result.balance = { error: e instanceof Error ? e.message : String(e) };
|
|
3626
|
+
}
|
|
3627
|
+
try {
|
|
3628
|
+
result.positions = await ex.fetchPositions([cfg.symbol]);
|
|
3629
|
+
} catch (e) {
|
|
3630
|
+
result.positions = { error: e instanceof Error ? e.message : String(e) };
|
|
3631
|
+
}
|
|
3632
|
+
try {
|
|
3633
|
+
result.open_orders = await ex.fetchOpenOrders(cfg.symbol);
|
|
3634
|
+
} catch (e) {
|
|
3635
|
+
result.open_orders = { error: e instanceof Error ? e.message : String(e) };
|
|
3636
|
+
}
|
|
3637
|
+
return ok(result);
|
|
3638
|
+
}
|
|
3639
|
+
case "open": {
|
|
3640
|
+
if (!direction || !["long", "short"].includes(direction)) {
|
|
3641
|
+
return err('Missing "direction": must be "long" or "short"');
|
|
3642
|
+
}
|
|
3643
|
+
const ex = getExchange(cfg.exchange, cfg.market_type);
|
|
3644
|
+
const bal = await ex.fetchBalance();
|
|
3645
|
+
const quote = cfg.symbol.split("/")[1]?.split(":")[0] || "USDT";
|
|
3646
|
+
const available = Number(bal.free?.[quote] || 0);
|
|
3647
|
+
if (available < 1) return err(`Insufficient ${quote} balance: ${available}`);
|
|
3648
|
+
const ticker = await ex.fetchTicker(cfg.symbol);
|
|
3649
|
+
const price = ticker.last ?? ticker.close ?? 0;
|
|
3650
|
+
if (!price) return err("Could not fetch current price");
|
|
3651
|
+
await ex.loadMarkets();
|
|
3652
|
+
const mkt = ex.markets[cfg.symbol];
|
|
3653
|
+
if (!mkt) return err(`Symbol '${cfg.symbol}' not found on ${cfg.exchange}`);
|
|
3654
|
+
const contractSize = mkt.contractSize || 1;
|
|
3655
|
+
const amountStep = mkt.precision?.amount || 0.01;
|
|
3656
|
+
const amountMin = mkt.limits?.amount?.min || amountStep;
|
|
3657
|
+
const capital = available * cfg.capital_pct;
|
|
3658
|
+
const positionValue = capital * cfg.leverage;
|
|
3659
|
+
const amountInBase = positionValue / price;
|
|
3660
|
+
const rawAmount = cfg.market_type !== "spot" && contractSize ? amountInBase / contractSize : amountInBase;
|
|
3661
|
+
const amount = Math.max(Math.floor(rawAmount / amountStep) * amountStep, amountMin);
|
|
3662
|
+
if (amount * contractSize * price < 1) {
|
|
3663
|
+
return err(`Position too small: ${amount} contracts \u2248 ${(amount * contractSize).toFixed(6)} ${mkt.base}`);
|
|
3664
|
+
}
|
|
3665
|
+
try {
|
|
3666
|
+
await ex.setLeverage(cfg.leverage, cfg.symbol);
|
|
3667
|
+
} catch {
|
|
3668
|
+
}
|
|
3669
|
+
const side = direction === "long" ? "buy" : "sell";
|
|
3670
|
+
const order = await ex.createOrder(cfg.symbol, "market", side, amount);
|
|
3671
|
+
const slPrice = direction === "long" ? price * (1 - cfg.stop_loss_pct) : price * (1 + cfg.stop_loss_pct);
|
|
3672
|
+
const tpPrice = direction === "long" ? price * (1 + cfg.take_profit_pct) : price * (1 - cfg.take_profit_pct);
|
|
3673
|
+
const closeSide = direction === "long" ? "sell" : "buy";
|
|
3674
|
+
let sl, tp;
|
|
3675
|
+
try {
|
|
3676
|
+
sl = await ex.createOrder(cfg.symbol, "market", closeSide, amount, void 0, {
|
|
3677
|
+
stopLossPrice: Number(slPrice.toPrecision(6)),
|
|
3678
|
+
reduceOnly: true
|
|
3679
|
+
});
|
|
3680
|
+
} catch (e) {
|
|
3681
|
+
sl = { error: e instanceof Error ? e.message : String(e) };
|
|
3682
|
+
}
|
|
3683
|
+
try {
|
|
3684
|
+
tp = await ex.createOrder(cfg.symbol, "market", closeSide, amount, void 0, {
|
|
3685
|
+
takeProfitPrice: Number(tpPrice.toPrecision(6)),
|
|
3686
|
+
reduceOnly: true
|
|
3687
|
+
});
|
|
3688
|
+
} catch (e) {
|
|
3689
|
+
tp = { error: e instanceof Error ? e.message : String(e) };
|
|
3690
|
+
}
|
|
3691
|
+
return ok({
|
|
3692
|
+
direction,
|
|
3693
|
+
amount,
|
|
3694
|
+
amount_base: `${Number((amount * contractSize).toPrecision(4))} ${mkt.base}`,
|
|
3695
|
+
contract_size: contractSize !== 1 ? `1 contract = ${contractSize} ${mkt.base}` : null,
|
|
3696
|
+
entry_price: price,
|
|
3697
|
+
stop_loss: Number(slPrice.toPrecision(6)),
|
|
3698
|
+
take_profit: Number(tpPrice.toPrecision(6)),
|
|
3699
|
+
order_id: order.id,
|
|
3700
|
+
sl_order: sl?.id ?? sl?.error,
|
|
3701
|
+
tp_order: tp?.id ?? tp?.error,
|
|
3702
|
+
capital_used: capital.toFixed(2),
|
|
3703
|
+
position_value: positionValue.toFixed(2)
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
case "close": {
|
|
3707
|
+
const ex = getExchange(cfg.exchange, cfg.market_type);
|
|
3708
|
+
try {
|
|
3709
|
+
if (ex.has["cancelAllOrders"]) {
|
|
3710
|
+
await ex.cancelAllOrders(cfg.symbol);
|
|
3711
|
+
} else {
|
|
3712
|
+
const open = await ex.fetchOpenOrders(cfg.symbol);
|
|
3713
|
+
await Promise.allSettled(open.map((o) => ex.cancelOrder(o.id, cfg.symbol)));
|
|
3714
|
+
}
|
|
3715
|
+
} catch {
|
|
3716
|
+
}
|
|
3717
|
+
const positions = await ex.fetchPositions([cfg.symbol]);
|
|
3718
|
+
const pos = positions.find(
|
|
3719
|
+
(p) => p.symbol === cfg.symbol && Math.abs(Number(p.contracts || 0)) > 0
|
|
3720
|
+
);
|
|
3721
|
+
if (!pos) return ok({ closed: false, reason: "No open position" });
|
|
3722
|
+
const amount = Math.abs(Number(pos.contracts));
|
|
3723
|
+
const closeSide = Number(pos.contracts) > 0 ? "sell" : "buy";
|
|
3724
|
+
const order = await ex.createOrder(cfg.symbol, "market", closeSide, amount);
|
|
3725
|
+
return ok({
|
|
3726
|
+
closed: true,
|
|
3727
|
+
side: closeSide,
|
|
3728
|
+
amount,
|
|
3729
|
+
order_id: order.id
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
} catch (e) {
|
|
3734
|
+
return err(e);
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
);
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3452
3740
|
// src/tools/index.ts
|
|
3453
3741
|
function registerAllTools(server2) {
|
|
3454
3742
|
registerTradeTools(server2);
|
|
@@ -3460,13 +3748,15 @@ function registerAllTools(server2) {
|
|
|
3460
3748
|
registerGuideTools(server2);
|
|
3461
3749
|
registerFreqtradeTools(server2);
|
|
3462
3750
|
registerFreqtradeDevTools(server2);
|
|
3751
|
+
registerFtDeployTools(server2);
|
|
3752
|
+
registerAutoTradeTools(server2);
|
|
3463
3753
|
}
|
|
3464
3754
|
|
|
3465
3755
|
// src/index.ts
|
|
3466
|
-
var __dirname =
|
|
3756
|
+
var __dirname = dirname3(fileURLToPath3(import.meta.url));
|
|
3467
3757
|
var pkg = JSON.parse(
|
|
3468
|
-
|
|
3469
|
-
|
|
3758
|
+
readFileSync2(
|
|
3759
|
+
resolve3(__dirname, "..", "package.json"),
|
|
3470
3760
|
"utf-8"
|
|
3471
3761
|
)
|
|
3472
3762
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aicoin/aicoin-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "AiCoin MCP Server - unified crypto market data & trading via AiCoin API + CCXT",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"build",
|
|
12
|
+
"scripts",
|
|
12
13
|
"python",
|
|
13
14
|
"README.md",
|
|
14
15
|
".env.example"
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Freqtrade One-Click Deployment via git clone + setup.sh (official method)
|
|
3
|
+
// Reads exchange keys from env vars, creates config, starts as background process
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
const FT_DIR = resolve(process.env.HOME || '', '.freqtrade');
|
|
10
|
+
const SRC_DIR = resolve(FT_DIR, 'source');
|
|
11
|
+
const VENV_DIR = resolve(SRC_DIR, '.venv');
|
|
12
|
+
const USER_DATA = resolve(FT_DIR, 'user_data');
|
|
13
|
+
const STRAT_DIR = resolve(USER_DATA, 'strategies');
|
|
14
|
+
const CONFIG_PATH = resolve(USER_DATA, 'config.json');
|
|
15
|
+
const PID_FILE = resolve(FT_DIR, 'freqtrade.pid');
|
|
16
|
+
const LOG_FILE = resolve(FT_DIR, 'freqtrade.log');
|
|
17
|
+
const API_PORT = process.env.FREQTRADE_PORT || '8080';
|
|
18
|
+
|
|
19
|
+
const FT_BIN = resolve(VENV_DIR, 'bin', 'freqtrade');
|
|
20
|
+
|
|
21
|
+
function run(cmd, opts = {}) {
|
|
22
|
+
return execSync(cmd, { encoding: 'utf-8', timeout: 600000, ...opts }).trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasCommand(cmd) {
|
|
26
|
+
try { run(`which ${cmd}`); return true; } catch { return false; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Find the best Python >= 3.11 (Freqtrade requirement)
|
|
30
|
+
function findPython() {
|
|
31
|
+
for (const bin of ['python3.13', 'python3.12', 'python3.11', 'python3']) {
|
|
32
|
+
try {
|
|
33
|
+
const version = run(`${bin} --version`);
|
|
34
|
+
const match = version.match(/(\d+)\.(\d+)/);
|
|
35
|
+
if (match) {
|
|
36
|
+
const major = Number(match[1]), minor = Number(match[2]);
|
|
37
|
+
if (major === 3 && minor >= 11) return { bin, major, minor, version };
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Ensure Python 3.11+ is available (Freqtrade requirement)
|
|
45
|
+
function ensureModernPython() {
|
|
46
|
+
let py = findPython();
|
|
47
|
+
if (py) return py;
|
|
48
|
+
|
|
49
|
+
// No Python 3.11+ found — try to install
|
|
50
|
+
if (process.platform === 'darwin') {
|
|
51
|
+
if (!hasCommand('brew')) {
|
|
52
|
+
throw new Error('Homebrew not found. Install: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"');
|
|
53
|
+
}
|
|
54
|
+
console.error('Python 3.11+ not found. Installing Python 3.12 via Homebrew...');
|
|
55
|
+
const brewEnv = { ...process.env, HOMEBREW_NO_AUTO_UPDATE: '1', HOMEBREW_NO_INSTALL_CLEANUP: '1' };
|
|
56
|
+
run('brew install python@3.12', { timeout: 300000, env: brewEnv });
|
|
57
|
+
py = findPython();
|
|
58
|
+
if (py) return py;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new Error('Python 3.11+ required. Install: brew install python@3.12 (macOS) or apt install python3.12 python3.12-venv (Linux)');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Exchange & config ───
|
|
65
|
+
|
|
66
|
+
function detectExchange() {
|
|
67
|
+
const exchanges = ['BINANCE', 'OKX', 'BYBIT', 'BITGET', 'GATE', 'HTX', 'KUCOIN', 'MEXC'];
|
|
68
|
+
for (const ex of exchanges) {
|
|
69
|
+
if (process.env[`${ex}_API_KEY`] && process.env[`${ex}_SECRET`]) {
|
|
70
|
+
return {
|
|
71
|
+
name: ex.toLowerCase(),
|
|
72
|
+
key: process.env[`${ex}_API_KEY`],
|
|
73
|
+
secret: process.env[`${ex}_SECRET`],
|
|
74
|
+
password: process.env[`${ex}_PASSPHRASE`] || '',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function generateConfig(exchangeInfo, apiPassword, params = {}) {
|
|
82
|
+
const config = {
|
|
83
|
+
trading_mode: params.trading_mode || 'futures',
|
|
84
|
+
margin_mode: params.margin_mode || 'isolated',
|
|
85
|
+
max_open_trades: params.max_open_trades || 3,
|
|
86
|
+
stake_currency: 'USDT',
|
|
87
|
+
stake_amount: params.stake_amount || 'unlimited',
|
|
88
|
+
tradable_balance_ratio: params.tradable_balance_ratio || 0.5,
|
|
89
|
+
dry_run: params.dry_run !== false,
|
|
90
|
+
dry_run_wallet: 1000,
|
|
91
|
+
cancel_open_orders_on_exit: false,
|
|
92
|
+
exchange: {
|
|
93
|
+
name: exchangeInfo.name,
|
|
94
|
+
key: exchangeInfo.key,
|
|
95
|
+
secret: exchangeInfo.secret,
|
|
96
|
+
...(exchangeInfo.password ? { password: exchangeInfo.password } : {}),
|
|
97
|
+
ccxt_config: {},
|
|
98
|
+
ccxt_async_config: {},
|
|
99
|
+
pair_whitelist: params.pairs || ['BTC/USDT:USDT', 'ETH/USDT:USDT'],
|
|
100
|
+
pair_blacklist: [],
|
|
101
|
+
},
|
|
102
|
+
pairlists: [{ method: 'StaticPairList' }],
|
|
103
|
+
entry_pricing: { price_side: 'same', use_order_book: true, order_book_top: 1 },
|
|
104
|
+
exit_pricing: { price_side: 'same', use_order_book: true, order_book_top: 1 },
|
|
105
|
+
api_server: {
|
|
106
|
+
enabled: true,
|
|
107
|
+
listen_ip_address: '127.0.0.1',
|
|
108
|
+
listen_port: Number(API_PORT),
|
|
109
|
+
verbosity: 'error',
|
|
110
|
+
enable_openapi: false,
|
|
111
|
+
jwt_secret_key: randomBytes(16).toString('hex'),
|
|
112
|
+
CORS_origins: [],
|
|
113
|
+
username: 'freqtrader',
|
|
114
|
+
password: apiPassword,
|
|
115
|
+
},
|
|
116
|
+
bot_name: 'aicoin-freqtrade',
|
|
117
|
+
initial_state: 'running',
|
|
118
|
+
force_entry_enable: true,
|
|
119
|
+
internals: { process_throttle_secs: 5 },
|
|
120
|
+
};
|
|
121
|
+
const proxyUrl = process.env.PROXY_URL || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
|
122
|
+
if (proxyUrl) {
|
|
123
|
+
config.exchange.ccxt_config.proxies = { https: proxyUrl, http: proxyUrl };
|
|
124
|
+
config.exchange.ccxt_async_config.aiohttp_proxy = proxyUrl;
|
|
125
|
+
// HTTP proxies don't support WebSocket — disable WS to force REST polling
|
|
126
|
+
config.exchange.enable_ws = false;
|
|
127
|
+
}
|
|
128
|
+
return config;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getPid() {
|
|
132
|
+
if (!existsSync(PID_FILE)) return null;
|
|
133
|
+
const pid = readFileSync(PID_FILE, 'utf-8').trim();
|
|
134
|
+
if (!pid) return null;
|
|
135
|
+
try { process.kill(Number(pid), 0); return Number(pid); } catch { return null; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Actions ───
|
|
139
|
+
|
|
140
|
+
const actions = {
|
|
141
|
+
check: async () => {
|
|
142
|
+
const checks = {};
|
|
143
|
+
// Python — needs 3.11+
|
|
144
|
+
const py = findPython();
|
|
145
|
+
checks.python = py ? `${py.version} (${py.bin})` : false;
|
|
146
|
+
if (!py) {
|
|
147
|
+
try {
|
|
148
|
+
const v = run('python3 --version');
|
|
149
|
+
checks.python_warning = `${v} found but Freqtrade requires 3.11+. Deploy will auto-install 3.12.`;
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
// git
|
|
153
|
+
checks.git = hasCommand('git');
|
|
154
|
+
// Freqtrade source
|
|
155
|
+
checks.source_cloned = existsSync(resolve(SRC_DIR, 'setup.sh'));
|
|
156
|
+
// Freqtrade installed
|
|
157
|
+
checks.freqtrade_installed = existsSync(FT_BIN);
|
|
158
|
+
if (checks.freqtrade_installed) {
|
|
159
|
+
try { checks.freqtrade_version = run(`${FT_BIN} --version`); } catch {}
|
|
160
|
+
}
|
|
161
|
+
// Exchange keys
|
|
162
|
+
const ex = detectExchange();
|
|
163
|
+
checks.exchange = ex ? { name: ex.name, configured: true } : { configured: false };
|
|
164
|
+
// Running
|
|
165
|
+
const pid = getPid();
|
|
166
|
+
checks.running = !!pid;
|
|
167
|
+
if (pid) checks.pid = pid;
|
|
168
|
+
|
|
169
|
+
checks.ready = (!!py || process.platform === 'darwin') && checks.git && checks.exchange?.configured;
|
|
170
|
+
if (!checks.ready) {
|
|
171
|
+
checks.missing = [];
|
|
172
|
+
if (!py && process.platform !== 'darwin') checks.missing.push('Python 3.11+ not found');
|
|
173
|
+
if (!checks.git) checks.missing.push('git not found');
|
|
174
|
+
if (!checks.exchange?.configured) checks.missing.push('No exchange API keys in env');
|
|
175
|
+
}
|
|
176
|
+
return checks;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
deploy: async (params = {}) => {
|
|
180
|
+
// 1. Ensure Python 3.11+ (auto-installs on macOS if needed)
|
|
181
|
+
const py = ensureModernPython();
|
|
182
|
+
console.error(`Using ${py.version} (${py.bin})`);
|
|
183
|
+
|
|
184
|
+
// 2. Ensure git
|
|
185
|
+
if (!hasCommand('git')) throw new Error('git not found. Install: apt install git (Linux) or xcode-select --install (macOS)');
|
|
186
|
+
|
|
187
|
+
// 3. Detect exchange
|
|
188
|
+
const exchangeInfo = detectExchange();
|
|
189
|
+
if (!exchangeInfo) throw new Error('No exchange API keys found in env');
|
|
190
|
+
|
|
191
|
+
// 4. Create directories
|
|
192
|
+
mkdirSync(STRAT_DIR, { recursive: true });
|
|
193
|
+
|
|
194
|
+
// 5. Clone + install Freqtrade via official setup.sh
|
|
195
|
+
if (!existsSync(FT_BIN)) {
|
|
196
|
+
if (!existsSync(resolve(SRC_DIR, 'setup.sh'))) {
|
|
197
|
+
console.error('Cloning Freqtrade repository...');
|
|
198
|
+
run(`git clone https://github.com/freqtrade/freqtrade.git ${SRC_DIR}`, { timeout: 120000 });
|
|
199
|
+
run(`cd ${SRC_DIR} && git checkout stable`, { timeout: 30000 });
|
|
200
|
+
}
|
|
201
|
+
console.error('Running Freqtrade setup.sh (this may take a few minutes)...');
|
|
202
|
+
run(`cd ${SRC_DIR} && ./setup.sh -i`, { timeout: 600000 });
|
|
203
|
+
if (!existsSync(FT_BIN)) {
|
|
204
|
+
throw new Error('Freqtrade installation failed. Check output above for errors.');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 6. Generate config
|
|
209
|
+
const apiPassword = randomBytes(8).toString('hex');
|
|
210
|
+
const config = generateConfig(exchangeInfo, apiPassword, params);
|
|
211
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
212
|
+
|
|
213
|
+
// 7. Create sample strategy if none exists
|
|
214
|
+
const samplePath = resolve(STRAT_DIR, 'SampleStrategy.py');
|
|
215
|
+
if (!existsSync(samplePath)) {
|
|
216
|
+
writeFileSync(samplePath, SAMPLE_STRATEGY);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 8. Stop existing process
|
|
220
|
+
const oldPid = getPid();
|
|
221
|
+
if (oldPid) { try { process.kill(oldPid, 'SIGTERM'); } catch {} }
|
|
222
|
+
|
|
223
|
+
// 9. Start freqtrade as background process (with proxy env vars)
|
|
224
|
+
const strategy = params.strategy || 'SampleStrategy';
|
|
225
|
+
const proxyEnv = process.env.PROXY_URL || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
|
226
|
+
const proxyPrefix = proxyEnv ? `env HTTPS_PROXY=${proxyEnv} HTTP_PROXY=${proxyEnv} ` : '';
|
|
227
|
+
run(`nohup ${proxyPrefix}${FT_BIN} trade --config ${CONFIG_PATH} --strategy ${strategy} --userdir ${USER_DATA} > ${LOG_FILE} 2>&1 & echo $! > ${PID_FILE}`);
|
|
228
|
+
|
|
229
|
+
// 10. Wait for startup
|
|
230
|
+
let ready = false;
|
|
231
|
+
for (let i = 0; i < 15; i++) {
|
|
232
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
233
|
+
const pid = getPid();
|
|
234
|
+
if (pid) {
|
|
235
|
+
try {
|
|
236
|
+
const res = await fetch(`http://127.0.0.1:${API_PORT}/api/v1/ping`, {
|
|
237
|
+
headers: { Authorization: 'Basic ' + Buffer.from(`freqtrader:${apiPassword}`).toString('base64') },
|
|
238
|
+
signal: AbortSignal.timeout(3000),
|
|
239
|
+
});
|
|
240
|
+
if (res.ok) { ready = true; break; }
|
|
241
|
+
} catch {}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
success: true,
|
|
247
|
+
exchange: exchangeInfo.name,
|
|
248
|
+
strategy,
|
|
249
|
+
dry_run: config.dry_run,
|
|
250
|
+
pairs: config.exchange.pair_whitelist,
|
|
251
|
+
api_url: `http://127.0.0.1:${API_PORT}`,
|
|
252
|
+
api_password: apiPassword,
|
|
253
|
+
pid: getPid(),
|
|
254
|
+
ready,
|
|
255
|
+
log_file: LOG_FILE,
|
|
256
|
+
config_path: CONFIG_PATH,
|
|
257
|
+
strategies_dir: STRAT_DIR,
|
|
258
|
+
note: config.dry_run
|
|
259
|
+
? 'Running in DRY-RUN mode (no real money). Use deploy with {"dry_run":false} for live trading.'
|
|
260
|
+
: 'WARNING: Running in LIVE mode with real money!',
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
update: async () => {
|
|
265
|
+
if (!existsSync(resolve(SRC_DIR, 'setup.sh'))) {
|
|
266
|
+
return { error: 'Freqtrade not installed. Run deploy first.' };
|
|
267
|
+
}
|
|
268
|
+
const pid = getPid();
|
|
269
|
+
if (pid) { try { process.kill(pid, 'SIGTERM'); } catch {} }
|
|
270
|
+
console.error('Updating Freqtrade...');
|
|
271
|
+
run(`cd ${SRC_DIR} && ./setup.sh -u`, { timeout: 600000 });
|
|
272
|
+
return { updated: true, note: 'Run start to restart Freqtrade.' };
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
status: async () => {
|
|
276
|
+
const pid = getPid();
|
|
277
|
+
if (!pid) return { running: false };
|
|
278
|
+
let lastLogs = '';
|
|
279
|
+
try { lastLogs = run(`tail -5 ${LOG_FILE} 2>/dev/null`); } catch {}
|
|
280
|
+
return { running: true, pid, log_file: LOG_FILE, last_logs: lastLogs };
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
stop: async () => {
|
|
284
|
+
const pid = getPid();
|
|
285
|
+
if (!pid) return { stopped: false, reason: 'Not running' };
|
|
286
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
287
|
+
try { writeFileSync(PID_FILE, ''); } catch {}
|
|
288
|
+
return { stopped: true, pid };
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
start: async (params = {}) => {
|
|
292
|
+
if (getPid()) return { started: false, reason: 'Already running' };
|
|
293
|
+
if (!existsSync(FT_BIN)) throw new Error('Freqtrade not installed. Run deploy first.');
|
|
294
|
+
if (!existsSync(CONFIG_PATH)) throw new Error('No config found. Run deploy first.');
|
|
295
|
+
const strategy = params.strategy || 'SampleStrategy';
|
|
296
|
+
const proxyUrl = process.env.PROXY_URL || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
|
297
|
+
const proxyPrefix = proxyUrl ? `env HTTPS_PROXY=${proxyUrl} HTTP_PROXY=${proxyUrl} ` : '';
|
|
298
|
+
run(`nohup ${proxyPrefix}${FT_BIN} trade --config ${CONFIG_PATH} --strategy ${strategy} --userdir ${USER_DATA} > ${LOG_FILE} 2>&1 & echo $! > ${PID_FILE}`);
|
|
299
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
300
|
+
return { started: true, pid: getPid() };
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
logs: async (params = {}) => {
|
|
304
|
+
const lines = params.lines || 50;
|
|
305
|
+
try { return { logs: run(`tail -${lines} ${LOG_FILE} 2>/dev/null`) }; } catch { return { logs: 'No log file found' }; }
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
backtest: async (params = {}) => {
|
|
309
|
+
if (!existsSync(FT_BIN)) throw new Error('Freqtrade not installed. Run deploy first.');
|
|
310
|
+
if (!existsSync(CONFIG_PATH)) throw new Error('No config found. Run deploy first.');
|
|
311
|
+
const strategy = params.strategy || 'SampleStrategy';
|
|
312
|
+
const timeframe = params.timeframe || '1h';
|
|
313
|
+
const timerange = params.timerange || '';
|
|
314
|
+
const timerangeArg = timerange ? ` --timerange ${timerange}` : '';
|
|
315
|
+
|
|
316
|
+
const proxyEnv = process.env.PROXY_URL || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
|
317
|
+
const proxyPrefix = proxyEnv ? `env HTTPS_PROXY=${proxyEnv} HTTP_PROXY=${proxyEnv} ` : '';
|
|
318
|
+
|
|
319
|
+
console.error('Downloading historical data...');
|
|
320
|
+
try {
|
|
321
|
+
run(
|
|
322
|
+
`${proxyPrefix}${FT_BIN} download-data --config ${CONFIG_PATH} --timeframe ${timeframe}${timerangeArg} --userdir ${USER_DATA}`,
|
|
323
|
+
{ timeout: 300000 }
|
|
324
|
+
);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
console.error(`Data download warning: ${e.message}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.error(`Running backtest: strategy=${strategy}, timeframe=${timeframe}${timerange ? `, timerange=${timerange}` : ''}...`);
|
|
330
|
+
const output = run(
|
|
331
|
+
`${proxyPrefix}${FT_BIN} backtesting --config ${CONFIG_PATH} --strategy ${strategy} --timeframe ${timeframe}${timerangeArg} --userdir ${USER_DATA}`,
|
|
332
|
+
{ timeout: 600000 }
|
|
333
|
+
);
|
|
334
|
+
return { strategy, timeframe, timerange: timerange || 'all available', output };
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
download_data: async (params = {}) => {
|
|
338
|
+
if (!existsSync(FT_BIN)) throw new Error('Freqtrade not installed. Run deploy first.');
|
|
339
|
+
if (!existsSync(CONFIG_PATH)) throw new Error('No config found. Run deploy first.');
|
|
340
|
+
const timeframe = params.timeframe || '1h';
|
|
341
|
+
const timerange = params.timerange || '';
|
|
342
|
+
const timerangeArg = timerange ? ` --timerange ${timerange}` : '';
|
|
343
|
+
|
|
344
|
+
const proxyEnv = process.env.PROXY_URL || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
|
345
|
+
const proxyPrefix = proxyEnv ? `env HTTPS_PROXY=${proxyEnv} HTTP_PROXY=${proxyEnv} ` : '';
|
|
346
|
+
|
|
347
|
+
console.error(`Downloading data: timeframe=${timeframe}${timerange ? `, timerange=${timerange}` : ''}...`);
|
|
348
|
+
const output = run(
|
|
349
|
+
`${proxyPrefix}${FT_BIN} download-data --config ${CONFIG_PATH} --timeframe ${timeframe}${timerangeArg} --userdir ${USER_DATA}`,
|
|
350
|
+
{ timeout: 300000 }
|
|
351
|
+
);
|
|
352
|
+
return { timeframe, timerange: timerange || 'all available', output };
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
remove: async () => {
|
|
356
|
+
const pid = getPid();
|
|
357
|
+
if (pid) { try { process.kill(pid, 'SIGTERM'); } catch {} }
|
|
358
|
+
try { writeFileSync(PID_FILE, ''); } catch {}
|
|
359
|
+
return { removed: true, note: `Process stopped. Config preserved at ${FT_DIR}. To fully remove: rm -rf ${FT_DIR}` };
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// ─── Sample strategy (pure pandas, no TA-Lib dependency) ───
|
|
364
|
+
|
|
365
|
+
const SAMPLE_STRATEGY = `# Sample RSI + EMA strategy for Freqtrade
|
|
366
|
+
# Uses pure pandas — no TA-Lib C library required
|
|
367
|
+
from freqtrade.strategy import IStrategy
|
|
368
|
+
from pandas import DataFrame
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class SampleStrategy(IStrategy):
|
|
372
|
+
INTERFACE_VERSION = 3
|
|
373
|
+
timeframe = '5m'
|
|
374
|
+
can_short = True
|
|
375
|
+
|
|
376
|
+
minimal_roi = {"0": 0.05, "30": 0.03, "60": 0.02, "120": 0.01}
|
|
377
|
+
|
|
378
|
+
stoploss = -0.03
|
|
379
|
+
trailing_stop = True
|
|
380
|
+
trailing_stop_positive = 0.01
|
|
381
|
+
trailing_stop_positive_offset = 0.02
|
|
382
|
+
|
|
383
|
+
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
384
|
+
# RSI (pure pandas, no talib)
|
|
385
|
+
delta = dataframe['close'].diff()
|
|
386
|
+
gain = delta.clip(lower=0).rolling(window=14).mean()
|
|
387
|
+
loss = (-delta.clip(upper=0)).rolling(window=14).mean()
|
|
388
|
+
rs = gain / loss
|
|
389
|
+
dataframe['rsi'] = 100 - (100 / (1 + rs))
|
|
390
|
+
|
|
391
|
+
# EMA (pure pandas)
|
|
392
|
+
dataframe['ema_fast'] = dataframe['close'].ewm(span=8, adjust=False).mean()
|
|
393
|
+
dataframe['ema_slow'] = dataframe['close'].ewm(span=21, adjust=False).mean()
|
|
394
|
+
return dataframe
|
|
395
|
+
|
|
396
|
+
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
397
|
+
dataframe.loc[
|
|
398
|
+
(dataframe['rsi'] < 35) &
|
|
399
|
+
(dataframe['ema_fast'] > dataframe['ema_slow']) &
|
|
400
|
+
(dataframe['volume'] > 0),
|
|
401
|
+
'enter_long'] = 1
|
|
402
|
+
dataframe.loc[
|
|
403
|
+
(dataframe['rsi'] > 65) &
|
|
404
|
+
(dataframe['ema_fast'] < dataframe['ema_slow']) &
|
|
405
|
+
(dataframe['volume'] > 0),
|
|
406
|
+
'enter_short'] = 1
|
|
407
|
+
return dataframe
|
|
408
|
+
|
|
409
|
+
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
410
|
+
dataframe.loc[
|
|
411
|
+
(dataframe['rsi'] > 70),
|
|
412
|
+
'exit_long'] = 1
|
|
413
|
+
dataframe.loc[
|
|
414
|
+
(dataframe['rsi'] < 30),
|
|
415
|
+
'exit_short'] = 1
|
|
416
|
+
return dataframe
|
|
417
|
+
`;
|
|
418
|
+
|
|
419
|
+
// ─── CLI ───
|
|
420
|
+
|
|
421
|
+
const [action, ...rest] = process.argv.slice(2);
|
|
422
|
+
if (!action || !actions[action]) {
|
|
423
|
+
console.log(`Usage: node ft-deploy.mjs <action> [json-params]\nActions: ${Object.keys(actions).join(', ')}`);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
const params = rest.length ? JSON.parse(rest.join(' ')) : {};
|
|
427
|
+
actions[action](params).then(r => {
|
|
428
|
+
r._reminder = 'IMPORTANT: Always use ft_deploy tool for ALL Freqtrade operations. NEVER use Docker commands.';
|
|
429
|
+
console.log(JSON.stringify(r, null, 2));
|
|
430
|
+
}).catch(e => {
|
|
431
|
+
console.error(e.message);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
});
|