231 lines
7.9 KiB
TypeScript
231 lines
7.9 KiB
TypeScript
import 'dotenv/config';
|
|
import { createClient } from '@supabase/supabase-js';
|
|
|
|
type SeedProfile = {
|
|
name: string;
|
|
allocated_capital: number;
|
|
risk_per_trade_percent: number;
|
|
symbols: string;
|
|
is_active: boolean;
|
|
strategy_config: Record<string, any>;
|
|
};
|
|
|
|
const supabaseUrl = String(process.env.SUPABASE_URL || '').trim();
|
|
const supabaseKey = String(
|
|
process.env.SUPABASE_KEY
|
|
|| process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
|| process.env.SUPABASE_ANON_KEY
|
|
|| ''
|
|
).trim();
|
|
|
|
if (!supabaseUrl || !supabaseKey) {
|
|
throw new Error('Missing Supabase credentials. Expected SUPABASE_URL and SUPABASE_KEY/SUPABASE_SERVICE_ROLE_KEY.');
|
|
}
|
|
|
|
const supabase = createClient(supabaseUrl, supabaseKey);
|
|
const args = process.argv.slice(2);
|
|
const userIdArg = args.find((arg) => arg.startsWith('--user-id='))?.split('=')[1]?.trim();
|
|
const dryRun = args.includes('--dry-run');
|
|
|
|
const PROFILE_SPLIT = {
|
|
aggressive: 1500,
|
|
conservative: 1500
|
|
};
|
|
|
|
const buildProfiles = (): SeedProfile[] => {
|
|
const frequentMomentum: SeedProfile = {
|
|
name: 'Frequent Momentum Pro',
|
|
allocated_capital: PROFILE_SPLIT.aggressive,
|
|
risk_per_trade_percent: 1.2,
|
|
symbols: 'BTC/USDT, ETH/USDT, SOL/USDT',
|
|
is_active: true,
|
|
strategy_config: {
|
|
rules: [
|
|
{ ruleId: 'TrendBiasRule', enabled: true, params: { fastPeriod: 20, slowPeriod: 100 } },
|
|
{ ruleId: 'SessionRule', enabled: true, params: { sessions: ['NY', 'LDN'] } },
|
|
{ ruleId: 'ZoneRule', enabled: true, params: { timeframe: '15m', zonePercent: 1.2 } },
|
|
{ ruleId: 'MomentumRule', enabled: true, params: { timeframe: '15m', rsiPeriod: 9, overbought: 72, oversold: 28 } },
|
|
{
|
|
ruleId: 'EntryTriggerRule',
|
|
enabled: true,
|
|
params: {
|
|
timeframe: '15m',
|
|
wickRatioThreshold: 0.45,
|
|
enableEmaReclaim: true,
|
|
enableWickRejection: true
|
|
}
|
|
},
|
|
{ ruleId: 'RiskManagementRule', enabled: true, params: { atrPeriod: 10, slMultiplier: 1.2, maxRisk: 1.2 } },
|
|
{ ruleId: 'AIAnalysisRule', enabled: false, params: { minConfidence: 70 } }
|
|
],
|
|
riskLimits: {
|
|
maxDailyLossUsd: 45,
|
|
maxConsecutiveLosses: 3,
|
|
maxOpenTrades: 2
|
|
},
|
|
execution: {
|
|
orderType: 'market',
|
|
cooldownMinutes: 8,
|
|
entryMode: 'both',
|
|
profitExitPercent: 0.8
|
|
}
|
|
}
|
|
};
|
|
|
|
const controlledIntraday: SeedProfile = {
|
|
name: 'Controlled Intraday Guard',
|
|
allocated_capital: PROFILE_SPLIT.conservative,
|
|
risk_per_trade_percent: 0.8,
|
|
symbols: 'BTC/USDT, ETH/USDT',
|
|
is_active: true,
|
|
strategy_config: {
|
|
rules: [
|
|
{ ruleId: 'TrendBiasRule', enabled: true, params: { fastPeriod: 50, slowPeriod: 200 } },
|
|
{ ruleId: 'SessionRule', enabled: true, params: { sessions: ['NY', 'LDN'] } },
|
|
{ ruleId: 'ZoneRule', enabled: true, params: { timeframe: '1h', zonePercent: 1.0 } },
|
|
{ ruleId: 'MomentumRule', enabled: true, params: { timeframe: '1h', rsiPeriod: 14, overbought: 68, oversold: 32 } },
|
|
{
|
|
ruleId: 'EntryTriggerRule',
|
|
enabled: true,
|
|
params: {
|
|
timeframe: '1h',
|
|
wickRatioThreshold: 0.5,
|
|
enableEmaReclaim: true,
|
|
enableWickRejection: true
|
|
}
|
|
},
|
|
{ ruleId: 'RiskManagementRule', enabled: true, params: { atrPeriod: 14, slMultiplier: 1.0, maxRisk: 0.9 } },
|
|
{ ruleId: 'AIAnalysisRule', enabled: false, params: { minConfidence: 75 } }
|
|
],
|
|
riskLimits: {
|
|
maxDailyLossUsd: 30,
|
|
maxConsecutiveLosses: 2,
|
|
maxOpenTrades: 2
|
|
},
|
|
execution: {
|
|
orderType: 'market',
|
|
cooldownMinutes: 18,
|
|
entryMode: 'long_only',
|
|
profitExitPercent: 1.0
|
|
}
|
|
}
|
|
};
|
|
|
|
return [frequentMomentum, controlledIntraday];
|
|
};
|
|
|
|
const fetchTargetUsers = async (): Promise<string[]> => {
|
|
if (userIdArg) return [userIdArg];
|
|
|
|
const { data, error } = await supabase
|
|
.from('users')
|
|
.select('user_id')
|
|
.eq('trade_enable', true);
|
|
if (error) {
|
|
throw new Error(`Failed to fetch active users: ${error.message}`);
|
|
}
|
|
|
|
const ids = (data || [])
|
|
.map((row: any) => String(row?.user_id || '').trim())
|
|
.filter(Boolean);
|
|
return Array.from(new Set(ids));
|
|
};
|
|
|
|
const upsertProfileForUser = async (userId: string, profile: SeedProfile): Promise<'inserted' | 'updated'> => {
|
|
const { data: existing, error: existingError } = await supabase
|
|
.from('trade_profiles')
|
|
.select('id')
|
|
.eq('user_id', userId)
|
|
.eq('name', profile.name)
|
|
.order('created_at', { ascending: true })
|
|
.limit(1);
|
|
|
|
if (existingError) {
|
|
throw new Error(`Failed to query existing profile "${profile.name}" for user ${userId}: ${existingError.message}`);
|
|
}
|
|
|
|
const payload = {
|
|
user_id: userId,
|
|
name: profile.name,
|
|
allocated_capital: profile.allocated_capital,
|
|
risk_per_trade_percent: profile.risk_per_trade_percent,
|
|
symbols: profile.symbols,
|
|
is_active: profile.is_active,
|
|
strategy_config: profile.strategy_config
|
|
};
|
|
|
|
if (existing && existing.length > 0) {
|
|
if (!dryRun) {
|
|
const { error: updateError } = await supabase
|
|
.from('trade_profiles')
|
|
.update(payload)
|
|
.eq('id', existing[0].id);
|
|
if (updateError) {
|
|
throw new Error(`Failed to update profile "${profile.name}" for user ${userId}: ${updateError.message}`);
|
|
}
|
|
}
|
|
return 'updated';
|
|
}
|
|
|
|
if (!dryRun) {
|
|
const { error: insertError } = await supabase
|
|
.from('trade_profiles')
|
|
.insert([payload]);
|
|
if (insertError) {
|
|
throw new Error(`Failed to insert profile "${profile.name}" for user ${userId}: ${insertError.message}`);
|
|
}
|
|
}
|
|
return 'inserted';
|
|
};
|
|
|
|
const run = async (): Promise<void> => {
|
|
const users = await fetchTargetUsers();
|
|
const profiles = buildProfiles();
|
|
|
|
if (!users.length) {
|
|
console.log(JSON.stringify({ ok: true, dry_run: dryRun, users_targeted: 0, message: 'No active users found.' }, null, 2));
|
|
return;
|
|
}
|
|
|
|
let inserted = 0;
|
|
let updated = 0;
|
|
const perUser: Array<{ user_id: string; inserted: number; updated: number }> = [];
|
|
|
|
for (const userId of users) {
|
|
let userInserted = 0;
|
|
let userUpdated = 0;
|
|
for (const profile of profiles) {
|
|
const result = await upsertProfileForUser(userId, profile);
|
|
if (result === 'inserted') {
|
|
inserted += 1;
|
|
userInserted += 1;
|
|
} else {
|
|
updated += 1;
|
|
userUpdated += 1;
|
|
}
|
|
}
|
|
perUser.push({ user_id: userId, inserted: userInserted, updated: userUpdated });
|
|
}
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
dry_run: dryRun,
|
|
users_targeted: users.length,
|
|
profiles_per_user: profiles.length,
|
|
total_allocation_per_user: PROFILE_SPLIT.aggressive + PROFILE_SPLIT.conservative,
|
|
totals: {
|
|
inserted,
|
|
updated
|
|
},
|
|
per_user: perUser
|
|
}, null, 2));
|
|
};
|
|
|
|
run().catch((error) => {
|
|
console.error(JSON.stringify({
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
}, null, 2));
|
|
process.exit(1);
|
|
});
|