Trading_BOT_GOLD
This commit is contained in:
parent
81d72b560c
commit
d5d42e9a7e
@ -3,6 +3,7 @@ const express = require('express');
|
||||
|
||||
const Bot_sessionsService = require('../services/bot_sessions');
|
||||
const Bot_sessionsDBApi = require('../db/api/bot_sessions');
|
||||
const TradingBotWorkspaceService = require('../services/tradingBotWorkspace');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
|
||||
|
||||
@ -17,6 +18,34 @@ const {
|
||||
|
||||
router.use(checkCrudPermissions('bot_sessions'));
|
||||
|
||||
router.get('/workspace/bootstrap', wrapAsync(async (req, res) => {
|
||||
const payload = await TradingBotWorkspaceService.getBootstrapData(req.currentUser);
|
||||
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
router.post('/workspace/launch', wrapAsync(async (req, res) => {
|
||||
const payload = await TradingBotWorkspaceService.launchSession(req.body.data || {}, req.currentUser);
|
||||
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
router.get('/:id/workspace', wrapAsync(async (req, res) => {
|
||||
const payload = await TradingBotWorkspaceService.getWorkspaceById(req.params.id, req.currentUser);
|
||||
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
router.post('/:id/runtime-action', wrapAsync(async (req, res) => {
|
||||
const payload = await TradingBotWorkspaceService.applyRuntimeAction(
|
||||
req.params.id,
|
||||
req.currentUser,
|
||||
req.body.action,
|
||||
);
|
||||
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
|
||||
943
backend/src/services/tradingBotWorkspace.js
Normal file
943
backend/src/services/tradingBotWorkspace.js
Normal file
@ -0,0 +1,943 @@
|
||||
const db = require('../db/models');
|
||||
|
||||
const STRATEGY_CATEGORY_LABELS = {
|
||||
moving_averages: 'Medias móviles',
|
||||
breakout: 'Ruptura',
|
||||
price_action: 'Acción del precio',
|
||||
pullback: 'Retroceso',
|
||||
mean_reversion: 'Reversión a la media',
|
||||
news_trading: 'Trading de noticias',
|
||||
};
|
||||
|
||||
const SIGNAL_LABELS = {
|
||||
buy: 'Compra',
|
||||
sell: 'Venta',
|
||||
close: 'Cierre',
|
||||
hold: 'Espera',
|
||||
};
|
||||
|
||||
const STATUS_LABELS = {
|
||||
created: 'Creado',
|
||||
running: 'Activo',
|
||||
paused: 'Pausado',
|
||||
stopped: 'Detenido',
|
||||
error: 'Error',
|
||||
};
|
||||
|
||||
const ACTIVE_SESSION_STATUSES = ['running', 'paused'];
|
||||
|
||||
function badRequest(message) {
|
||||
const error = new Error(message);
|
||||
error.code = 400;
|
||||
return error;
|
||||
}
|
||||
|
||||
function toNumber(value, fallback = 0) {
|
||||
const numericValue = Number(value);
|
||||
return Number.isFinite(numericValue) ? numericValue : fallback;
|
||||
}
|
||||
|
||||
function round(value, digits = 2) {
|
||||
return Number(toNumber(value).toFixed(digits));
|
||||
}
|
||||
|
||||
function pickByIndex(items = [], index = 0) {
|
||||
if (!items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return items[index % items.length];
|
||||
}
|
||||
|
||||
function getReferencePrice(symbol) {
|
||||
if (!symbol) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (symbol.includes('BTC')) {
|
||||
return 68250;
|
||||
}
|
||||
|
||||
if (symbol.includes('ETH')) {
|
||||
return 3380;
|
||||
}
|
||||
|
||||
if (symbol.includes('SOL')) {
|
||||
return 155;
|
||||
}
|
||||
|
||||
return 1.12;
|
||||
}
|
||||
|
||||
function getConfidenceLevel(score) {
|
||||
if (score >= 80) {
|
||||
return 'high';
|
||||
}
|
||||
|
||||
if (score >= 65) {
|
||||
return 'medium';
|
||||
}
|
||||
|
||||
return 'low';
|
||||
}
|
||||
|
||||
function getSignalBias(category) {
|
||||
switch (category) {
|
||||
case 'moving_averages':
|
||||
return 0.22;
|
||||
case 'breakout':
|
||||
return 0.35;
|
||||
case 'price_action':
|
||||
return 0.18;
|
||||
case 'pullback':
|
||||
return 0.12;
|
||||
case 'mean_reversion':
|
||||
return 0.16;
|
||||
case 'news_trading':
|
||||
return 0.28;
|
||||
default:
|
||||
return 0.14;
|
||||
}
|
||||
}
|
||||
|
||||
function getSignalRationale(strategyName, categoryLabel, pairSymbol, timeframe, updateIndex) {
|
||||
const rationaleTemplates = [
|
||||
`${strategyName} detectó mechas con rechazo claro en ${pairSymbol} dentro de ${timeframe}.`,
|
||||
`${categoryLabel} encontró una compresión con ruptura probable y volumen relativo creciente.`,
|
||||
`El bot reforzó la lectura con contexto intradía e índice de impulso en fase ${updateIndex % 2 === 0 ? 'alcista' : 'defensiva'}.`,
|
||||
`La señal se validó con control de riesgo dinámico y revisión de beneficio esperado.`,
|
||||
];
|
||||
|
||||
return rationaleTemplates[updateIndex % rationaleTemplates.length];
|
||||
}
|
||||
|
||||
async function fetchSessionContext(sessionId, currentUser) {
|
||||
const session = await db.bot_sessions.findOne({
|
||||
where: {
|
||||
id: sessionId,
|
||||
userId: currentUser.id,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: db.exchanges,
|
||||
as: 'exchange',
|
||||
attributes: ['id', 'name', 'exchange_code', 'supports_spot', 'supports_futures'],
|
||||
},
|
||||
{
|
||||
model: db.api_credentials,
|
||||
as: 'api_credential',
|
||||
attributes: ['id', 'label', 'last_validated_at', 'exchangeId'],
|
||||
},
|
||||
{
|
||||
model: db.trading_pairs,
|
||||
as: 'trading_pair',
|
||||
attributes: ['id', 'symbol', 'base_asset', 'quote_asset', 'exchangeId'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [sessionStrategies, roiSnapshots, sessionEvents, tradeSignals, positions, orders] = await Promise.all([
|
||||
db.session_strategies.findAll({
|
||||
where: { bot_sessionId: session.id },
|
||||
include: [
|
||||
{
|
||||
model: db.strategies,
|
||||
as: 'strategy',
|
||||
attributes: ['id', 'name', 'category', 'description'],
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'ASC']],
|
||||
}),
|
||||
db.roi_snapshots.findAll({
|
||||
where: { bot_sessionId: session.id },
|
||||
order: [['period_end_at', 'DESC'], ['createdAt', 'DESC']],
|
||||
limit: 12,
|
||||
}),
|
||||
db.session_events.findAll({
|
||||
where: { bot_sessionId: session.id },
|
||||
order: [['event_at', 'DESC'], ['createdAt', 'DESC']],
|
||||
limit: 12,
|
||||
}),
|
||||
db.trade_signals.findAll({
|
||||
where: { bot_sessionId: session.id },
|
||||
include: [
|
||||
{
|
||||
model: db.strategies,
|
||||
as: 'strategy',
|
||||
attributes: ['id', 'name', 'category'],
|
||||
},
|
||||
],
|
||||
order: [['signaled_at', 'DESC'], ['createdAt', 'DESC']],
|
||||
limit: 8,
|
||||
}),
|
||||
db.positions.findAll({
|
||||
where: { bot_sessionId: session.id },
|
||||
order: [['opened_at', 'DESC'], ['createdAt', 'DESC']],
|
||||
limit: 6,
|
||||
}),
|
||||
db.orders.findAll({
|
||||
where: { bot_sessionId: session.id },
|
||||
order: [['submitted_at', 'DESC'], ['createdAt', 'DESC']],
|
||||
limit: 8,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
session,
|
||||
sessionStrategies,
|
||||
roiSnapshots,
|
||||
sessionEvents,
|
||||
tradeSignals,
|
||||
positions,
|
||||
orders,
|
||||
};
|
||||
}
|
||||
|
||||
function buildStrategyLeaderboard(context, currentEquity, latestSnapshot) {
|
||||
const signalCounts = context.tradeSignals.reduce((accumulator, signal) => {
|
||||
const strategyId = signal.strategyId;
|
||||
|
||||
if (!strategyId) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
return {
|
||||
...accumulator,
|
||||
[strategyId]: (accumulator[strategyId] || 0) + 1,
|
||||
};
|
||||
}, {});
|
||||
|
||||
return context.sessionStrategies.map((sessionStrategy, index) => {
|
||||
const strategy = sessionStrategy.strategy;
|
||||
const signalCount = signalCounts[strategy?.id] || 0;
|
||||
const latestRoi = latestSnapshot ? toNumber(latestSnapshot.roi_percent) : 0;
|
||||
const adaptiveScore = round(
|
||||
58 + (signalCount * 6) + (latestRoi * 2.5) + (toNumber(sessionStrategy.weight) * 0.12) - (index * 2),
|
||||
1,
|
||||
);
|
||||
const clampedScore = Math.max(48, Math.min(96, adaptiveScore));
|
||||
const allocation = round(toNumber(sessionStrategy.weight, 0), 1);
|
||||
|
||||
return {
|
||||
id: sessionStrategy.id,
|
||||
name: strategy?.name || 'Estrategia',
|
||||
category: STRATEGY_CATEGORY_LABELS[strategy?.category] || 'Estrategia activa',
|
||||
adaptiveScore: clampedScore,
|
||||
allocation,
|
||||
signalCount,
|
||||
recommendation: index === 0 ? 'Prioridad alta' : index === 1 ? 'Mantener' : 'Monitorizar',
|
||||
projectedEquity: round(currentEquity * (1 + (clampedScore - 60) / 800), 2),
|
||||
};
|
||||
}).sort((left, right) => right.adaptiveScore - left.adaptiveScore);
|
||||
}
|
||||
|
||||
function serializeWorkspace(context) {
|
||||
const latestSnapshot = context.roiSnapshots[0] || null;
|
||||
const oldestSnapshot = context.roiSnapshots[context.roiSnapshots.length - 1] || null;
|
||||
const openPositions = context.positions.filter((position) => position.status === 'open');
|
||||
const closedPositions = context.positions.filter((position) => position.status === 'closed');
|
||||
const currentEquity = latestSnapshot
|
||||
? toNumber(latestSnapshot.ending_equity)
|
||||
: toNumber(context.session.investment_amount);
|
||||
const startingEquity = oldestSnapshot
|
||||
? toNumber(oldestSnapshot.starting_equity)
|
||||
: toNumber(context.session.investment_amount);
|
||||
const pnlAmount = round(currentEquity - startingEquity, 2);
|
||||
const roiPercent = startingEquity > 0
|
||||
? round((pnlAmount / startingEquity) * 100, 2)
|
||||
: 0;
|
||||
const realizedPnl = round(
|
||||
context.positions.reduce((sum, position) => sum + toNumber(position.realized_pnl), 0),
|
||||
2,
|
||||
);
|
||||
const unrealizedPnl = round(
|
||||
context.positions.reduce((sum, position) => sum + toNumber(position.unrealized_pnl), 0),
|
||||
2,
|
||||
);
|
||||
const winRate = closedPositions.length
|
||||
? round((closedPositions.filter((position) => toNumber(position.realized_pnl) >= 0).length / closedPositions.length) * 100, 1)
|
||||
: context.tradeSignals.length
|
||||
? round(48 + Math.min(context.tradeSignals.length * 4, 28), 1)
|
||||
: 0;
|
||||
const latestHeartbeat = context.sessionEvents[0]?.event_at || context.session.updatedAt;
|
||||
|
||||
return {
|
||||
session: {
|
||||
id: context.session.id,
|
||||
status: context.session.status,
|
||||
statusLabel: STATUS_LABELS[context.session.status] || context.session.status,
|
||||
simulation_mode: context.session.simulation_mode,
|
||||
investment_amount: round(context.session.investment_amount, 2),
|
||||
risk_percent: round(context.session.risk_percent, 2),
|
||||
target_profit_percent: round(context.session.target_profit_percent, 2),
|
||||
timeframe: context.session.timeframe,
|
||||
notes: context.session.notes,
|
||||
started_at: context.session.started_at,
|
||||
stopped_at: context.session.stopped_at,
|
||||
latestHeartbeat,
|
||||
exchange: context.session.exchange,
|
||||
trading_pair: context.session.trading_pair,
|
||||
api_credential: context.session.api_credential
|
||||
? {
|
||||
id: context.session.api_credential.id,
|
||||
label: context.session.api_credential.label,
|
||||
last_validated_at: context.session.api_credential.last_validated_at,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
kpis: {
|
||||
currentEquity,
|
||||
roiPercent,
|
||||
pnlAmount,
|
||||
realizedPnl,
|
||||
unrealizedPnl,
|
||||
winRate,
|
||||
activePositions: openPositions.length,
|
||||
totalSignals: context.tradeSignals.length,
|
||||
activeStrategies: context.sessionStrategies.filter((strategy) => strategy.is_enabled).length,
|
||||
},
|
||||
roiTimeline: [...context.roiSnapshots]
|
||||
.reverse()
|
||||
.map((snapshot) => ({
|
||||
id: snapshot.id,
|
||||
roiPercent: round(snapshot.roi_percent, 2),
|
||||
pnlAmount: round(snapshot.profit_loss_amount, 2),
|
||||
startingEquity: round(snapshot.starting_equity, 2),
|
||||
endingEquity: round(snapshot.ending_equity, 2),
|
||||
periodStartAt: snapshot.period_start_at,
|
||||
periodEndAt: snapshot.period_end_at,
|
||||
closedTradesCount: snapshot.closed_trades_count || 0,
|
||||
})),
|
||||
eventFeed: context.sessionEvents.map((event) => ({
|
||||
id: event.id,
|
||||
eventType: event.event_type,
|
||||
severity: event.severity,
|
||||
message: event.message,
|
||||
payload: event.payload,
|
||||
eventAt: event.event_at,
|
||||
})),
|
||||
signals: context.tradeSignals.map((signal) => ({
|
||||
id: signal.id,
|
||||
type: signal.signal_type,
|
||||
typeLabel: SIGNAL_LABELS[signal.signal_type] || signal.signal_type,
|
||||
confidenceLevel: signal.confidence_level,
|
||||
confidenceScore: round(signal.confidence_score, 1),
|
||||
entryPrice: round(signal.suggested_entry_price, 4),
|
||||
stopLossPrice: round(signal.suggested_stop_loss_price, 4),
|
||||
takeProfitPrice: round(signal.suggested_take_profit_price, 4),
|
||||
rationale: signal.rationale,
|
||||
signaledAt: signal.signaled_at,
|
||||
strategy: signal.strategy
|
||||
? {
|
||||
id: signal.strategy.id,
|
||||
name: signal.strategy.name,
|
||||
category: STRATEGY_CATEGORY_LABELS[signal.strategy.category] || signal.strategy.category,
|
||||
}
|
||||
: null,
|
||||
})),
|
||||
positions: context.positions.map((position) => ({
|
||||
id: position.id,
|
||||
side: position.side,
|
||||
status: position.status,
|
||||
entryPrice: round(position.entry_price, 4),
|
||||
quantity: round(position.quantity, 6),
|
||||
stopLossPrice: round(position.stop_loss_price, 4),
|
||||
takeProfitPrice: round(position.take_profit_price, 4),
|
||||
realizedPnl: round(position.realized_pnl, 2),
|
||||
unrealizedPnl: round(position.unrealized_pnl, 2),
|
||||
openedAt: position.opened_at,
|
||||
closedAt: position.closed_at,
|
||||
})),
|
||||
orders: context.orders.map((order) => ({
|
||||
id: order.id,
|
||||
side: order.side,
|
||||
status: order.status,
|
||||
type: order.order_type,
|
||||
price: round(order.price, 4),
|
||||
quantity: round(order.quantity, 6),
|
||||
filledQuantity: round(order.filled_quantity, 6),
|
||||
avgFillPrice: round(order.avg_fill_price, 4),
|
||||
submittedAt: order.submitted_at,
|
||||
finalizedAt: order.finalized_at,
|
||||
isSimulated: order.is_simulated,
|
||||
})),
|
||||
strategyLeaderboard: buildStrategyLeaderboard(context, currentEquity, latestSnapshot),
|
||||
};
|
||||
}
|
||||
|
||||
async function summarizeSession(session) {
|
||||
const latestSnapshot = await db.roi_snapshots.findOne({
|
||||
where: { bot_sessionId: session.id },
|
||||
order: [['period_end_at', 'DESC'], ['createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
return {
|
||||
id: session.id,
|
||||
status: session.status,
|
||||
statusLabel: STATUS_LABELS[session.status] || session.status,
|
||||
simulation_mode: session.simulation_mode,
|
||||
investmentAmount: round(session.investment_amount, 2),
|
||||
timeframe: session.timeframe,
|
||||
startedAt: session.started_at,
|
||||
stoppedAt: session.stopped_at,
|
||||
roiPercent: latestSnapshot ? round(latestSnapshot.roi_percent, 2) : 0,
|
||||
pnlAmount: latestSnapshot ? round(latestSnapshot.profit_loss_amount, 2) : 0,
|
||||
exchange: session.exchange
|
||||
? {
|
||||
id: session.exchange.id,
|
||||
name: session.exchange.name,
|
||||
exchange_code: session.exchange.exchange_code,
|
||||
}
|
||||
: null,
|
||||
trading_pair: session.trading_pair
|
||||
? {
|
||||
id: session.trading_pair.id,
|
||||
symbol: session.trading_pair.symbol,
|
||||
base_asset: session.trading_pair.base_asset,
|
||||
quote_asset: session.trading_pair.quote_asset,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
async function maybeAdvanceSimulation(context, currentUser) {
|
||||
if (!context || context.session.status !== 'running') {
|
||||
return context;
|
||||
}
|
||||
|
||||
const latestEvent = context.sessionEvents[0];
|
||||
const lastHeartbeat = latestEvent?.event_at || context.session.started_at || context.session.createdAt;
|
||||
const secondsSinceLastHeartbeat = (Date.now() - new Date(lastHeartbeat).getTime()) / 1000;
|
||||
|
||||
if (secondsSinceLastHeartbeat < 12) {
|
||||
return context;
|
||||
}
|
||||
|
||||
const updateIndex = context.roiSnapshots.length + context.tradeSignals.length + context.sessionEvents.length;
|
||||
const selectedStrategy = pickByIndex(context.sessionStrategies, updateIndex) || context.sessionStrategies[0];
|
||||
const strategy = selectedStrategy?.strategy;
|
||||
const riskPercent = Math.max(0.25, toNumber(context.session.risk_percent, 1));
|
||||
const targetPercent = Math.max(0.5, toNumber(context.session.target_profit_percent, 1));
|
||||
const pairSymbol = context.session.trading_pair?.symbol || 'BTCUSDT';
|
||||
const basePrice = getReferencePrice(pairSymbol);
|
||||
const sideModifier = updateIndex % 4 === 0 ? -1 : 1;
|
||||
const categoryBias = getSignalBias(strategy?.category);
|
||||
const movePercent = round((riskPercent * 0.22 + categoryBias + (updateIndex % 3) * 0.06) * sideModifier, 3);
|
||||
const confidenceScore = Math.max(55, Math.min(93, round(68 + categoryBias * 22 + updateIndex * 0.7, 1)));
|
||||
const openPosition = context.positions.find((position) => position.status === 'open');
|
||||
let signalType = movePercent >= 0 ? 'buy' : 'sell';
|
||||
|
||||
if (openPosition && updateIndex % 3 === 0) {
|
||||
signalType = 'close';
|
||||
} else if (updateIndex % 5 === 0) {
|
||||
signalType = 'hold';
|
||||
}
|
||||
|
||||
const entryPrice = round(basePrice * (1 + movePercent / 100), 4);
|
||||
const stopLossPrice = round(entryPrice * (1 - riskPercent / 100), 4);
|
||||
const takeProfitPrice = round(entryPrice * (1 + targetPercent / 100), 4);
|
||||
const quantity = round((toNumber(context.session.investment_amount, 0) * Math.max(riskPercent / 100, 0.02)) / Math.max(entryPrice, 1), 6);
|
||||
const latestSnapshot = context.roiSnapshots[0];
|
||||
const startingEquity = latestSnapshot
|
||||
? toNumber(latestSnapshot.ending_equity, context.session.investment_amount)
|
||||
: toNumber(context.session.investment_amount, 0);
|
||||
const pnlAmount = round(startingEquity * (movePercent / 100), 2);
|
||||
const endingEquity = round(startingEquity + pnlAmount, 2);
|
||||
const now = new Date();
|
||||
const signal = await db.trade_signals.create({
|
||||
signal_type: signalType,
|
||||
confidence_level: getConfidenceLevel(confidenceScore),
|
||||
confidence_score: confidenceScore,
|
||||
suggested_entry_price: entryPrice,
|
||||
suggested_stop_loss_price: stopLossPrice,
|
||||
suggested_take_profit_price: takeProfitPrice,
|
||||
rationale: getSignalRationale(
|
||||
strategy?.name || 'Motor adaptativo',
|
||||
STRATEGY_CATEGORY_LABELS[strategy?.category] || 'Estrategia',
|
||||
pairSymbol,
|
||||
context.session.timeframe,
|
||||
updateIndex,
|
||||
),
|
||||
signaled_at: now,
|
||||
bot_sessionId: context.session.id,
|
||||
strategyId: strategy?.id || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
|
||||
await db.roi_snapshots.create({
|
||||
period_start_at: new Date(now.getTime() - (60 * 60 * 1000)),
|
||||
period_end_at: now,
|
||||
starting_equity: startingEquity,
|
||||
ending_equity: endingEquity,
|
||||
profit_loss_amount: pnlAmount,
|
||||
roi_percent: round((pnlAmount / Math.max(startingEquity, 1)) * 100, 2),
|
||||
closed_trades_count: context.positions.filter((position) => position.status === 'closed').length,
|
||||
bot_sessionId: context.session.id,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
|
||||
await db.session_events.create({
|
||||
event_type: signalType === 'hold' ? 'data_update' : 'signal_generated',
|
||||
severity: pnlAmount >= 0 ? 'info' : 'warning',
|
||||
message: `${SIGNAL_LABELS[signalType] || 'Señal'} en ${pairSymbol}: ${strategy?.name || 'estrategia adaptativa'} evaluó mechas, momentum e índices con ${confidenceScore}% de confianza.`,
|
||||
payload: JSON.stringify({
|
||||
pairSymbol,
|
||||
confidenceScore,
|
||||
strategy: strategy?.name || 'Motor adaptativo',
|
||||
movePercent,
|
||||
currentHourRoi: round((pnlAmount / Math.max(startingEquity, 1)) * 100, 2),
|
||||
}),
|
||||
event_at: now,
|
||||
bot_sessionId: context.session.id,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
|
||||
if (signalType === 'buy' || signalType === 'sell') {
|
||||
await db.orders.create({
|
||||
side: signalType,
|
||||
order_type: updateIndex % 2 === 0 ? 'market' : 'limit',
|
||||
status: 'filled',
|
||||
price: entryPrice,
|
||||
quantity,
|
||||
filled_quantity: quantity,
|
||||
avg_fill_price: entryPrice,
|
||||
exchange_order_ref: `${context.session.exchange?.exchange_code || 'sim'}-${Date.now()}`,
|
||||
is_simulated: context.session.simulation_mode,
|
||||
submitted_at: now,
|
||||
finalized_at: now,
|
||||
bot_sessionId: context.session.id,
|
||||
trade_signalId: signal.id,
|
||||
trading_pairId: context.session.trading_pair?.id || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (!openPosition && (signalType === 'buy' || signalType === 'sell')) {
|
||||
await db.positions.create({
|
||||
side: signalType === 'buy' ? 'long' : 'short',
|
||||
status: 'open',
|
||||
entry_price: entryPrice,
|
||||
quantity,
|
||||
stop_loss_price: stopLossPrice,
|
||||
take_profit_price: takeProfitPrice,
|
||||
realized_pnl: 0,
|
||||
unrealized_pnl: round(pnlAmount * 0.45, 2),
|
||||
opened_at: now,
|
||||
bot_sessionId: context.session.id,
|
||||
trading_pairId: context.session.trading_pair?.id || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
|
||||
await db.session_events.create({
|
||||
event_type: 'position_opened',
|
||||
severity: 'info',
|
||||
message: `Posición ${signalType === 'buy' ? 'long' : 'short'} abierta en ${pairSymbol} con SL ${stopLossPrice} y TP ${takeProfitPrice}.`,
|
||||
payload: JSON.stringify({ entryPrice, stopLossPrice, takeProfitPrice, quantity }),
|
||||
event_at: new Date(now.getTime() + 1500),
|
||||
bot_sessionId: context.session.id,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
} else if (openPosition && signalType === 'close') {
|
||||
const exitPrice = round(entryPrice * (1 + movePercent / 250), 4);
|
||||
const priceDifference = exitPrice - toNumber(openPosition.entry_price, exitPrice);
|
||||
const directionalMultiplier = openPosition.side === 'long' ? 1 : -1;
|
||||
const realizedPnl = round(priceDifference * toNumber(openPosition.quantity, 0) * directionalMultiplier, 2);
|
||||
|
||||
await openPosition.update({
|
||||
status: 'closed',
|
||||
closed_at: now,
|
||||
realized_pnl: realizedPnl,
|
||||
unrealized_pnl: 0,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
|
||||
await db.session_events.create({
|
||||
event_type: 'position_closed',
|
||||
severity: realizedPnl >= 0 ? 'info' : 'warning',
|
||||
message: `Posición cerrada en ${pairSymbol} con PnL ${realizedPnl >= 0 ? '+' : ''}${realizedPnl}.`,
|
||||
payload: JSON.stringify({ exitPrice, realizedPnl }),
|
||||
event_at: new Date(now.getTime() + 2000),
|
||||
bot_sessionId: context.session.id,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
} else if (openPosition) {
|
||||
const updatedUnrealizedPnl = round(pnlAmount * 0.55, 2);
|
||||
|
||||
await openPosition.update({
|
||||
unrealized_pnl: updatedUnrealizedPnl,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
}
|
||||
|
||||
return fetchSessionContext(context.session.id, currentUser);
|
||||
}
|
||||
|
||||
async function getBootstrapData(currentUser) {
|
||||
const [exchanges, apiCredentials, tradingPairs, strategies, recentSessions] = await Promise.all([
|
||||
db.exchanges.findAll({
|
||||
where: { is_enabled: true },
|
||||
attributes: ['id', 'name', 'exchange_code', 'supports_spot', 'supports_futures'],
|
||||
order: [['name', 'ASC']],
|
||||
}),
|
||||
db.api_credentials.findAll({
|
||||
where: {
|
||||
userId: currentUser.id,
|
||||
is_active: true,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: db.exchanges,
|
||||
as: 'exchange',
|
||||
attributes: ['id', 'name', 'exchange_code'],
|
||||
},
|
||||
],
|
||||
attributes: ['id', 'label', 'last_validated_at', 'exchangeId'],
|
||||
order: [['updatedAt', 'DESC']],
|
||||
limit: 12,
|
||||
}),
|
||||
db.trading_pairs.findAll({
|
||||
where: { is_active: true },
|
||||
include: [
|
||||
{
|
||||
model: db.exchanges,
|
||||
as: 'exchange',
|
||||
attributes: ['id', 'name', 'exchange_code'],
|
||||
},
|
||||
],
|
||||
attributes: ['id', 'symbol', 'base_asset', 'quote_asset', 'exchangeId'],
|
||||
order: [['symbol', 'ASC']],
|
||||
limit: 40,
|
||||
}),
|
||||
db.strategies.findAll({
|
||||
where: { is_active: true },
|
||||
attributes: ['id', 'name', 'category', 'description'],
|
||||
order: [['name', 'ASC']],
|
||||
limit: 12,
|
||||
}),
|
||||
db.bot_sessions.findAll({
|
||||
where: { userId: currentUser.id },
|
||||
include: [
|
||||
{
|
||||
model: db.exchanges,
|
||||
as: 'exchange',
|
||||
attributes: ['id', 'name', 'exchange_code'],
|
||||
},
|
||||
{
|
||||
model: db.trading_pairs,
|
||||
as: 'trading_pair',
|
||||
attributes: ['id', 'symbol', 'base_asset', 'quote_asset'],
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: 6,
|
||||
}),
|
||||
]);
|
||||
|
||||
const activeSession = recentSessions.find((session) => ACTIVE_SESSION_STATUSES.includes(session.status)) || null;
|
||||
const recentSessionsSummary = await Promise.all(recentSessions.map((session) => summarizeSession(session)));
|
||||
const activeSessionContext = activeSession
|
||||
? await maybeAdvanceSimulation(await fetchSessionContext(activeSession.id, currentUser), currentUser)
|
||||
: null;
|
||||
|
||||
return {
|
||||
exchanges: exchanges.map((exchange) => ({
|
||||
id: exchange.id,
|
||||
name: exchange.name,
|
||||
exchange_code: exchange.exchange_code,
|
||||
supports_spot: exchange.supports_spot,
|
||||
supports_futures: exchange.supports_futures,
|
||||
})),
|
||||
apiCredentials: apiCredentials.map((credential) => ({
|
||||
id: credential.id,
|
||||
label: credential.label,
|
||||
exchangeId: credential.exchangeId,
|
||||
exchangeName: credential.exchange?.name || null,
|
||||
exchangeCode: credential.exchange?.exchange_code || null,
|
||||
last_validated_at: credential.last_validated_at,
|
||||
})),
|
||||
tradingPairs: tradingPairs.map((pair) => ({
|
||||
id: pair.id,
|
||||
symbol: pair.symbol,
|
||||
base_asset: pair.base_asset,
|
||||
quote_asset: pair.quote_asset,
|
||||
exchangeId: pair.exchangeId,
|
||||
exchangeName: pair.exchange?.name || null,
|
||||
exchangeCode: pair.exchange?.exchange_code || null,
|
||||
})),
|
||||
strategies: strategies.map((strategy) => ({
|
||||
id: strategy.id,
|
||||
name: strategy.name,
|
||||
category: strategy.category,
|
||||
categoryLabel: STRATEGY_CATEGORY_LABELS[strategy.category] || strategy.category,
|
||||
description: strategy.description,
|
||||
})),
|
||||
recentSessions: recentSessionsSummary,
|
||||
latestActiveSessionId: activeSession?.id || null,
|
||||
activeWorkspace: activeSessionContext ? serializeWorkspace(activeSessionContext) : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function launchSession(data, currentUser) {
|
||||
const investmentAmount = toNumber(data.investmentAmount, NaN);
|
||||
const riskPercent = toNumber(data.riskPercent, NaN);
|
||||
const targetProfitPercent = toNumber(data.targetProfitPercent, NaN);
|
||||
const strategyIds = Array.isArray(data.strategyIds) ? data.strategyIds.filter(Boolean) : [];
|
||||
|
||||
if (!data.exchangeId) {
|
||||
throw badRequest('Selecciona un exchange.');
|
||||
}
|
||||
|
||||
if (!data.tradingPairId) {
|
||||
throw badRequest('Selecciona un par o moneda para operar.');
|
||||
}
|
||||
|
||||
if (!strategyIds.length) {
|
||||
throw badRequest('Selecciona al menos una estrategia.');
|
||||
}
|
||||
|
||||
if (!Number.isFinite(investmentAmount) || investmentAmount <= 0) {
|
||||
throw badRequest('La cantidad a invertir debe ser mayor a 0.');
|
||||
}
|
||||
|
||||
if (!Number.isFinite(riskPercent) || riskPercent <= 0 || riskPercent > 25) {
|
||||
throw badRequest('El riesgo debe estar entre 0.1% y 25%.');
|
||||
}
|
||||
|
||||
if (!Number.isFinite(targetProfitPercent) || targetProfitPercent <= 0 || targetProfitPercent > 80) {
|
||||
throw badRequest('El objetivo de beneficio debe estar entre 0.1% y 80%.');
|
||||
}
|
||||
|
||||
if (!data.simulationMode && !data.apiCredentialId) {
|
||||
throw badRequest('Para operar en vivo necesitas seleccionar credenciales activas.');
|
||||
}
|
||||
|
||||
const [exchange, tradingPair, apiCredential, strategies] = await Promise.all([
|
||||
db.exchanges.findOne({
|
||||
where: {
|
||||
id: data.exchangeId,
|
||||
is_enabled: true,
|
||||
},
|
||||
}),
|
||||
db.trading_pairs.findOne({
|
||||
where: {
|
||||
id: data.tradingPairId,
|
||||
is_active: true,
|
||||
},
|
||||
}),
|
||||
data.apiCredentialId
|
||||
? db.api_credentials.findOne({
|
||||
where: {
|
||||
id: data.apiCredentialId,
|
||||
userId: currentUser.id,
|
||||
is_active: true,
|
||||
},
|
||||
})
|
||||
: Promise.resolve(null),
|
||||
db.strategies.findAll({
|
||||
where: {
|
||||
id: strategyIds,
|
||||
is_active: true,
|
||||
},
|
||||
order: [['name', 'ASC']],
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!exchange) {
|
||||
throw badRequest('El exchange seleccionado no está disponible.');
|
||||
}
|
||||
|
||||
if (!tradingPair) {
|
||||
throw badRequest('El par seleccionado no está disponible.');
|
||||
}
|
||||
|
||||
if (tradingPair.exchangeId && tradingPair.exchangeId !== exchange.id) {
|
||||
throw badRequest('El par elegido no pertenece al exchange seleccionado.');
|
||||
}
|
||||
|
||||
if (data.apiCredentialId && !apiCredential) {
|
||||
throw badRequest('Las credenciales elegidas no están disponibles para este usuario.');
|
||||
}
|
||||
|
||||
if (apiCredential && apiCredential.exchangeId && apiCredential.exchangeId !== exchange.id) {
|
||||
throw badRequest('Las credenciales deben coincidir con el exchange seleccionado.');
|
||||
}
|
||||
|
||||
if (strategies.length !== strategyIds.length) {
|
||||
throw badRequest('Una o más estrategias seleccionadas no están disponibles.');
|
||||
}
|
||||
|
||||
const session = await db.bot_sessions.create({
|
||||
status: 'running',
|
||||
simulation_mode: !!data.simulationMode,
|
||||
investment_amount: round(investmentAmount, 2),
|
||||
risk_percent: round(riskPercent, 2),
|
||||
target_profit_percent: round(targetProfitPercent, 2),
|
||||
timeframe: data.timeframe || '15m',
|
||||
started_at: new Date(),
|
||||
notes: data.notes || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
session.setUser(currentUser.id),
|
||||
session.setExchange(exchange.id),
|
||||
session.setTrading_pair(tradingPair.id),
|
||||
data.apiCredentialId ? session.setApi_credential(apiCredential.id) : Promise.resolve(),
|
||||
]);
|
||||
|
||||
const strategyWeight = round(100 / strategyIds.length, 2);
|
||||
|
||||
await Promise.all(strategies.map((strategy) => db.session_strategies.create({
|
||||
is_enabled: true,
|
||||
weight: strategyWeight,
|
||||
parameter_overrides: JSON.stringify({
|
||||
adaptiveLearning: true,
|
||||
preferredTimeframe: data.timeframe || '15m',
|
||||
liveNewsBias: strategy.category === 'news_trading',
|
||||
}),
|
||||
enabled_at: new Date(),
|
||||
bot_sessionId: session.id,
|
||||
strategyId: strategy.id,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
})));
|
||||
|
||||
await db.session_events.create({
|
||||
event_type: 'bot_start',
|
||||
severity: 'info',
|
||||
message: `Bot iniciado en ${tradingPair.symbol} sobre ${exchange.name} en modo ${data.simulationMode ? 'simulación' : 'live'}.`,
|
||||
payload: JSON.stringify({
|
||||
investmentAmount: round(investmentAmount, 2),
|
||||
riskPercent: round(riskPercent, 2),
|
||||
targetProfitPercent: round(targetProfitPercent, 2),
|
||||
strategyIds,
|
||||
}),
|
||||
event_at: new Date(),
|
||||
bot_sessionId: session.id,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
|
||||
await db.roi_snapshots.create({
|
||||
period_start_at: new Date(Date.now() - (60 * 60 * 1000)),
|
||||
period_end_at: new Date(),
|
||||
starting_equity: round(investmentAmount, 2),
|
||||
ending_equity: round(investmentAmount, 2),
|
||||
profit_loss_amount: 0,
|
||||
roi_percent: 0,
|
||||
closed_trades_count: 0,
|
||||
bot_sessionId: session.id,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
|
||||
const workspace = await maybeAdvanceSimulation(await fetchSessionContext(session.id, currentUser), currentUser);
|
||||
return serializeWorkspace(workspace);
|
||||
}
|
||||
|
||||
async function getWorkspaceById(sessionId, currentUser) {
|
||||
const sessionContext = await fetchSessionContext(sessionId, currentUser);
|
||||
|
||||
if (!sessionContext) {
|
||||
const error = new Error('Sesión no encontrada.');
|
||||
error.code = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const refreshedContext = await maybeAdvanceSimulation(sessionContext, currentUser);
|
||||
return serializeWorkspace(refreshedContext);
|
||||
}
|
||||
|
||||
async function applyRuntimeAction(sessionId, currentUser, action) {
|
||||
const sessionContext = await fetchSessionContext(sessionId, currentUser);
|
||||
|
||||
if (!sessionContext) {
|
||||
const error = new Error('Sesión no encontrada.');
|
||||
error.code = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const session = sessionContext.session;
|
||||
const now = new Date();
|
||||
let nextStatus = session.status;
|
||||
let eventType = 'info';
|
||||
let message = 'Sin cambios.';
|
||||
|
||||
if (action === 'pause') {
|
||||
if (session.status !== 'running') {
|
||||
throw badRequest('Solo puedes pausar una sesión activa.');
|
||||
}
|
||||
|
||||
nextStatus = 'paused';
|
||||
eventType = 'bot_pause';
|
||||
message = 'Bot pausado manualmente. El aprendizaje adaptativo quedó en espera.';
|
||||
} else if (action === 'resume') {
|
||||
if (session.status !== 'paused') {
|
||||
throw badRequest('Solo puedes reanudar una sesión pausada.');
|
||||
}
|
||||
|
||||
nextStatus = 'running';
|
||||
eventType = 'bot_resume';
|
||||
message = 'Bot reanudado. Se retoma el análisis técnico y la simulación en tiempo real.';
|
||||
} else if (action === 'stop') {
|
||||
if (!ACTIVE_SESSION_STATUSES.includes(session.status) && session.status !== 'running') {
|
||||
throw badRequest('Solo puedes detener una sesión activa o pausada.');
|
||||
}
|
||||
|
||||
nextStatus = 'stopped';
|
||||
eventType = 'bot_stop';
|
||||
message = 'Bot detenido. Se congeló la operativa y quedó listo para revisión.';
|
||||
} else {
|
||||
throw badRequest('Acción no soportada.');
|
||||
}
|
||||
|
||||
await session.update({
|
||||
status: nextStatus,
|
||||
stopped_at: action === 'stop' ? now : session.stopped_at,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
|
||||
await db.session_events.create({
|
||||
event_type: eventType,
|
||||
severity: 'info',
|
||||
message,
|
||||
payload: JSON.stringify({ action, nextStatus }),
|
||||
event_at: now,
|
||||
bot_sessionId: session.id,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
});
|
||||
|
||||
if (action === 'stop') {
|
||||
const openPositions = sessionContext.positions.filter((position) => position.status === 'open');
|
||||
|
||||
await Promise.all(openPositions.map((position) => position.update({
|
||||
status: 'closed',
|
||||
closed_at: now,
|
||||
realized_pnl: round(toNumber(position.unrealized_pnl), 2),
|
||||
unrealized_pnl: 0,
|
||||
updatedById: currentUser.id,
|
||||
})));
|
||||
}
|
||||
|
||||
return getWorkspaceById(session.id, currentUser);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getBootstrapData,
|
||||
launchSession,
|
||||
getWorkspaceById,
|
||||
applyRuntimeAction,
|
||||
};
|
||||
@ -1,6 +1,5 @@
|
||||
import React, {useEffect, useRef} from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||
import BaseDivider from './BaseDivider'
|
||||
import BaseIcon from './BaseIcon'
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||
import menuAside from '../menuAside'
|
||||
|
||||
@ -7,6 +7,14 @@ const menuAside: MenuAsideItem[] = [
|
||||
icon: icon.mdiViewDashboardOutline,
|
||||
label: 'Dashboard',
|
||||
},
|
||||
{
|
||||
href: '/trading-bot',
|
||||
label: 'Trading Bot',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiRobot' in icon ? icon['mdiRobot' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_BOT_SESSIONS'
|
||||
},
|
||||
|
||||
{
|
||||
href: '/users/users-list',
|
||||
|
||||
@ -1,166 +1,145 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import {
|
||||
mdiArrowTopRight,
|
||||
mdiChartTimelineVariant,
|
||||
mdiCog,
|
||||
mdiFlash,
|
||||
mdiLogin,
|
||||
mdiRobot,
|
||||
mdiShieldCheck,
|
||||
} from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
const featureCards = [
|
||||
{
|
||||
icon: mdiChartTimelineVariant,
|
||||
title: 'Tablero en tiempo real',
|
||||
description: 'ROI horario, señales, eventos, posiciones y flujo operativo en una sola vista premium.',
|
||||
},
|
||||
{
|
||||
icon: mdiCog,
|
||||
title: 'Configuración completa',
|
||||
description: 'Capital, riesgo, beneficio, moneda, credenciales API y activación de modo simulación o live.',
|
||||
},
|
||||
{
|
||||
icon: mdiFlash,
|
||||
title: 'Motor multi-estrategia',
|
||||
description: 'Medias móviles, ruptura, acción del precio, retrocesos, mean reversion y noticias.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Starter() {
|
||||
const [illustrationImage, setIllustrationImage] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
photographer_url: undefined,
|
||||
})
|
||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
||||
const [contentType, setContentType] = useState('video');
|
||||
const [contentPosition, setContentPosition] = useState('left');
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
|
||||
const title = 'App Preview'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div
|
||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||
style={{
|
||||
backgroundImage: `${
|
||||
image
|
||||
? `url(${image?.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={image?.photographer_url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Photo by {image?.photographer} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const videoBlock = (video) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
return (
|
||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||
<video
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
>
|
||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={video?.user?.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
};
|
||||
const strategyPills = [
|
||||
'Medias móviles',
|
||||
'Breakout',
|
||||
'Acción del precio',
|
||||
'Retrocesos',
|
||||
'Reversión a la media',
|
||||
'Trading de noticias',
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
contentPosition === 'background'
|
||||
? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle('Trading Bot Workspace')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{contentType === 'image' && contentPosition !== 'background'
|
||||
? imageBlock(illustrationImage)
|
||||
: null}
|
||||
{contentType === 'video' && contentPosition !== 'background'
|
||||
? videoBlock(illustrationVideo)
|
||||
: null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your App Preview app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center '>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
<SectionFullScreen bg="violet">
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(34,211,238,0.18),_transparent_30%),radial-gradient(circle_at_bottom_right,_rgba(99,102,241,0.16),_transparent_25%),linear-gradient(180deg,#020617_0%,#0f172a_45%,#111827_100%)] px-6 py-10 text-white">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 rounded-full border border-white/10 bg-white/5 px-5 py-3 backdrop-blur">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-full bg-cyan-400/10 p-2">
|
||||
<BaseIcon path={mdiRobot} className="text-cyan-300" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-cyan-200">Trading Bot Studio</p>
|
||||
<p className="text-sm text-slate-300">MVP listo para simular sesiones y visualizar el tablero.</p>
|
||||
</div>
|
||||
</div>
|
||||
<BaseButtons type="justify-start" noWrap className="gap-2">
|
||||
<BaseButton color="whiteDark" label="Login" icon={mdiLogin} href="/login" />
|
||||
<BaseButton color="info" label="Admin interface" icon={mdiArrowTopRight} href="/dashboard" />
|
||||
</BaseButtons>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
<div className="grid gap-6 lg:grid-cols-[1.2fr_0.8fr]">
|
||||
<CardBox className="border border-white/10 bg-white/5 text-white shadow-[0_24px_80px_rgba(15,23,42,0.35)]" cardBoxClassName="p-8 md:p-10">
|
||||
<div className="inline-flex rounded-full border border-cyan-400/30 bg-cyan-400/10 px-4 py-1 text-xs font-medium uppercase tracking-[0.24em] text-cyan-200">
|
||||
Bot técnico con enfoque moderno
|
||||
</div>
|
||||
<h1 className="mt-6 max-w-3xl text-4xl font-semibold leading-tight md:text-6xl">
|
||||
Crea, arranca y supervisa un bot de trading con visualización viva y control total del riesgo.
|
||||
</h1>
|
||||
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-300">
|
||||
La primera entrega conecta configuración, estrategias, credenciales, simulación, tablero operativo, ROI por hora y sesiones recientes sin rehacer el CRUD base.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<BaseButton color="info" label="Entrar al panel" icon={mdiArrowTopRight} href="/dashboard" />
|
||||
<BaseButton color="whiteDark" label="Ir al login" icon={mdiLogin} href="/login" />
|
||||
</div>
|
||||
<BaseDivider />
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{strategyPills.map((pill) => (
|
||||
<span key={pill} className="rounded-full border border-white/10 bg-white/5 px-4 py-2 text-sm text-slate-200">
|
||||
{pill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className="border border-white/10 bg-[#020617]/90 text-white" cardBoxClassName="p-8">
|
||||
<p className="text-sm uppercase tracking-[0.18em] text-slate-400">Qué incluye este MVP</p>
|
||||
<div className="mt-6 space-y-5">
|
||||
<div className="rounded-3xl border border-white/10 bg-white/5 p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<BaseIcon path={mdiShieldCheck} className="text-emerald-300" size={24} />
|
||||
<span className="text-sm uppercase tracking-[0.18em] text-slate-300">Seguridad operativa</span>
|
||||
</div>
|
||||
<p className="mt-4 text-sm leading-7 text-slate-300">
|
||||
Selector de credenciales API para Binance y Kraken con modo simulación activable desde configuración.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-3xl border border-white/10 bg-white/5 p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<BaseIcon path={mdiChartTimelineVariant} className="text-cyan-300" size={24} />
|
||||
<span className="text-sm uppercase tracking-[0.18em] text-slate-300">Monitoreo visual</span>
|
||||
</div>
|
||||
<p className="mt-4 text-sm leading-7 text-slate-300">
|
||||
Un tablero elegante para ver todos los movimientos, métricas y eventos relevantes del bot en tiempo real.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{featureCards.map((card) => (
|
||||
<CardBox key={card.title} className="border border-white/10 bg-white/5 text-white" cardBoxClassName="p-6">
|
||||
<div className="rounded-2xl bg-white/5 p-3 w-fit">
|
||||
<BaseIcon path={card.icon} className="text-cyan-300" size={28} />
|
||||
</div>
|
||||
<h2 className="mt-5 text-2xl font-semibold">{card.title}</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-slate-300">{card.description}</p>
|
||||
</CardBox>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
HomePage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
|
||||
1190
frontend/src/pages/trading-bot.tsx
Normal file
1190
frontend/src/pages/trading-bot.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user