Skill: liji3597/solana-dca (version 0.1.0)

--- name: solana-dca description: 创建和管理 DCA 定投策略 — 支持自动周期性购买 Solana 代币。当用户想设置定投、查看策略或调整自动购买计划时触发。 version: 1.0.0 metadata: openclaw: requires: env: ["SOLANA_NETWORK"] bins: ["node"] emoji: "📈" --- # Solana DCA 定投策略引擎 ## When to Use - 用户想设置自动定投（DCA / 定期购买） - 用户想查看、暂停或恢复已有策略 - 用户提到"定投"、"DCA"、"自动买"、"每周/每天买" ## Workflow ### 用户想创建定投策略 用户可能不会一次给齐所有参数，逐步引导： 1. **确定目标代币** — 如果未指定，询问："你想定投哪个代币？（SOL / JUP / BONK / RAY）" 2. **确定金额** — 如果未指定，询问："每次投入多少 USDC？" 3. **确定频率** — 如果未指定，询问："多久执行一次？" - 可选：`daily`（每天）、`weekly`（每周）、`monthly`（每月）、`6hours`（每6小时） 4. **确认参数** — 执行前总结： > "确认创建策略：每周投入 100 USDC 购买 SOL，确定吗？" 5. **执行** — `node skills/solana-dca/scripts/create-dca.js <user_id> <token> <amount> <schedule>` 6. **后续建议** — "策略已创建！你可以随时说'查看策略'来查看状态" ### 用户想查看策略 1. `node skills/solana-dca/scripts/list-strategies.js <user_id>` 2. 如果无策略，引导创建："还没有策略，要创建一个吗？" 3. 如果有策略，清晰展示每个策略的状态 ### 用户想暂停策略 1. 如果用户没指定 ID，先列出所有策略让用户选择 2. 确认操作："确定暂停策略 #X 吗？" 3. `node skills/solana-dca/scripts/pause-strategy.js <user_id> <strategy_id>` ### 用户想恢复策略 1. 列出已暂停的策略 2. `node skills/solana-dca/scripts/resume-strategy.js <user_id> <strategy_id>` ## Guardrails - **首次使用必须告知** — "当前 DCA 为模拟功能，记录策略但不实际执行链上交换" - **确认后再创建** — 永远在执行前让用户确认参数 - **不推荐频率** — 不说"建议每周定投"，让用户自己决定 - **金额限制** — 提醒用户根据自身风险承受能力设置金额 ## Available Scripts | 脚本 | 用途 | 参数 | |------|------|------| | `create-dca.js` | 创建策略 | `<user_id> <token> <amount_usdc> <daily\|weekly\|monthly\|6hours>` | | `list-strategies.js` | 列出策略 | `<user_id> [--lang en]` | | `pause-strategy.js` | 暂停策略 | `<user_id> <strategy_id>` | | `resume-strategy.js` | 恢复策略 | `<user_id> <strategy_id>` |

path: scripts/create-dca.js
content: #!/usr/bin/env node
/**
 * Create a DCA strategy
 * Usage: node create-dca.js <telegram_user_id> <target_token> <amount_usdc> <schedule> [--lang zh|en]
 * schedule: daily | weekly | monthly | 6hours
 */
const path = require('path');
const sharedDir = path.resolve(__dirname, '..', '..', '..', 'shared');

const { createStrategy } = require(path.join(sharedDir, 'services'));
const { formatError } = require(path.join(sharedDir, 'errors'));
const { resolveToken } = require(path.join(sharedDir, 'price-service'));

const telegramId = process.argv[2];
const targetToken = process.argv[3];
const amountRaw = process.argv[4];
const schedule = process.argv[5];
const lang = process.argv.includes('--lang') ? process.argv[process.argv.indexOf('--lang') + 1] : 'zh';

function printMissingParams(missing) {
    console.log(JSON.stringify({
        code: 'MISSING_PARAMS',
        missing,
        message: `我需要更多信息才能继续：${missing.join('、')} / I need more information to proceed: ${missing.join(', ')}`
    }, null, 2));
    process.exit(0);
}

const missing = [];
if (!telegramId) missing.push('用户ID / User ID');
if (!targetToken) missing.push('目标代币 / Target token');
if (!amountRaw) missing.push('金额 / Amount');
if (!schedule) missing.push('频率 / Schedule');
if (missing.length > 0) {
    printMissingParams(missing);
}

const amount = Number.parseFloat(amountRaw);
if (!Number.isFinite(amount) || amount <= 0) {
    console.log(`❌ ${formatError('MISSING_REQUIRED_PARAMS', lang)}: ${lang === 'zh' ? '金额必须是大于 0 的数字' : 'Amount must be a number greater than 0'}`);
    process.exit(1);
}

const schedules = {
    daily: '0 9 * * *',
    weekly: '0 9 * * 1',
    monthly: '0 9 1 * *',
    '6hours': '0 */6 * * *',
};

const cron = schedules[schedule];
if (!cron) {
    console.log(`❌ ${formatError('MISSING_REQUIRED_PARAMS', lang)}: ${lang === 'zh' ? '无效频率（支持: daily, weekly, monthly, 6hours）' : 'Invalid schedule (supported: daily, weekly, monthly, 6hours)'}`);
    process.exit(1);
}

if (typeof createStrategy !== 'function') {
    console.log(`❌ ${formatError('INTERNAL_ERROR', lang)}`);
    process.exit(1);
}

const symbol = targetToken.toUpperCase();
if (!resolveToken(symbol)) {
    console.log(`❌ ${formatError('UNKNOWN_TOKEN', lang)}`);
    process.exit(1);
}

const name = `DCA ${amount} USDC -> ${symbol}`;

const result = createStrategy(telegramId, {
    name,
    source_token: 'USDC',
    target_token: symbol,
    amount,
    cron_expression: cron,
});

if (!result.ok) {
    console.log(`❌ ${formatError(result.code, lang)}`);
    process.exit(1);
}

const scheduleNames = {
    daily: '每天 9:00 UTC / Daily 9:00 UTC',
    weekly: '每周一 9:00 UTC / Every Monday 9:00 UTC',
    monthly: '每月1日 9:00 UTC / Monthly 1st 9:00 UTC',
    '6hours': '每6小时 / Every 6 hours',
};

console.log(`🎉 ${lang === 'zh' ? 'DCA 策略已创建' : 'DCA strategy created'} / Strategy created!`);
console.log(`  ID: #${result.strategyId}`);
console.log(`  名称 / Name: ${name}`);
console.log(`  频率 / Schedule: ${scheduleNames[schedule]}`);
console.log('  状态 / Status: active');


path: scripts/list-strategies.js
content: #!/usr/bin/env node
/**
 * List DCA strategies for a user
 * Usage: node list-strategies.js <telegram_user_id> [--lang zh|en]
 */
const path = require('path');
const sharedDir = path.resolve(__dirname, '..', '..', '..', 'shared');

const { listStrategies } = require(path.join(sharedDir, 'services'));
const { formatError } = require(path.join(sharedDir, 'errors'));
const { formatStrategy } = require(path.join(sharedDir, 'formatter'));

const telegramId = process.argv[2];
const lang = process.argv.includes('--lang') ? process.argv[process.argv.indexOf('--lang') + 1] : 'zh';

if (!telegramId) {
    console.log(JSON.stringify({
        code: 'MISSING_PARAMS',
        missing: ['用户ID / User ID'],
        message: '我需要更多信息才能继续：用户ID / User ID / I need more information to proceed: User ID'
    }, null, 2));
    process.exit(0);
}

if (typeof listStrategies !== 'function') {
    console.log(`❌ ${formatError('INTERNAL_ERROR', lang)}`);
    process.exit(1);
}

const result = listStrategies(telegramId);
if (!result.ok) {
    console.log(`❌ ${formatError(result.code, lang)}`);
    process.exit(1);
}

const strategies = result.strategies || [];
if (strategies.length === 0) {
    console.log('📭 暂无定投策略 / No strategies yet');
    process.exit(0);
}

console.log(`📋 定投策略列表 / DCA Strategies (${strategies.length}):\n`);
for (const strategy of strategies) {
    console.log(formatStrategy(strategy, lang));
    console.log('');
}


path: scripts/pause-strategy.js
content: #!/usr/bin/env node
/**
 * Pause a DCA strategy
 * Usage: node pause-strategy.js <telegram_user_id> <strategy_id> [--lang zh|en]
 */
const path = require('path');
const sharedDir = path.resolve(__dirname, '..', '..', '..', 'shared');
const { pauseStrategy } = require(path.join(sharedDir, 'services'));
const { formatError } = require(path.join(sharedDir, 'errors'));

const telegramId = process.argv[2];
const strategyId = process.argv[3];
const lang = process.argv.includes('--lang') ? process.argv[process.argv.indexOf('--lang') + 1] : 'zh';

// Parameter validation with MISSING_PARAMS protocol
if (!telegramId || !strategyId) {
    const missing = [];
    if (!telegramId) missing.push(lang === 'zh' ? '用户ID' : 'User ID');
    if (!strategyId) missing.push(lang === 'zh' ? '策略ID' : 'Strategy ID');

    console.log(JSON.stringify({
        code: 'MISSING_PARAMS',
        missing,
        message: lang === 'zh'
            ? `我需要更多信息才能继续：${missing.join('、')}`
            : `I need more information to proceed: ${missing.join(', ')}`
    }, null, 2));
    process.exit(0);
}

const result = pauseStrategy(telegramId, strategyId);

// Handle already paused (warning, not error)
if (result.code === 'STRATEGY_ALREADY_PAUSED') {
    console.log(`⚠️ ${formatError(result.code, lang)}`);
    process.exit(0);
}

// Handle actual errors
if (!result.ok) {
    console.log(`❌ ${formatError(result.code, lang)}`);
    process.exit(1);
}

// Success
console.log(`🟡 ${formatError('OK', lang).replace('操作成功', `策略 #${result.strategyId} 已暂停`)} / Strategy #${result.strategyId} paused`);


path: scripts/resume-strategy.js
content: #!/usr/bin/env node
/**
 * Resume a paused DCA strategy
 * Usage: node resume-strategy.js <telegram_user_id> <strategy_id> [--lang zh|en]
 */
const path = require('path');
const sharedDir = path.resolve(__dirname, '..', '..', '..', 'shared');
const { resumeStrategy } = require(path.join(sharedDir, 'services'));
const { formatError } = require(path.join(sharedDir, 'errors'));

const telegramId = process.argv[2];
const strategyId = process.argv[3];
const lang = process.argv.includes('--lang') ? process.argv[process.argv.indexOf('--lang') + 1] : 'zh';

// Parameter validation with MISSING_PARAMS protocol
if (!telegramId || !strategyId) {
    const missing = [];
    if (!telegramId) missing.push(lang === 'zh' ? '用户ID' : 'User ID');
    if (!strategyId) missing.push(lang === 'zh' ? '策略ID' : 'Strategy ID');

    console.log(JSON.stringify({
        code: 'MISSING_PARAMS',
        missing,
        message: lang === 'zh'
            ? `我需要更多信息才能继续：${missing.join('、')}`
            : `I need more information to proceed: ${missing.join(', ')}`
    }, null, 2));
    process.exit(0);
}

const result = resumeStrategy(telegramId, strategyId);

// Handle already active (warning, not error)
if (result.code === 'STRATEGY_ALREADY_ACTIVE') {
    console.log(`⚠️ ${formatError(result.code, lang)}`);
    process.exit(0);
}

// Handle actual errors
if (!result.ok) {
    console.log(`❌ ${formatError(result.code, lang)}`);
    process.exit(1);
}

// Success
console.log(`🟢 ${formatError('OK', lang).replace('操作成功', `策略 #${result.strategyId} 已恢复`)} / Strategy #${result.strategyId} resumed`);


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)