@weppa-cloud/mcp-google-ads
Version:
Google Ads MCP server for growth marketing - campaign optimization, keyword research, and ROI tracking
629 lines • 29.6 kB
JavaScript
import { z } from 'zod';
const budgetOptimizerSchema = z.object({
targetRoas: z.number().min(1).default(3).describe('ROAS objetivo para redistribución'),
maxBudgetChange: z.number().min(0).max(100).default(30).describe('Cambio máximo de presupuesto en %'),
includeSeasonality: z.boolean().default(true).describe('Considerar estacionalidad'),
});
const budgetForecastSchema = z.object({
forecastDays: z.number().min(7).max(90).default(30).describe('Días a proyectar'),
scenarioType: z.enum(['conservative', 'realistic', 'aggressive']).default('realistic'),
plannedBudgetIncrease: z.number().optional().describe('Incremento de presupuesto planeado en %'),
});
const smartBiddingRecommendationSchema = z.object({
minConversionsRequired: z.number().default(30).describe('Conversiones mínimas para recomendar'),
includeSimulation: z.boolean().default(true).describe('Incluir simulación de resultados'),
});
export const budgetTools = [
{
name: 'budget_optimizer',
description: '💰 Optimiza la distribución de presupuesto entre campañas para maximizar ROI',
inputSchema: budgetOptimizerSchema,
handler: async (args, auth) => {
const validated = budgetOptimizerSchema.parse(args);
const { startDate, endDate } = getDateRange('30days');
// Obtener datos de campañas con presupuesto
const query = `
SELECT
campaign.id,
campaign.name,
campaign.status,
campaign_budget.id,
campaign_budget.amount_micros,
campaign_budget.total_amount_micros,
campaign_budget.status,
metrics.cost_micros,
metrics.conversions,
metrics.conversions_value,
metrics.clicks,
metrics.impressions,
metrics.search_impression_share,
metrics.search_lost_impression_share_budget
FROM campaign
WHERE segments.date BETWEEN '${startDate}' AND '${endDate}'
AND campaign.status = 'ENABLED'
AND campaign_budget.status = 'ENABLED'
ORDER BY metrics.cost_micros DESC
`;
const campaigns = await auth.query(query);
// Calcular métricas para cada campaña
const campaignAnalysis = campaigns.map((row) => {
const currentBudget = row.campaign_budget.amount_micros / 1_000_000;
const dailyBudget = row.campaign_budget.total_amount_micros
? row.campaign_budget.total_amount_micros / 1_000_000 / 30
: currentBudget;
const spend = row.metrics.cost_micros / 1_000_000;
const revenue = row.metrics.conversions_value || 0;
const roas = spend > 0 ? revenue / spend : 0;
const budgetUtilization = dailyBudget > 0 ? (spend / 30) / dailyBudget : 0;
return {
id: row.campaign.id,
name: row.campaign.name,
currentDailyBudget: dailyBudget,
actualSpend: spend,
revenue: revenue,
roas: roas,
conversions: row.metrics.conversions || 0,
budgetUtilization: budgetUtilization,
lostImpressionShareBudget: row.metrics.search_lost_impression_share_budget || 0,
impressionShare: row.metrics.search_impression_share || 0,
};
});
// Calcular presupuesto total disponible
const totalCurrentBudget = campaignAnalysis.reduce((sum, c) => sum + c.currentDailyBudget, 0);
// Optimizar distribución
const optimization = optimizeBudgetDistribution(campaignAnalysis, totalCurrentBudget, validated.targetRoas, validated.maxBudgetChange);
// Considerar estacionalidad si está habilitada
let seasonalityAdjustments = null;
if (validated.includeSeasonality) {
seasonalityAdjustments = calculateSeasonalityAdjustments(campaigns);
}
// Calcular impacto proyectado
const projectedImpact = calculateProjectedImpact(campaignAnalysis, optimization);
return {
summary: {
totalDailyBudget: totalCurrentBudget,
currentOverallRoas: calculateOverallRoas(campaignAnalysis),
projectedOverallRoas: projectedImpact.projectedRoas,
estimatedRevenueIncrease: projectedImpact.revenueIncrease,
},
recommendations: optimization.recommendations,
budgetChanges: optimization.changes.filter(c => Math.abs(c.changePercent) > 5),
seasonalityInsights: seasonalityAdjustments,
implementation: {
priority: generatePriorityActions(optimization.changes),
warnings: generateBudgetWarnings(optimization.changes),
timeline: 'Implementa cambios gradualmente en 3-5 días para minimizar volatilidad',
},
};
},
},
{
name: 'budget_forecast',
description: '📈 Proyecta resultados futuros basados en diferentes escenarios de presupuesto',
inputSchema: budgetForecastSchema,
handler: async (args, auth) => {
const validated = budgetForecastSchema.parse(args);
// Obtener datos históricos (90 días para mejor proyección)
const historicalDays = 90;
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - historicalDays);
const query = `
SELECT
segments.date,
metrics.cost_micros,
metrics.conversions,
metrics.conversions_value,
metrics.clicks,
metrics.impressions
FROM customer
WHERE segments.date BETWEEN '${startDate.toISOString().split('T')[0]}'
AND '${endDate.toISOString().split('T')[0]}'
ORDER BY segments.date
`;
const historicalData = await auth.query(query);
// Calcular tendencias y estacionalidad
const trends = analyzeTrends(historicalData);
const seasonality = detectSeasonalityPatterns(historicalData);
// Generar proyecciones según escenario
const projections = generateProjections(historicalData, trends, seasonality, validated.forecastDays, validated.scenarioType, validated.plannedBudgetIncrease);
// Calcular métricas clave proyectadas
const forecastMetrics = calculateForecastMetrics(projections);
// Identificar riesgos y oportunidades
const riskAnalysis = analyzeforecastRisks(projections, trends);
return {
forecast: {
scenario: validated.scenarioType,
period: `Next ${validated.forecastDays} days`,
budgetIncrease: validated.plannedBudgetIncrease || 0,
},
projections: {
daily: projections.daily.slice(0, 7), // Primeros 7 días detallados
weekly: projections.weekly,
summary: forecastMetrics,
},
insights: {
trends: trends,
seasonality: seasonality,
confidenceLevel: calculateConfidenceLevel(historicalData, trends),
},
risks: riskAnalysis,
recommendations: generateForecastRecommendations(forecastMetrics, validated.scenarioType, validated.plannedBudgetIncrease),
};
},
},
{
name: 'smart_bidding_recommendations',
description: '🤖 Recomienda estrategias de Smart Bidding personalizadas por campaña',
inputSchema: smartBiddingRecommendationSchema,
handler: async (args, auth) => {
const validated = smartBiddingRecommendationSchema.parse(args);
const { startDate, endDate } = getDateRange('30days');
const query = `
SELECT
campaign.id,
campaign.name,
campaign.bidding_strategy_type,
campaign.target_cpa.target_cpa_micros,
campaign.target_roas.target_roas,
campaign.maximize_conversions.target_cpa_micros,
metrics.conversions,
metrics.conversions_value,
metrics.cost_micros,
metrics.average_cpc,
metrics.cost_per_conversion
FROM campaign
WHERE segments.date BETWEEN '${startDate}' AND '${endDate}'
AND campaign.status = 'ENABLED'
`;
const campaigns = await auth.query(query);
const recommendations = [];
for (const campaign of campaigns) {
const conversions = campaign.metrics?.conversions || 0;
const cost = (campaign.metrics?.cost_micros || 0) / 1_000_000;
const revenue = campaign.metrics?.conversions_value || 0;
const currentStrategy = campaign.campaign?.bidding_strategy_type || 'UNKNOWN';
const roas = cost > 0 ? revenue / cost : 0;
const cpa = conversions > 0 ? cost / conversions : 0;
const recommendation = {
campaignId: campaign.campaign?.id || '',
campaignName: campaign.campaign?.name || 'Unknown',
currentStrategy: currentStrategy,
performance: {
conversions: conversions,
roas: roas,
cpa: cpa,
spend: cost,
},
};
// Analizar y recomendar estrategia
if (conversions >= validated.minConversionsRequired) {
if (currentStrategy === 'MANUAL_CPC' || currentStrategy === 'MANUAL_CPM') {
// Recomendar cambio a Smart Bidding
if (revenue > 0 && roas > 2) {
recommendation.suggestedStrategy = 'TARGET_ROAS';
recommendation.suggestedTarget = Math.floor(roas * 0.8 * 100) / 100; // 80% del ROAS actual
recommendation.reason = 'Suficientes conversiones con valor para optimizar ROAS';
recommendation.expectedImpact = 'Aumento del 15-25% en revenue manteniendo eficiencia';
}
else if (conversions > validated.minConversionsRequired) {
recommendation.suggestedStrategy = 'TARGET_CPA';
recommendation.suggestedTarget = Math.ceil(cpa * 1.1); // 110% del CPA actual
recommendation.reason = 'Volumen de conversiones adecuado para Target CPA';
recommendation.expectedImpact = 'Aumento del 20-30% en conversiones';
}
else {
recommendation.suggestedStrategy = 'MAXIMIZE_CONVERSIONS';
recommendation.reason = 'Buena base de conversiones para maximizar volumen';
recommendation.expectedImpact = 'Aumento del 25-35% en conversiones';
}
recommendation.priority = 'HIGH';
recommendation.confidence = 'HIGH';
}
else if (currentStrategy === 'TARGET_CPA' && roas > 3) {
// Sugerir cambio a Target ROAS si hay buen valor
recommendation.suggestedStrategy = 'TARGET_ROAS';
recommendation.suggestedTarget = Math.floor(roas * 0.85 * 100) / 100;
recommendation.reason = 'Excelente ROAS actual, optimizar para valor';
recommendation.priority = 'MEDIUM';
recommendation.confidence = 'MEDIUM';
}
}
else {
// No suficientes conversiones
if (currentStrategy !== 'MAXIMIZE_CONVERSIONS' && conversions > 10) {
recommendation.suggestedStrategy = 'MAXIMIZE_CONVERSIONS';
recommendation.reason = 'Aumentar volumen de conversiones antes de optimizar CPA/ROAS';
recommendation.priority = 'MEDIUM';
recommendation.confidence = 'MEDIUM';
recommendation.expectedImpact = 'Recopilar más datos para futuras optimizaciones';
}
else {
recommendation.suggestedStrategy = 'KEEP_CURRENT';
recommendation.reason = conversions < 10
? 'Insuficientes conversiones para Smart Bidding efectivo'
: 'Estrategia actual apropiada por ahora';
recommendation.priority = 'LOW';
}
}
// Simular impacto si está habilitado
if (validated.includeSimulation && recommendation.suggestedStrategy !== 'KEEP_CURRENT') {
recommendation.simulation = simulateBiddingChange(campaign, recommendation.suggestedStrategy, recommendation.suggestedTarget);
}
recommendations.push(recommendation);
}
// Ordenar por prioridad e impacto potencial
recommendations.sort((a, b) => {
const priorityOrder = { HIGH: 3, MEDIUM: 2, LOW: 1 };
return priorityOrder[b.priority] -
priorityOrder[a.priority];
});
return {
summary: {
totalCampaigns: campaigns.length,
eligibleForSmartBidding: recommendations.filter(r => r.suggestedStrategy !== 'KEEP_CURRENT').length,
highPriorityChanges: recommendations.filter(r => r.priority === 'HIGH').length,
},
recommendations: recommendations.slice(0, 20),
implementationGuide: {
testing: 'Implementa cambios en campañas de prueba primero',
monitoring: 'Monitorea durante 2-3 semanas antes de evaluar',
rollback: 'Prepara plan de reversión si performance cae >20%',
},
bestPractices: generateSmartBiddingBestPractices(recommendations),
};
},
},
];
function getDateRange(timeframe) {
const endDate = new Date();
const startDate = new Date();
switch (timeframe) {
case '7days':
startDate.setDate(startDate.getDate() - 7);
break;
case '30days':
startDate.setDate(startDate.getDate() - 30);
break;
case '90days':
startDate.setDate(startDate.getDate() - 90);
break;
}
return {
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0],
};
}
function optimizeBudgetDistribution(campaigns, totalBudget, targetRoas, maxChangePercent) {
// Calcular scores para cada campaña
const scoredCampaigns = campaigns.map(c => {
let score = 0;
// Factor ROAS (peso: 40%)
if (c.roas >= targetRoas) {
score += 40 * (c.roas / targetRoas);
}
else {
score += 40 * (c.roas / targetRoas) * 0.5;
}
// Factor de oportunidad perdida (peso: 30%)
score += 30 * c.lostImpressionShareBudget;
// Factor de utilización de presupuesto (peso: 20%)
score += 20 * Math.min(c.budgetUtilization, 1);
// Factor de volumen de conversiones (peso: 10%)
const maxConversions = Math.max(...campaigns.map(c => c.conversions));
score += 10 * (c.conversions / maxConversions);
return { ...c, score };
});
// Normalizar scores
const totalScore = scoredCampaigns.reduce((sum, c) => sum + c.score, 0);
// Calcular nueva distribución
const changes = scoredCampaigns.map(c => {
const idealBudget = (c.score / totalScore) * totalBudget;
const currentBudget = c.currentDailyBudget;
// Aplicar límite de cambio máximo
let newBudget = idealBudget;
const maxIncrease = currentBudget * (1 + maxChangePercent / 100);
const maxDecrease = currentBudget * (1 - maxChangePercent / 100);
newBudget = Math.min(newBudget, maxIncrease);
newBudget = Math.max(newBudget, maxDecrease);
const change = newBudget - currentBudget;
const changePercent = currentBudget > 0 ? (change / currentBudget) * 100 : 0;
return {
campaignId: c.id,
campaignName: c.name,
currentBudget: currentBudget,
recommendedBudget: newBudget,
change: change,
changePercent: changePercent,
reason: generateBudgetChangeReason(c, changePercent),
};
});
// Generar recomendaciones
const recommendations = [];
const increasingBudget = changes.filter(c => c.changePercent > 10);
if (increasingBudget.length > 0) {
recommendations.push(`📈 Aumentar presupuesto en ${increasingBudget.length} campañas de alto rendimiento`);
}
const decreasingBudget = changes.filter(c => c.changePercent < -10);
if (decreasingBudget.length > 0) {
recommendations.push(`📉 Reducir presupuesto en ${decreasingBudget.length} campañas de bajo rendimiento`);
}
return { changes, recommendations };
}
function calculateSeasonalityAdjustments(campaigns) {
// Análisis simplificado de estacionalidad
// En producción, esto requeriría datos históricos más extensos
const currentMonth = new Date().getMonth();
const seasonalFactors = {
0: 'Enero - Post-navidad, menor demanda',
11: 'Diciembre - Pico navideño, aumentar presupuesto',
10: 'Noviembre - Black Friday/Cyber Monday',
6: 'Julio - Inicio temporada verano',
7: 'Agosto - Vacaciones, posible menor conversión',
};
return {
currentPeriod: seasonalFactors[currentMonth] || 'Período normal',
recommendation: currentMonth === 10 || currentMonth === 11
? 'Considera aumentar presupuestos 20-30% por temporada alta'
: 'Mantener presupuestos actuales',
};
}
function calculateProjectedImpact(current, optimization) {
let projectedRevenue = 0;
let projectedCost = 0;
current.forEach(campaign => {
const change = optimization.changes.find((c) => c.campaignId === campaign.id);
if (change) {
const budgetMultiplier = change.recommendedBudget / campaign.currentDailyBudget;
// Asumir elasticidad del 0.7 (70% del incremento se traduce en más gasto)
const spendMultiplier = 1 + (budgetMultiplier - 1) * 0.7;
projectedCost += campaign.actualSpend * spendMultiplier;
// Revenue aumenta menos que el gasto (ley de rendimientos decrecientes)
const revenueMultiplier = 1 + (spendMultiplier - 1) * 0.6;
projectedRevenue += campaign.revenue * revenueMultiplier;
}
});
const currentRevenue = current.reduce((sum, c) => sum + c.revenue, 0);
const currentCost = current.reduce((sum, c) => sum + c.actualSpend, 0);
return {
projectedRoas: projectedCost > 0 ? projectedRevenue / projectedCost : 0,
revenueIncrease: projectedRevenue - currentRevenue,
costIncrease: projectedCost - currentCost,
};
}
function generateBudgetChangeReason(campaign, changePercent) {
if (changePercent > 10) {
if (campaign.roas > 3)
return 'Alto ROAS - escalar para más revenue';
if (campaign.lostImpressionShareBudget > 0.2)
return 'Perdiendo impresiones por presupuesto';
return 'Buen rendimiento con potencial de crecimiento';
}
else if (changePercent < -10) {
if (campaign.roas < 1)
return 'ROAS no rentable - reducir pérdidas';
if (campaign.conversions === 0)
return 'Sin conversiones - revisar estrategia';
return 'Bajo rendimiento comparado con otras campañas';
}
return 'Mantener presupuesto actual';
}
function calculateOverallRoas(campaigns) {
const totalRevenue = campaigns.reduce((sum, c) => sum + c.revenue, 0);
const totalCost = campaigns.reduce((sum, c) => sum + c.actualSpend, 0);
return totalCost > 0 ? totalRevenue / totalCost : 0;
}
function generatePriorityActions(changes) {
return changes
.filter(c => Math.abs(c.changePercent) > 20)
.sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent))
.slice(0, 5)
.map(c => ({
campaign: c.campaignName,
action: c.changePercent > 0 ? 'INCREASE' : 'DECREASE',
amount: `$${Math.abs(c.change).toFixed(2)}`,
urgency: Math.abs(c.changePercent) > 30 ? 'HIGH' : 'MEDIUM',
}));
}
function generateBudgetWarnings(changes) {
const warnings = [];
const drasticCuts = changes.filter(c => c.changePercent < -30);
if (drasticCuts.length > 0) {
warnings.push(`⚠️ ${drasticCuts.length} campañas con reducción >30% - monitorear impacto`);
}
const bigIncreases = changes.filter(c => c.changePercent > 50);
if (bigIncreases.length > 0) {
warnings.push(`📈 ${bigIncreases.length} campañas con aumento >50% - escalar gradualmente`);
}
return warnings;
}
function analyzeTrends(data) {
// Calcular tendencia de conversiones y costos
const recentDays = data.slice(-30);
const previousDays = data.slice(-60, -30);
const recentAvgConversions = recentDays.reduce((sum, d) => sum + (d.metrics.conversions || 0), 0) / recentDays.length;
const previousAvgConversions = previousDays.reduce((sum, d) => sum + (d.metrics.conversions || 0), 0) / previousDays.length;
const conversionTrend = previousAvgConversions > 0
? (recentAvgConversions - previousAvgConversions) / previousAvgConversions
: 0;
return {
conversions: {
trend: conversionTrend > 0 ? 'INCREASING' : 'DECREASING',
changeRate: conversionTrend,
},
interpretation: interpretTrend(conversionTrend),
};
}
function detectSeasonalityPatterns(data) {
// Análisis simplificado por día de la semana
const dayPatterns = {};
data.forEach(row => {
const date = new Date(row.segments.date);
const dayOfWeek = date.getDay();
if (!dayPatterns[dayOfWeek]) {
dayPatterns[dayOfWeek] = [];
}
dayPatterns[dayOfWeek].push(row.metrics.conversions || 0);
});
// Calcular promedio por día
const dayAverages = Object.entries(dayPatterns).map(([day, conversions]) => ({
day: parseInt(day),
avgConversions: conversions.reduce((a, b) => a + b, 0) / conversions.length,
}));
// Identificar mejores y peores días
const sorted = [...dayAverages].sort((a, b) => b.avgConversions - a.avgConversions);
return {
bestDays: sorted.slice(0, 2),
worstDays: sorted.slice(-2),
pattern: 'Weekly seasonality detected',
};
}
function generateProjections(historical, trends, seasonality, days, scenario, budgetIncrease) {
const dailyProjections = [];
const baselineAvg = historical.slice(-30).reduce((sum, d) => sum + (d.metrics.conversions || 0), 0) / 30;
// Factores según escenario
const scenarioFactors = {
conservative: 0.9,
realistic: 1.0,
aggressive: 1.15,
};
const factor = scenarioFactors[scenario];
const budgetFactor = budgetIncrease ? 1 + (budgetIncrease / 100) * 0.7 : 1;
// Generar proyecciones diarias
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() + i + 1);
// Aplicar tendencia y factores
let projectedConversions = baselineAvg * factor * budgetFactor;
// Aplicar ligera tendencia
if (trends.conversions.trend === 'INCREASING') {
projectedConversions *= 1 + (trends.conversions.changeRate * 0.5);
}
dailyProjections.push({
date: date.toISOString().split('T')[0],
conversions: Math.round(projectedConversions),
confidence: scenario === 'realistic' ? 'MEDIUM' : 'LOW',
});
}
// Agregar a semanas
const weeklyProjections = [];
for (let i = 0; i < days; i += 7) {
const weekData = dailyProjections.slice(i, i + 7);
const weekTotal = weekData.reduce((sum, d) => sum + d.conversions, 0);
weeklyProjections.push({
weekNumber: Math.floor(i / 7) + 1,
conversions: weekTotal,
});
}
return {
daily: dailyProjections,
weekly: weeklyProjections,
};
}
function calculateForecastMetrics(projections) {
const totalProjectedConversions = projections.daily.reduce((sum, d) => sum + d.conversions, 0);
const avgDailyConversions = totalProjectedConversions / projections.daily.length;
return {
totalProjectedConversions,
avgDailyConversions,
weeklyAverage: projections.weekly.length > 0
? projections.weekly.reduce((sum, w) => sum + w.conversions, 0) / projections.weekly.length
: 0,
};
}
function analyzeforecastRisks(projections, trends) {
const risks = [];
if (trends.conversions.trend === 'DECREASING') {
risks.push({
type: 'TREND_RISK',
severity: 'MEDIUM',
description: 'Tendencia decreciente puede impactar proyecciones',
mitigation: 'Revisar y optimizar campañas para revertir tendencia',
});
}
risks.push({
type: 'MARKET_RISK',
severity: 'LOW',
description: 'Cambios en competencia o demanda no considerados',
mitigation: 'Monitorear competidores y ajustar pujas si es necesario',
});
return risks;
}
function calculateConfidenceLevel(historical, trends) {
// Basado en la cantidad de datos y estabilidad
if (historical.length < 30)
return 'LOW';
const variance = calculateVariance(historical.map(d => d.metrics.conversions || 0));
if (variance < 0.2)
return 'HIGH';
if (variance < 0.5)
return 'MEDIUM';
return 'LOW';
}
function calculateVariance(numbers) {
const mean = numbers.reduce((a, b) => a + b, 0) / numbers.length;
const variance = numbers.reduce((sum, num) => sum + Math.pow(num - mean, 2), 0) / numbers.length;
return Math.sqrt(variance) / mean; // Coeficiente de variación
}
function generateForecastRecommendations(metrics, scenario, budgetIncrease) {
const recommendations = [];
if (scenario === 'conservative' && budgetIncrease && budgetIncrease > 20) {
recommendations.push('⚠️ Escenario conservador con alto incremento - considera enfoque gradual');
}
if (metrics.avgDailyConversions > 100) {
recommendations.push('📊 Alto volumen proyectado - asegura capacidad de fulfillment');
}
recommendations.push('🔄 Revisa proyecciones semanalmente y ajusta según resultados reales');
recommendations.push('💡 Implementa alertas automáticas si performance varía >20% de proyección');
return recommendations;
}
function interpretTrend(trend) {
if (trend > 0.2)
return '📈 Fuerte crecimiento - capitalizar momentum';
if (trend > 0)
return '📊 Crecimiento estable';
if (trend > -0.1)
return '➡️ Estable con ligera caída';
return '📉 Tendencia negativa - requiere intervención';
}
function simulateBiddingChange(campaign, newStrategy, target) {
// Simulación básica basada en datos históricos y mejores prácticas
const currentConversions = campaign.metrics.conversions || 0;
const currentCost = campaign.metrics.cost_micros / 1_000_000;
let projectedChange = {
conversions: 0,
cost: 0,
confidence: 'MEDIUM',
};
switch (newStrategy) {
case 'TARGET_ROAS':
projectedChange.conversions = currentConversions * 1.15;
projectedChange.cost = currentCost * 1.05;
break;
case 'TARGET_CPA':
projectedChange.conversions = currentConversions * 1.25;
projectedChange.cost = currentCost * 1.1;
break;
case 'MAXIMIZE_CONVERSIONS':
projectedChange.conversions = currentConversions * 1.3;
projectedChange.cost = currentCost * 1.2;
break;
}
return {
projected: projectedChange,
timeToResults: '2-3 semanas para estabilización',
confidence: currentConversions > 50 ? 'HIGH' : 'MEDIUM',
};
}
function generateSmartBiddingBestPractices(recommendations) {
const practices = [];
const targetRoasCount = recommendations.filter(r => r.suggestedStrategy === 'TARGET_ROAS').length;
if (targetRoasCount > 0) {
practices.push('🎯 Para Target ROAS: Comienza con objetivo 10-20% menor al actual');
}
practices.push('⏰ Permite 2-3 semanas de aprendizaje antes de hacer ajustes');
practices.push('📊 No cambies objetivos más de 20% a la vez');
practices.push('🔍 Monitorea Search Terms Report semanalmente');
return practices;
}
//# sourceMappingURL=budget.js.map