-
Notifications
You must be signed in to change notification settings - Fork 0
fix(ads-simulator): address code review — CLI validation, constants, … #143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,10 +5,12 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| * telegram-ads-simulator.js | ||||||||||||||||||||||||||||||||||||||||||||||||
| * RuneWager Telegram Ads Simulator v1.0 | ||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Validates 30 Telegram ad creatives for compliance, simulates user behavior | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Validates Telegram ad creatives for compliance, simulates user behavior | ||||||||||||||||||||||||||||||||||||||||||||||||
| * across time buckets, days of week, channel types, and user personas, runs | ||||||||||||||||||||||||||||||||||||||||||||||||
| * an auto-optimization loop across 3 iterations, and outputs structured JSON | ||||||||||||||||||||||||||||||||||||||||||||||||
| * followed by a plain-text human-readable summary. | ||||||||||||||||||||||||||||||||||||||||||||||||
| * an auto-optimization loop, and outputs structured JSON followed by a | ||||||||||||||||||||||||||||||||||||||||||||||||
| * plain-text human-readable summary. | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Key tuning constants (TOP_COMBOS, OPT_ITERATIONS, etc.) are centralized | ||||||||||||||||||||||||||||||||||||||||||||||||
| * in the CONSTANTS block below. | ||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Usage: | ||||||||||||||||||||||||||||||||||||||||||||||||
| * node scripts/telegram-ads-simulator.js | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -27,10 +29,30 @@ const path = require('path'); | |||||||||||||||||||||||||||||||||||||||||||||||
| // CONSTANTS | ||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_LENGTH = 160; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const BOT_LINK = 'https://t.me/RuneWager_bot'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_LENGTH = 160; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const BOT_LINK = 'https://t.me/RuneWager_bot'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const TELEGRAM_LINK_RE = /^https:\/\/(t\.me|telegram\.me)\/[A-Za-z0-9_]+(?:\/\d+)?$/; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const FORBIDDEN_WORDS = ['cashout', 'withdraw', 'payout']; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const FORBIDDEN_WORDS = ['cashout', 'withdraw', 'payout']; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Simulation tuning — change here to adjust the whole simulation | ||||||||||||||||||||||||||||||||||||||||||||||||
| const IMPRESSIONS_BASE = 10000; // Baseline impressions per ad run at bid=1.0 | ||||||||||||||||||||||||||||||||||||||||||||||||
| const OPT_ITERATIONS = 3; // Number of copy-optimization iterations to run | ||||||||||||||||||||||||||||||||||||||||||||||||
| const TOP_COMBOS = 20; // Top combinations stored per ad in by_time_day_channel_persona | ||||||||||||||||||||||||||||||||||||||||||||||||
| const TOP_AD_COUNT = 3; // Number of top ads to select and report | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const USAGE = [ | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'Usage: node telegram-ads-simulator.js [options]', | ||||||||||||||||||||||||||||||||||||||||||||||||
| '', | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'Options:', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ' --output <path> Write full JSON + summary to <path>; print summary to stdout.', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ' --summary-only Print only the human-readable plain-text summary.', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ' --json-only Print only the machine-readable JSON.', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ' (no flags) Print JSON then summary to stdout.', | ||||||||||||||||||||||||||||||||||||||||||||||||
| '', | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'Constraints:', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ' --summary-only and --json-only are mutually exclusive.', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ' --output cannot be combined with --summary-only or --json-only.', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ].join('\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||||||
| // RAW AD CANDIDATES (30 variants) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -464,7 +486,7 @@ function bidSensitivity(baselineCTR, baselineCPC, baselineROI) { | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||
| bid, | ||||||||||||||||||||||||||||||||||||||||||||||||
| estimated_impressions: Math.round(10000 * profile.impressionFactor), | ||||||||||||||||||||||||||||||||||||||||||||||||
| estimated_impressions: Math.round(IMPRESSIONS_BASE * profile.impressionFactor), | ||||||||||||||||||||||||||||||||||||||||||||||||
| CTR: adjCTR, | ||||||||||||||||||||||||||||||||||||||||||||||||
| CPC, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ROI_index: ROI, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -622,7 +644,7 @@ function runAdSimulation(validatedAd) { | |||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Aggregate baseline across all 1,512 combinations | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Aggregate baseline across all combinations | ||||||||||||||||||||||||||||||||||||||||||||||||
| const n = allCombos.length; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const mean = key => r4(allCombos.reduce((s, c) => s + c[key], 0) / n); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -636,11 +658,11 @@ function runAdSimulation(validatedAd) { | |||||||||||||||||||||||||||||||||||||||||||||||
| ROI_index: mean('ROI_index'), | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Top 20 combinations by ROI_index (sorted copy, no mutation of original) | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Top combinations by ROI_index (sorted copy, no mutation of original) | ||||||||||||||||||||||||||||||||||||||||||||||||
| const top20 = allCombos | ||||||||||||||||||||||||||||||||||||||||||||||||
| .slice() | ||||||||||||||||||||||||||||||||||||||||||||||||
| .sort((a, b) => b.ROI_index - a.ROI_index) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .slice(0, 20); | ||||||||||||||||||||||||||||||||||||||||||||||||
| .slice(0, TOP_COMBOS); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Bid sensitivity at baseline aggregate metrics | ||||||||||||||||||||||||||||||||||||||||||||||||
| const bid_sensitivity = bidSensitivity( | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -649,8 +671,8 @@ function runAdSimulation(validatedAd) { | |||||||||||||||||||||||||||||||||||||||||||||||
| baseline_metrics.ROI_index, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // 3-iteration optimization | ||||||||||||||||||||||||||||||||||||||||||||||||
| const optimized_versions = [1, 2, 3].map(iter => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Optimization iterations | ||||||||||||||||||||||||||||||||||||||||||||||||
| const optimized_versions = Array.from({ length: OPT_ITERATIONS }, (_, i) => i + 1).map(iter => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| iteration: iter, | ||||||||||||||||||||||||||||||||||||||||||||||||
| text: optimizeText(text, category, iter), | ||||||||||||||||||||||||||||||||||||||||||||||||
| metrics: iterMetrics(baseline_metrics, iter), | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -681,7 +703,7 @@ function runAdSimulation(validatedAd) { | |||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| function selectTopAds(adResults, validatedAds) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const scored = adResults.map(r => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const m = r.optimized_versions[2].metrics; // iter-3 metrics | ||||||||||||||||||||||||||||||||||||||||||||||||
| const m = r.optimized_versions[OPT_ITERATIONS - 1].metrics; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const score = m.ROI_index * 0.50 | ||||||||||||||||||||||||||||||||||||||||||||||||
| + m.overall_prize_redemption_rate * 100 * 0.30 | ||||||||||||||||||||||||||||||||||||||||||||||||
| + m.overall_leaderboard_participation_rate * 100 * 0.20; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -690,35 +712,51 @@ function selectTopAds(adResults, validatedAds) { | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| scored.sort((a, b) => b.score - a.score); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return scored.slice(0, 3).map(({ ad_id, result }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const iter3 = result.optimized_versions[2]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const m = iter3.metrics; | ||||||||||||||||||||||||||||||||||||||||||||||||
| return scored.slice(0, TOP_AD_COUNT).map(({ ad_id, result }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const finalIter = result.optimized_versions[OPT_ITERATIONS - 1]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const m = finalIter.metrics; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Best bid: highest ROI_index from bid_sensitivity | ||||||||||||||||||||||||||||||||||||||||||||||||
| const bestBidEntry = result.bid_sensitivity | ||||||||||||||||||||||||||||||||||||||||||||||||
| .slice() | ||||||||||||||||||||||||||||||||||||||||||||||||
| .sort((a, b) => b.ROI_index - a.ROI_index)[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Aggregate best time windows from top-5 combinations | ||||||||||||||||||||||||||||||||||||||||||||||||
| const top5 = result.by_time_day_channel_persona.slice(0, 5); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Aggregate best time windows across ALL top combinations (not just top-5). | ||||||||||||||||||||||||||||||||||||||||||||||||
| // For each channel, accumulate ROI-weighted counts for every time bucket and | ||||||||||||||||||||||||||||||||||||||||||||||||
| // day of week that appears in the top-combo set, then select the most | ||||||||||||||||||||||||||||||||||||||||||||||||
| // ROI-weighted time bucket and top-3 days per channel. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const windowMap = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||
| for (const combo of top5) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| for (const combo of result.by_time_day_channel_persona) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const key = combo.channel_type; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!windowMap[key]) windowMap[key] = { times: new Set(), days: new Set() }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowMap[key].times.add(combo.time_bucket); | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowMap[key].days.add(combo.day_of_week); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!windowMap[key]) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowMap[key] = { timeWeight: {}, dayWeight: {}, combos: 0 }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| const w = windowMap[key]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| w.timeWeight[combo.time_bucket] = (w.timeWeight[combo.time_bucket] || 0) + combo.ROI_index; | ||||||||||||||||||||||||||||||||||||||||||||||||
| w.dayWeight[combo.day_of_week] = (w.dayWeight[combo.day_of_week] || 0) + combo.ROI_index; | ||||||||||||||||||||||||||||||||||||||||||||||||
| w.combos++; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const best_time_windows = Object.entries(windowMap).map(([ch, v]) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| time_bucket: [...v.times][0] || '16:00-20:00', | ||||||||||||||||||||||||||||||||||||||||||||||||
| days_of_week: [...v.days], | ||||||||||||||||||||||||||||||||||||||||||||||||
| channel_types: [ch], | ||||||||||||||||||||||||||||||||||||||||||||||||
| reason: `Top ROI_index combination for ${ch} in this ad`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const best_time_windows = Object.entries(windowMap).map(([ch, v]) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Pick the single time bucket with the highest accumulated ROI weight | ||||||||||||||||||||||||||||||||||||||||||||||||
| const topTime = Object.entries(v.timeWeight) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .sort((a, b) => b[1] - a[1])[0][0]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Pick the top-3 days by accumulated ROI weight | ||||||||||||||||||||||||||||||||||||||||||||||||
| const topDays = Object.entries(v.dayWeight) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .sort((a, b) => b[1] - a[1]) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .slice(0, 3) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .map(([day]) => day); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||
| time_bucket: topTime, | ||||||||||||||||||||||||||||||||||||||||||||||||
| days_of_week: topDays, | ||||||||||||||||||||||||||||||||||||||||||||||||
| channel_types: [ch], | ||||||||||||||||||||||||||||||||||||||||||||||||
| reason: `Highest ROI-weighted time/day for ${ch} across ${v.combos} top combinations`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||
| ad_id, | ||||||||||||||||||||||||||||||||||||||||||||||||
| final_text: iter3.text, | ||||||||||||||||||||||||||||||||||||||||||||||||
| final_text: finalIter.text, | ||||||||||||||||||||||||||||||||||||||||||||||||
| best_bid: bestBidEntry.bid, | ||||||||||||||||||||||||||||||||||||||||||||||||
| best_time_windows, | ||||||||||||||||||||||||||||||||||||||||||||||||
| summary_metrics: { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -862,7 +900,7 @@ function generateSummary(output) { | |||||||||||||||||||||||||||||||||||||||||||||||
| lines.push(`Total ads validated: ${output.validated_ads.length}`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| lines.push(`Fully compliant (no changes needed): ${compliant}`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| lines.push(`Auto-fixed (minor edits applied): ${violations}`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| lines.push('All 30 ads are Telegram Ads compliant after validation.'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| lines.push(`All ${output.validated_ads.length} ads are Telegram Ads compliant after validation.`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| lines.push(''); | ||||||||||||||||||||||||||||||||||||||||||||||||
| lines.push(`Simulation ran: ${output.validated_ads.length} ads x ${output.simulation_config.time_buckets.length} time buckets x ${output.simulation_config.days_of_week.length} days x ${output.simulation_config.channel_types.length} channels x ${output.simulation_config.personas.length} personas = ${output.validated_ads.length * output.simulation_config.time_buckets.length * output.simulation_config.days_of_week.length * output.simulation_config.channel_types.length * output.simulation_config.personas.length} combinations`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| lines.push(''); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -881,42 +919,68 @@ function pct(v) { return (v * 100).toFixed(2) + '%'; } | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Orchestrates the full simulation pipeline: | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 1. Validate all 30 ads | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 2. Run simulation across all dimension combinations | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 3. Run 3-iteration optimization loop | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 4. Select top 3 ads | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 5. Output PART A (JSON) then PART B (plain-text summary) | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 1. Parse and validate CLI arguments — fail fast on bad input | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 2. Validate all ads for Telegram Ads compliance | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 3. Run simulation across all dimension combinations | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 4. Run OPT_ITERATIONS-iteration optimization loop | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 5. Select top TOP_AD_COUNT ads | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 6. Output PART A (JSON) then PART B (plain-text summary) | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| function main() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const args = process.argv.slice(2); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const outputIdx = args.indexOf('--output'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const outputFile = outputIdx >= 0 ? args[outputIdx + 1] : null; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // ---- Argument parsing ---- | ||||||||||||||||||||||||||||||||||||||||||||||||
| const summaryOnly = args.includes('--summary-only'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const jsonOnly = args.includes('--json-only'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const jsonOnly = args.includes('--json-only'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const outputIdx = args.indexOf('--output'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const outputFile = outputIdx >= 0 ? args[outputIdx + 1] : null; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Fail fast with a clear message on invalid argument combinations | ||||||||||||||||||||||||||||||||||||||||||||||||
| const die = msg => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.stderr.write(`Error: ${msg}\n\n${USAGE}\n`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (summaryOnly && jsonOnly) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| die('--summary-only and --json-only are mutually exclusive.'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (outputIdx >= 0 && (!outputFile || outputFile.startsWith('--'))) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| die('--output requires a file path argument (e.g. --output results/ads.json).'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (outputFile && (summaryOnly || jsonOnly)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| die('--output cannot be combined with --summary-only or --json-only.'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| const KNOWN_FLAGS = new Set(['--output', '--summary-only', '--json-only']); | ||||||||||||||||||||||||||||||||||||||||||||||||
| for (const arg of args) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (arg.startsWith('--') && !KNOWN_FLAGS.has(arg)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| die(`Unknown flag "${arg}".`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+950
to
+958
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Consider allowing a help flag instead of treating it as an unknown option. Because of the strict
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // ---- Simulation ---- | ||||||||||||||||||||||||||||||||||||||||||||||||
| const log = msg => process.stderr.write(msg + '\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const adCount = RAW_ADS.length; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| log('RuneWager Telegram Ads Simulator v1.0'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| log('--------------------------------------'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| log('Step 1: Validating 30 ad creatives for Telegram Ads compliance...'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| log(`Step 1: Validating ${adCount} ad creatives for Telegram Ads compliance...`); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const validated_ads = RAW_ADS.map((text, i) => validateAd(text, i)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const fixedCount = validated_ads.filter(a => !a.compliant).length; | ||||||||||||||||||||||||||||||||||||||||||||||||
| log(` Done. ${validated_ads.length - fixedCount} fully compliant, ${fixedCount} auto-fixed.`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| log(` Done. ${adCount - fixedCount} fully compliant, ${fixedCount} auto-fixed.`); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const totalCombos = validated_ads.length | ||||||||||||||||||||||||||||||||||||||||||||||||
| * TIME_BUCKETS.length * DAYS_OF_WEEK.length | ||||||||||||||||||||||||||||||||||||||||||||||||
| * CHANNEL_TYPES.length * PERSONAS.length; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const combosPerAd = TIME_BUCKETS.length * DAYS_OF_WEEK.length * CHANNEL_TYPES.length * PERSONAS.length; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const totalCombos = adCount * combosPerAd; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| log(`Step 2: Running simulation (${totalCombos.toLocaleString()} total combinations)...`); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const ad_results = validated_ads.map((ad, i) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if ((i + 1) % 5 === 0 || i === 0) log(` Ad ${i + 1}/30 simulated...`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if ((i + 1) % 5 === 0 || i === 0) log(` Ad ${i + 1}/${adCount} simulated...`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return runAdSimulation(ad); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| log(' Done. Baseline metrics, bid sensitivity, and 3-iteration optimization computed.'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| log(` Done. Baseline metrics, bid sensitivity, and ${OPT_ITERATIONS}-iteration optimization computed.`); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| log('Step 3: Selecting top 3 ads by composite score...'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| log(`Step 3: Selecting top ${TOP_AD_COUNT} ads by composite score...`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const top_ads = selectTopAds(ad_results, validated_ads); | ||||||||||||||||||||||||||||||||||||||||||||||||
| log(` Top ads: #${top_ads.map(a => a.ad_id).join(', #')}`); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -929,8 +993,11 @@ function main() { | |||||||||||||||||||||||||||||||||||||||||||||||
| violations: a.violations, | ||||||||||||||||||||||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||||||||||||||||||||||
| simulation_config: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| impressions_per_simulation: 10000, | ||||||||||||||||||||||||||||||||||||||||||||||||
| impressions_per_simulation: IMPRESSIONS_BASE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| base_bid: 1.0, | ||||||||||||||||||||||||||||||||||||||||||||||||
| top_combos_per_ad: TOP_COMBOS, | ||||||||||||||||||||||||||||||||||||||||||||||||
| optimization_iterations: OPT_ITERATIONS, | ||||||||||||||||||||||||||||||||||||||||||||||||
| top_ads_selected: TOP_AD_COUNT, | ||||||||||||||||||||||||||||||||||||||||||||||||
| time_buckets: TIME_BUCKETS, | ||||||||||||||||||||||||||||||||||||||||||||||||
| days_of_week: DAYS_OF_WEEK, | ||||||||||||||||||||||||||||||||||||||||||||||||
| channel_types: CHANNEL_TYPES, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -951,34 +1018,29 @@ function main() { | |||||||||||||||||||||||||||||||||||||||||||||||
| HighRollerCasinoChat: 'High-value casino players. Best prize redemption and bonus completion rates.', | ||||||||||||||||||||||||||||||||||||||||||||||||
| CasualGamingChannel: 'Casual gaming community. Lower conversion for casino-style content.', | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| note: 'by_time_day_channel_persona contains the top 20 combinations by ROI_index per ad (out of 1,512 total combinations each).', | ||||||||||||||||||||||||||||||||||||||||||||||||
| note: `by_time_day_channel_persona contains the top ${TOP_COMBOS} combinations by ROI_index per ad (out of ${combosPerAd} total combinations each).`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ad_results, | ||||||||||||||||||||||||||||||||||||||||||||||||
| top_ads, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| log('Step 4: Generating output...'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const jsonStr = JSON.stringify(output, null, 2); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const jsonStr = JSON.stringify(output, null, 2); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const summaryStr = generateSummary(output); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (outputFile) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Write full output to file | ||||||||||||||||||||||||||||||||||||||||||||||||
| const fullContent = jsonStr + '\n\n' + summaryStr + '\n'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| fs.mkdirSync(path.dirname(path.resolve(outputFile)), { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(outputFile, fullContent, 'utf8'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(outputFile, jsonStr + '\n\n' + summaryStr + '\n', 'utf8'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| log(` Full output written to: ${outputFile}`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Always print summary to stdout when using --output | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.stdout.write(summaryStr + '\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (summaryOnly) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.stdout.write(summaryStr + '\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (jsonOnly) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.stdout.write(jsonStr + '\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Default: PART A (JSON) then PART B (summary) to stdout | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.stdout.write(jsonStr + '\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.stdout.write('\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.stdout.write(summaryStr + '\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.stdout.write(jsonStr + '\n\n' + summaryStr + '\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| log('Simulation complete.'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue: Guard against OPT_ITERATIONS being set to 0 or a negative value.
This logic relies on OPT_ITERATIONS being ≥ 1: you create
optimized_versionswithArray.from({ length: OPT_ITERATIONS })and later accessresult.optimized_versions[OPT_ITERATIONS - 1]. If OPT_ITERATIONS is ever set to 0 or negative,optimized_versionswill be empty andselectTopAdswill hitundefined.metricsat runtime. Consider either validating OPT_ITERATIONS (e.g., fail fast if < 1) or handling an emptyoptimized_versionsarray when selecting top ads.